Merge branch 'gitignore-runtime-exs' into 'develop'
[akkoma] / lib / pleroma / moderation_log.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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 "nicknames" => nicknames,
345 "tags" => tags,
346 "action" => "tag"
347 }
348 }) do
349 tags_string = tags |> Enum.join(", ")
350
351 "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
352 end
353
354 def get_log_entry_message(%ModerationLog{
355 data: %{
356 "actor" => %{"nickname" => actor_nickname},
357 "nicknames" => nicknames,
358 "tags" => tags,
359 "action" => "untag"
360 }
361 }) do
362 tags_string = tags |> Enum.join(", ")
363
364 "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
365 end
366
367 def get_log_entry_message(%ModerationLog{
368 data: %{
369 "actor" => %{"nickname" => actor_nickname},
370 "action" => "grant",
371 "subject" => users,
372 "permission" => permission
373 }
374 }) do
375 "@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
376 end
377
378 def get_log_entry_message(%ModerationLog{
379 data: %{
380 "actor" => %{"nickname" => actor_nickname},
381 "action" => "revoke",
382 "subject" => users,
383 "permission" => permission
384 }
385 }) do
386 "@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
387 end
388
389 def get_log_entry_message(%ModerationLog{
390 data: %{
391 "actor" => %{"nickname" => actor_nickname},
392 "action" => "relay_follow",
393 "target" => target
394 }
395 }) do
396 "@#{actor_nickname} followed relay: #{target}"
397 end
398
399 def get_log_entry_message(%ModerationLog{
400 data: %{
401 "actor" => %{"nickname" => actor_nickname},
402 "action" => "relay_unfollow",
403 "target" => target
404 }
405 }) do
406 "@#{actor_nickname} unfollowed relay: #{target}"
407 end
408
409 def get_log_entry_message(
410 %ModerationLog{
411 data: %{
412 "actor" => %{"nickname" => actor_nickname},
413 "action" => "report_update",
414 "subject" => %{"id" => subject_id, "state" => state, "type" => "report"}
415 }
416 } = log
417 ) do
418 "@#{actor_nickname} updated report ##{subject_id}" <>
419 subject_actor_nickname(log, " (on user ", ")") <>
420 " with '#{state}' state"
421 end
422
423 def get_log_entry_message(
424 %ModerationLog{
425 data: %{
426 "actor" => %{"nickname" => actor_nickname},
427 "action" => "report_note",
428 "subject" => %{"id" => subject_id, "type" => "report"},
429 "text" => text
430 }
431 } = log
432 ) do
433 "@#{actor_nickname} added note '#{text}' to report ##{subject_id}" <>
434 subject_actor_nickname(log, " on user ")
435 end
436
437 def get_log_entry_message(
438 %ModerationLog{
439 data: %{
440 "actor" => %{"nickname" => actor_nickname},
441 "action" => "report_note_delete",
442 "subject" => %{"id" => subject_id, "type" => "report"},
443 "text" => text
444 }
445 } = log
446 ) do
447 "@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}" <>
448 subject_actor_nickname(log, " on user ")
449 end
450
451 def get_log_entry_message(%ModerationLog{
452 data: %{
453 "actor" => %{"nickname" => actor_nickname},
454 "action" => "status_update",
455 "subject" => %{"id" => subject_id, "type" => "status"},
456 "sensitive" => nil,
457 "visibility" => visibility
458 }
459 }) do
460 "@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'"
461 end
462
463 def get_log_entry_message(%ModerationLog{
464 data: %{
465 "actor" => %{"nickname" => actor_nickname},
466 "action" => "status_update",
467 "subject" => %{"id" => subject_id, "type" => "status"},
468 "sensitive" => sensitive,
469 "visibility" => nil
470 }
471 }) do
472 "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'"
473 end
474
475 def get_log_entry_message(%ModerationLog{
476 data: %{
477 "actor" => %{"nickname" => actor_nickname},
478 "action" => "status_update",
479 "subject" => %{"id" => subject_id, "type" => "status"},
480 "sensitive" => sensitive,
481 "visibility" => visibility
482 }
483 }) do
484 "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{
485 visibility
486 }'"
487 end
488
489 def get_log_entry_message(%ModerationLog{
490 data: %{
491 "actor" => %{"nickname" => actor_nickname},
492 "action" => "status_delete",
493 "subject_id" => subject_id
494 }
495 }) do
496 "@#{actor_nickname} deleted status ##{subject_id}"
497 end
498
499 def get_log_entry_message(%ModerationLog{
500 data: %{
501 "actor" => %{"nickname" => actor_nickname},
502 "action" => "force_password_reset",
503 "subject" => subjects
504 }
505 }) do
506 "@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
507 end
508
509 def get_log_entry_message(%ModerationLog{
510 data: %{
511 "actor" => %{"nickname" => actor_nickname},
512 "action" => "confirm_email",
513 "subject" => subjects
514 }
515 }) do
516 "@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
517 end
518
519 def get_log_entry_message(%ModerationLog{
520 data: %{
521 "actor" => %{"nickname" => actor_nickname},
522 "action" => "resend_confirmation_email",
523 "subject" => subjects
524 }
525 }) do
526 "@#{actor_nickname} re-sent confirmation email for users: #{
527 users_to_nicknames_string(subjects)
528 }"
529 end
530
531 def get_log_entry_message(%ModerationLog{
532 data: %{
533 "actor" => %{"nickname" => actor_nickname},
534 "action" => "updated_users",
535 "subject" => subjects
536 }
537 }) do
538 "@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
539 end
540
541 def get_log_entry_message(%ModerationLog{
542 data: %{
543 "actor" => %{"nickname" => actor_nickname},
544 "action" => "chat_message_delete",
545 "subject_id" => subject_id
546 }
547 }) do
548 "@#{actor_nickname} deleted chat message ##{subject_id}"
549 end
550
551 def get_log_entry_message(%ModerationLog{
552 data: %{
553 "actor" => %{"nickname" => actor_nickname},
554 "action" => "create_backup",
555 "subject" => %{"nickname" => user_nickname}
556 }
557 }) do
558 "@#{actor_nickname} requested account backup for @#{user_nickname}"
559 end
560
561 defp nicknames_to_string(nicknames) do
562 nicknames
563 |> Enum.map(&"@#{&1}")
564 |> Enum.join(", ")
565 end
566
567 defp users_to_nicknames_string(users) do
568 users
569 |> Enum.map(&"@#{&1["nickname"]}")
570 |> Enum.join(", ")
571 end
572
573 defp subject_actor_nickname(%ModerationLog{data: data}, prefix_msg, postfix_msg \\ "") do
574 case data do
575 %{"subject_actor" => %{"nickname" => subject_actor}} ->
576 [prefix_msg, "@#{subject_actor}", postfix_msg]
577 |> Enum.reject(&(&1 == ""))
578 |> Enum.join()
579
580 _ ->
581 ""
582 end
583 end
584 end