7203423e2d8473df5f67e15b2f510cc38f0fef05
[anni] / lib / pleroma / moderation_log.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.ModerationLog do
6   use Ecto.Schema
7
8   alias Pleroma.Activity
9   alias Pleroma.ModerationLog
10   alias Pleroma.Repo
11   alias Pleroma.User
12
13   import Ecto.Query
14
15   @type t :: %__MODULE__{}
16   @type log_subject :: Activity.t() | User.t() | list(User.t())
17   @type log_params :: %{
18           required(:actor) => User.t(),
19           required(:action) => String.t(),
20           optional(:subject) => log_subject(),
21           optional(:subject_actor) => User.t(),
22           optional(:subject_id) => String.t(),
23           optional(:subjects) => list(User.t()),
24           optional(:permission) => String.t(),
25           optional(:text) => String.t(),
26           optional(:sensitive) => String.t(),
27           optional(:visibility) => String.t(),
28           optional(:followed) => User.t(),
29           optional(:follower) => User.t(),
30           optional(:nicknames) => list(String.t()),
31           optional(:tags) => list(String.t()),
32           optional(:target) => String.t()
33         }
34
35   schema "moderation_log" do
36     field(:data, :map)
37
38     timestamps()
39   end
40
41   def get_all(params) do
42     base_query =
43       get_all_query()
44       |> maybe_filter_by_date(params)
45       |> maybe_filter_by_user(params)
46       |> maybe_filter_by_search(params)
47
48     query_with_pagination = base_query |> paginate_query(params)
49
50     %{
51       items: Repo.all(query_with_pagination),
52       count: Repo.aggregate(base_query, :count, :id)
53     }
54   end
55
56   defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query
57
58   defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do
59     from(q in query,
60       where: q.inserted_at >= ^parse_datetime(start_date)
61     )
62   end
63
64   defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do
65     from(q in query,
66       where: q.inserted_at <= ^parse_datetime(end_date)
67     )
68   end
69
70   defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do
71     from(q in query,
72       where: q.inserted_at >= ^parse_datetime(start_date),
73       where: q.inserted_at <= ^parse_datetime(end_date)
74     )
75   end
76
77   defp maybe_filter_by_user(query, %{user_id: nil}), do: query
78
79   defp maybe_filter_by_user(query, %{user_id: user_id}) do
80     from(q in query,
81       where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id)
82     )
83   end
84
85   defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "",
86     do: query
87
88   defp maybe_filter_by_search(query, %{search: search}) do
89     from(q in query,
90       where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%")
91     )
92   end
93
94   defp paginate_query(query, %{page: page, page_size: page_size}) do
95     from(q in query,
96       limit: ^page_size,
97       offset: ^((page - 1) * page_size)
98     )
99   end
100
101   defp get_all_query do
102     from(q in __MODULE__,
103       order_by: [desc: q.inserted_at]
104     )
105   end
106
107   defp parse_datetime(datetime) do
108     {:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)
109
110     parsed_datetime
111   end
112
113   defp prepare_log_data(%{actor: actor, action: action} = attrs) do
114     %{
115       "actor" => user_to_map(actor),
116       "action" => action,
117       "message" => ""
118     }
119     |> Pleroma.Maps.put_if_present("subject_actor", user_to_map(attrs[:subject_actor]))
120   end
121
122   defp prepare_log_data(attrs), do: attrs
123
124   @spec insert_log(log_params()) :: {:ok, ModerationLog} | {:error, any}
125   def insert_log(%{actor: %User{}, subject: subjects, permission: permission} = attrs) do
126     data =
127       attrs
128       |> prepare_log_data
129       |> Map.merge(%{"subject" => user_to_map(subjects), "permission" => permission})
130
131     insert_log_entry_with_message(%ModerationLog{data: data})
132   end
133
134   def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs)
135       when action in ["report_note_delete", "report_update", "report_note"] do
136     data =
137       attrs
138       |> prepare_log_data
139       |> Pleroma.Maps.put_if_present("text", attrs[:text])
140       |> Map.merge(%{"subject" => report_to_map(subject)})
141
142     insert_log_entry_with_message(%ModerationLog{data: data})
143   end
144
145   def insert_log(
146         %{
147           actor: %User{},
148           action: action,
149           subject: %Activity{} = subject,
150           sensitive: sensitive,
151           visibility: visibility
152         } = attrs
153       )
154       when action == "status_update" do
155     data =
156       attrs
157       |> prepare_log_data
158       |> Map.merge(%{
159         "subject" => status_to_map(subject),
160         "sensitive" => sensitive,
161         "visibility" => visibility
162       })
163
164     insert_log_entry_with_message(%ModerationLog{data: data})
165   end
166
167   def insert_log(%{actor: %User{}, action: action, subject_id: subject_id} = attrs)
168       when action == "status_delete" do
169     data =
170       attrs
171       |> prepare_log_data
172       |> Map.merge(%{"subject_id" => subject_id})
173
174     insert_log_entry_with_message(%ModerationLog{data: data})
175   end
176
177   def insert_log(%{actor: %User{}, subject: subject, action: _action} = attrs) do
178     data =
179       attrs
180       |> prepare_log_data
181       |> Map.merge(%{"subject" => user_to_map(subject)})
182
183     insert_log_entry_with_message(%ModerationLog{data: data})
184   end
185
186   def insert_log(%{actor: %User{}, subjects: subjects, action: _action} = attrs) do
187     data =
188       attrs
189       |> prepare_log_data
190       |> Map.merge(%{"subjects" => user_to_map(subjects)})
191
192     insert_log_entry_with_message(%ModerationLog{data: data})
193   end
194
195   def insert_log(
196         %{
197           actor: %User{},
198           followed: %User{} = followed,
199           follower: %User{} = follower,
200           action: action
201         } = attrs
202       )
203       when action in ["unfollow", "follow"] do
204     data =
205       attrs
206       |> prepare_log_data
207       |> Map.merge(%{"followed" => user_to_map(followed), "follower" => user_to_map(follower)})
208
209     insert_log_entry_with_message(%ModerationLog{data: data})
210   end
211
212   def insert_log(%{
213         actor: %User{} = actor,
214         nicknames: nicknames,
215         tags: tags,
216         action: action
217       }) do
218     %ModerationLog{
219       data: %{
220         "actor" => user_to_map(actor),
221         "nicknames" => nicknames,
222         "tags" => tags,
223         "action" => action,
224         "message" => ""
225       }
226     }
227     |> insert_log_entry_with_message()
228   end
229
230   def insert_log(%{actor: %User{}, action: action, target: target} = attrs)
231       when action in ["relay_follow", "relay_unfollow"] do
232     data =
233       attrs
234       |> prepare_log_data
235       |> Map.merge(%{"target" => target})
236
237     insert_log_entry_with_message(%ModerationLog{data: data})
238   end
239
240   def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_id: subject_id}) do
241     %ModerationLog{
242       data: %{
243         "actor" => %{"nickname" => actor.nickname},
244         "action" => "chat_message_delete",
245         "subject_id" => subject_id
246       }
247     }
248     |> insert_log_entry_with_message()
249   end
250
251   @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
252   defp insert_log_entry_with_message(entry) do
253     entry.data["message"]
254     |> put_in(get_log_entry_message(entry))
255     |> Repo.insert()
256   end
257
258   defp user_to_map(users) when is_list(users) do
259     Enum.map(users, &user_to_map/1)
260   end
261
262   defp user_to_map(%User{} = user) do
263     user
264     |> Map.take([:id, :nickname])
265     |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
266     |> Map.put("type", "user")
267   end
268
269   defp user_to_map(_), do: nil
270
271   defp report_to_map(%Activity{} = report) do
272     %{"type" => "report", "id" => report.id, "state" => report.data["state"]}
273   end
274
275   defp status_to_map(%Activity{} = status) do
276     %{"type" => "status", "id" => status.id}
277   end
278
279   @spec get_log_entry_message(ModerationLog.t()) :: String.t()
280   def get_log_entry_message(%ModerationLog{
281         data: %{
282           "actor" => %{"nickname" => actor_nickname},
283           "action" => action,
284           "followed" => %{"nickname" => followed_nickname},
285           "follower" => %{"nickname" => follower_nickname}
286         }
287       }) do
288     "@#{actor_nickname} made @#{follower_nickname} #{action} @#{followed_nickname}"
289   end
290
291   def get_log_entry_message(%ModerationLog{
292         data: %{
293           "actor" => %{"nickname" => actor_nickname},
294           "action" => "delete",
295           "subject" => subjects
296         }
297       }) do
298     "@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
299   end
300
301   def get_log_entry_message(%ModerationLog{
302         data: %{
303           "actor" => %{"nickname" => actor_nickname},
304           "action" => "create",
305           "subjects" => subjects
306         }
307       }) do
308     "@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
309   end
310
311   def get_log_entry_message(%ModerationLog{
312         data: %{
313           "actor" => %{"nickname" => actor_nickname},
314           "action" => "activate",
315           "subject" => users
316         }
317       }) do
318     "@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
319   end
320
321   def get_log_entry_message(%ModerationLog{
322         data: %{
323           "actor" => %{"nickname" => actor_nickname},
324           "action" => "deactivate",
325           "subject" => users
326         }
327       }) do
328     "@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
329   end
330
331   def get_log_entry_message(%ModerationLog{
332         data: %{
333           "actor" => %{"nickname" => actor_nickname},
334           "action" => "approve",
335           "subject" => users
336         }
337       }) do
338     "@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
339   end
340
341   def get_log_entry_message(%ModerationLog{
342         data: %{
343           "actor" => %{"nickname" => actor_nickname},
344           "action" => "add_suggestion",
345           "subject" => users
346         }
347       }) do
348     "@#{actor_nickname} added suggested users: #{users_to_nicknames_string(users)}"
349   end
350
351   def get_log_entry_message(%ModerationLog{
352         data: %{
353           "actor" => %{"nickname" => actor_nickname},
354           "action" => "remove_suggestion",
355           "subject" => users
356         }
357       }) do
358     "@#{actor_nickname} removed suggested users: #{users_to_nicknames_string(users)}"
359   end
360
361   def get_log_entry_message(%ModerationLog{
362         data: %{
363           "actor" => %{"nickname" => actor_nickname},
364           "nicknames" => nicknames,
365           "tags" => tags,
366           "action" => "tag"
367         }
368       }) do
369     tags_string = tags |> Enum.join(", ")
370
371     "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
372   end
373
374   def get_log_entry_message(%ModerationLog{
375         data: %{
376           "actor" => %{"nickname" => actor_nickname},
377           "nicknames" => nicknames,
378           "tags" => tags,
379           "action" => "untag"
380         }
381       }) do
382     tags_string = tags |> Enum.join(", ")
383
384     "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
385   end
386
387   def get_log_entry_message(%ModerationLog{
388         data: %{
389           "actor" => %{"nickname" => actor_nickname},
390           "action" => "grant",
391           "subject" => users,
392           "permission" => permission
393         }
394       }) do
395     "@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
396   end
397
398   def get_log_entry_message(%ModerationLog{
399         data: %{
400           "actor" => %{"nickname" => actor_nickname},
401           "action" => "revoke",
402           "subject" => users,
403           "permission" => permission
404         }
405       }) do
406     "@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
407   end
408
409   def get_log_entry_message(%ModerationLog{
410         data: %{
411           "actor" => %{"nickname" => actor_nickname},
412           "action" => "relay_follow",
413           "target" => target
414         }
415       }) do
416     "@#{actor_nickname} followed relay: #{target}"
417   end
418
419   def get_log_entry_message(%ModerationLog{
420         data: %{
421           "actor" => %{"nickname" => actor_nickname},
422           "action" => "relay_unfollow",
423           "target" => target
424         }
425       }) do
426     "@#{actor_nickname} unfollowed relay: #{target}"
427   end
428
429   def get_log_entry_message(
430         %ModerationLog{
431           data: %{
432             "actor" => %{"nickname" => actor_nickname},
433             "action" => "report_update",
434             "subject" => %{"id" => subject_id, "state" => state, "type" => "report"}
435           }
436         } = log
437       ) do
438     "@#{actor_nickname} updated report ##{subject_id}" <>
439       subject_actor_nickname(log, " (on user ", ")") <>
440       " with '#{state}' state"
441   end
442
443   def get_log_entry_message(
444         %ModerationLog{
445           data: %{
446             "actor" => %{"nickname" => actor_nickname},
447             "action" => "report_note",
448             "subject" => %{"id" => subject_id, "type" => "report"},
449             "text" => text
450           }
451         } = log
452       ) do
453     "@#{actor_nickname} added note '#{text}' to report ##{subject_id}" <>
454       subject_actor_nickname(log, " on user ")
455   end
456
457   def get_log_entry_message(
458         %ModerationLog{
459           data: %{
460             "actor" => %{"nickname" => actor_nickname},
461             "action" => "report_note_delete",
462             "subject" => %{"id" => subject_id, "type" => "report"},
463             "text" => text
464           }
465         } = log
466       ) do
467     "@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}" <>
468       subject_actor_nickname(log, " on user ")
469   end
470
471   def get_log_entry_message(%ModerationLog{
472         data: %{
473           "actor" => %{"nickname" => actor_nickname},
474           "action" => "status_update",
475           "subject" => %{"id" => subject_id, "type" => "status"},
476           "sensitive" => nil,
477           "visibility" => visibility
478         }
479       }) do
480     "@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'"
481   end
482
483   def get_log_entry_message(%ModerationLog{
484         data: %{
485           "actor" => %{"nickname" => actor_nickname},
486           "action" => "status_update",
487           "subject" => %{"id" => subject_id, "type" => "status"},
488           "sensitive" => sensitive,
489           "visibility" => nil
490         }
491       }) do
492     "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'"
493   end
494
495   def get_log_entry_message(%ModerationLog{
496         data: %{
497           "actor" => %{"nickname" => actor_nickname},
498           "action" => "status_update",
499           "subject" => %{"id" => subject_id, "type" => "status"},
500           "sensitive" => sensitive,
501           "visibility" => visibility
502         }
503       }) do
504     "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{visibility}'"
505   end
506
507   def get_log_entry_message(%ModerationLog{
508         data: %{
509           "actor" => %{"nickname" => actor_nickname},
510           "action" => "status_delete",
511           "subject_id" => subject_id
512         }
513       }) do
514     "@#{actor_nickname} deleted status ##{subject_id}"
515   end
516
517   def get_log_entry_message(%ModerationLog{
518         data: %{
519           "actor" => %{"nickname" => actor_nickname},
520           "action" => "force_password_reset",
521           "subject" => subjects
522         }
523       }) do
524     "@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
525   end
526
527   def get_log_entry_message(%ModerationLog{
528         data: %{
529           "actor" => %{"nickname" => actor_nickname},
530           "action" => "confirm_email",
531           "subject" => subjects
532         }
533       }) do
534     "@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
535   end
536
537   def get_log_entry_message(%ModerationLog{
538         data: %{
539           "actor" => %{"nickname" => actor_nickname},
540           "action" => "resend_confirmation_email",
541           "subject" => subjects
542         }
543       }) do
544     "@#{actor_nickname} re-sent confirmation email for users: #{users_to_nicknames_string(subjects)}"
545   end
546
547   def get_log_entry_message(%ModerationLog{
548         data: %{
549           "actor" => %{"nickname" => actor_nickname},
550           "action" => "updated_users",
551           "subject" => subjects
552         }
553       }) do
554     "@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
555   end
556
557   def get_log_entry_message(%ModerationLog{
558         data: %{
559           "actor" => %{"nickname" => actor_nickname},
560           "action" => "chat_message_delete",
561           "subject_id" => subject_id
562         }
563       }) do
564     "@#{actor_nickname} deleted chat message ##{subject_id}"
565   end
566
567   def get_log_entry_message(%ModerationLog{
568         data: %{
569           "actor" => %{"nickname" => actor_nickname},
570           "action" => "create_backup",
571           "subject" => %{"nickname" => user_nickname}
572         }
573       }) do
574     "@#{actor_nickname} requested account backup for @#{user_nickname}"
575   end
576
577   defp nicknames_to_string(nicknames) do
578     nicknames
579     |> Enum.map(&"@#{&1}")
580     |> Enum.join(", ")
581   end
582
583   defp users_to_nicknames_string(users) do
584     users
585     |> Enum.map(&"@#{&1["nickname"]}")
586     |> Enum.join(", ")
587   end
588
589   defp subject_actor_nickname(%ModerationLog{data: data}, prefix_msg, postfix_msg \\ "") do
590     case data do
591       %{"subject_actor" => %{"nickname" => subject_actor}} ->
592         [prefix_msg, "@#{subject_actor}", postfix_msg]
593         |> Enum.reject(&(&1 == ""))
594         |> Enum.join()
595
596       _ ->
597         ""
598     end
599   end
600 end