99c729473696c84e36731791d62d54d553c30d40
[akkoma] / lib / pleroma / web / activity_pub / activity_pub.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.ActivityPub do
6 alias Pleroma.Activity
7 alias Pleroma.Activity.Ir.Topics
8 alias Pleroma.Config
9 alias Pleroma.Constants
10 alias Pleroma.Conversation
11 alias Pleroma.Conversation.Participation
12 alias Pleroma.Filter
13 alias Pleroma.Maps
14 alias Pleroma.Notification
15 alias Pleroma.Object
16 alias Pleroma.Object.Containment
17 alias Pleroma.Object.Fetcher
18 alias Pleroma.Pagination
19 alias Pleroma.Repo
20 alias Pleroma.Upload
21 alias Pleroma.User
22 alias Pleroma.Web.ActivityPub.MRF
23 alias Pleroma.Web.ActivityPub.Transmogrifier
24 alias Pleroma.Web.Streamer
25 alias Pleroma.Web.WebFinger
26 alias Pleroma.Workers.BackgroundWorker
27
28 import Ecto.Query
29 import Pleroma.Web.ActivityPub.Utils
30 import Pleroma.Web.ActivityPub.Visibility
31
32 require Logger
33 require Pleroma.Constants
34
35 defp get_recipients(%{"type" => "Create"} = data) do
36 to = Map.get(data, "to", [])
37 cc = Map.get(data, "cc", [])
38 bcc = Map.get(data, "bcc", [])
39 actor = Map.get(data, "actor", [])
40 recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
41 {recipients, to, cc}
42 end
43
44 defp get_recipients(data) do
45 to = Map.get(data, "to", [])
46 cc = Map.get(data, "cc", [])
47 bcc = Map.get(data, "bcc", [])
48 recipients = Enum.concat([to, cc, bcc])
49 {recipients, to, cc}
50 end
51
52 defp check_actor_is_active(nil), do: true
53
54 defp check_actor_is_active(actor) when is_binary(actor) do
55 case User.get_cached_by_ap_id(actor) do
56 %User{deactivated: deactivated} -> not deactivated
57 _ -> false
58 end
59 end
60
61 defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
62 limit = Config.get([:instance, :remote_limit])
63 String.length(content) <= limit
64 end
65
66 defp check_remote_limit(_), do: true
67
68 def increase_note_count_if_public(actor, object) do
69 if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
70 end
71
72 def decrease_note_count_if_public(actor, object) do
73 if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
74 end
75
76 defp increase_replies_count_if_reply(%{
77 "object" => %{"inReplyTo" => reply_ap_id} = object,
78 "type" => "Create"
79 }) do
80 if is_public?(object) do
81 Object.increase_replies_count(reply_ap_id)
82 end
83 end
84
85 defp increase_replies_count_if_reply(_create_data), do: :noop
86
87 @object_types ~w[ChatMessage Question Answer Audio Video Event Article]
88 @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
89 def persist(%{"type" => type} = object, meta) when type in @object_types do
90 with {:ok, object} <- Object.create(object) do
91 {:ok, object, meta}
92 end
93 end
94
95 def persist(object, meta) do
96 with local <- Keyword.fetch!(meta, :local),
97 {recipients, _, _} <- get_recipients(object),
98 {:ok, activity} <-
99 Repo.insert(%Activity{
100 data: object,
101 local: local,
102 recipients: recipients,
103 actor: object["actor"]
104 }),
105 # TODO: add tests for expired activities, when Note type will be supported in new pipeline
106 {:ok, _} <- maybe_create_activity_expiration(activity) do
107 {:ok, activity, meta}
108 end
109 end
110
111 @spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
112 def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
113 with nil <- Activity.normalize(map),
114 map <- lazy_put_activity_defaults(map, fake),
115 {_, true} <- {:actor_check, bypass_actor_check || check_actor_is_active(map["actor"])},
116 {_, true} <- {:remote_limit_pass, check_remote_limit(map)},
117 {:ok, map} <- MRF.filter(map),
118 {recipients, _, _} = get_recipients(map),
119 {:fake, false, map, recipients} <- {:fake, fake, map, recipients},
120 {:containment, :ok} <- {:containment, Containment.contain_child(map)},
121 {:ok, map, object} <- insert_full_object(map),
122 {:ok, activity} <- insert_activity_with_expiration(map, local, recipients) do
123 # Splice in the child object if we have one.
124 activity = Maps.put_if_present(activity, :object, object)
125
126 BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
127
128 {:ok, activity}
129 else
130 %Activity{} = activity ->
131 {:ok, activity}
132
133 {:actor_check, _} ->
134 {:error, false}
135
136 {:containment, _} = error ->
137 error
138
139 {:error, _} = error ->
140 error
141
142 {:fake, true, map, recipients} ->
143 activity = %Activity{
144 data: map,
145 local: local,
146 actor: map["actor"],
147 recipients: recipients,
148 id: "pleroma:fakeid"
149 }
150
151 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
152 {:ok, activity}
153
154 {:remote_limit_pass, _} ->
155 {:error, :remote_limit}
156
157 {:reject, _} = e ->
158 {:error, e}
159 end
160 end
161
162 defp insert_activity_with_expiration(data, local, recipients) do
163 struct = %Activity{
164 data: data,
165 local: local,
166 actor: data["actor"],
167 recipients: recipients
168 }
169
170 with {:ok, activity} <- Repo.insert(struct) do
171 maybe_create_activity_expiration(activity)
172 end
173 end
174
175 def notify_and_stream(activity) do
176 Notification.create_notifications(activity)
177
178 conversation = create_or_bump_conversation(activity, activity.actor)
179 participations = get_participations(conversation)
180 stream_out(activity)
181 stream_out_participations(participations)
182 end
183
184 defp maybe_create_activity_expiration(
185 %{data: %{"expires_at" => %DateTime{} = expires_at}} = activity
186 ) do
187 with {:ok, _job} <-
188 Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
189 activity_id: activity.id,
190 expires_at: expires_at
191 }) do
192 {:ok, activity}
193 end
194 end
195
196 defp maybe_create_activity_expiration(activity), do: {:ok, activity}
197
198 defp create_or_bump_conversation(activity, actor) do
199 with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
200 %User{} = user <- User.get_cached_by_ap_id(actor) do
201 Participation.mark_as_read(user, conversation)
202 {:ok, conversation}
203 end
204 end
205
206 defp get_participations({:ok, conversation}) do
207 conversation
208 |> Repo.preload(:participations, force: true)
209 |> Map.get(:participations)
210 end
211
212 defp get_participations(_), do: []
213
214 def stream_out_participations(participations) do
215 participations =
216 participations
217 |> Repo.preload(:user)
218
219 Streamer.stream("participation", participations)
220 end
221
222 def stream_out_participations(%Object{data: %{"context" => context}}, user) do
223 with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do
224 conversation = Repo.preload(conversation, :participations)
225
226 last_activity_id =
227 fetch_latest_direct_activity_id_for_context(conversation.ap_id, %{
228 user: user,
229 blocking_user: user
230 })
231
232 if last_activity_id do
233 stream_out_participations(conversation.participations)
234 end
235 end
236 end
237
238 def stream_out_participations(_, _), do: :noop
239
240 def stream_out(%Activity{data: %{"type" => data_type}} = activity)
241 when data_type in ["Create", "Announce", "Delete"] do
242 activity
243 |> Topics.get_activity_topics()
244 |> Streamer.stream(activity)
245 end
246
247 def stream_out(_activity) do
248 :noop
249 end
250
251 @spec create(map(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
252 def create(params, fake \\ false) do
253 with {:ok, result} <- Repo.transaction(fn -> do_create(params, fake) end) do
254 result
255 end
256 end
257
258 defp do_create(%{to: to, actor: actor, context: context, object: object} = params, fake) do
259 additional = params[:additional] || %{}
260 # only accept false as false value
261 local = !(params[:local] == false)
262 published = params[:published]
263 quick_insert? = Config.get([:env]) == :benchmark
264
265 create_data =
266 make_create_data(
267 %{to: to, actor: actor, published: published, context: context, object: object},
268 additional
269 )
270
271 with {:ok, activity} <- insert(create_data, local, fake),
272 {:fake, false, activity} <- {:fake, fake, activity},
273 _ <- increase_replies_count_if_reply(create_data),
274 {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
275 {:ok, _actor} <- increase_note_count_if_public(actor, activity),
276 _ <- notify_and_stream(activity),
277 :ok <- maybe_federate(activity) do
278 {:ok, activity}
279 else
280 {:quick_insert, true, activity} ->
281 {:ok, activity}
282
283 {:fake, true, activity} ->
284 {:ok, activity}
285
286 {:error, message} ->
287 Repo.rollback(message)
288 end
289 end
290
291 @spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}
292 def listen(%{to: to, actor: actor, context: context, object: object} = params) do
293 additional = params[:additional] || %{}
294 # only accept false as false value
295 local = !(params[:local] == false)
296 published = params[:published]
297
298 listen_data =
299 make_listen_data(
300 %{to: to, actor: actor, published: published, context: context, object: object},
301 additional
302 )
303
304 with {:ok, activity} <- insert(listen_data, local),
305 _ <- notify_and_stream(activity),
306 :ok <- maybe_federate(activity) do
307 {:ok, activity}
308 end
309 end
310
311 @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
312 {:ok, Activity.t()} | nil | {:error, any()}
313 def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
314 with {:ok, result} <-
315 Repo.transaction(fn -> do_unfollow(follower, followed, activity_id, local) end) do
316 result
317 end
318 end
319
320 defp do_unfollow(follower, followed, activity_id, local) do
321 with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
322 {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
323 unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
324 {:ok, activity} <- insert(unfollow_data, local),
325 _ <- notify_and_stream(activity),
326 :ok <- maybe_federate(activity) do
327 {:ok, activity}
328 else
329 nil -> nil
330 {:error, error} -> Repo.rollback(error)
331 end
332 end
333
334 @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
335 def flag(params) do
336 with {:ok, result} <- Repo.transaction(fn -> do_flag(params) end) do
337 result
338 end
339 end
340
341 defp do_flag(
342 %{
343 actor: actor,
344 context: _context,
345 account: account,
346 statuses: statuses,
347 content: content
348 } = params
349 ) do
350 # only accept false as false value
351 local = !(params[:local] == false)
352 forward = !(params[:forward] == false)
353
354 additional = params[:additional] || %{}
355
356 additional =
357 if forward do
358 Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
359 else
360 Map.merge(additional, %{"to" => [], "cc" => []})
361 end
362
363 with flag_data <- make_flag_data(params, additional),
364 {:ok, activity} <- insert(flag_data, local),
365 {:ok, stripped_activity} <- strip_report_status_data(activity),
366 _ <- notify_and_stream(activity),
367 :ok <-
368 maybe_federate(stripped_activity) do
369 User.all_superusers()
370 |> Enum.filter(fn user -> not is_nil(user.email) end)
371 |> Enum.each(fn superuser ->
372 superuser
373 |> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
374 |> Pleroma.Emails.Mailer.deliver_async()
375 end)
376
377 {:ok, activity}
378 else
379 {:error, error} -> Repo.rollback(error)
380 end
381 end
382
383 @spec move(User.t(), User.t(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
384 def move(%User{} = origin, %User{} = target, local \\ true) do
385 params = %{
386 "type" => "Move",
387 "actor" => origin.ap_id,
388 "object" => origin.ap_id,
389 "target" => target.ap_id
390 }
391
392 with true <- origin.ap_id in target.also_known_as,
393 {:ok, activity} <- insert(params, local),
394 _ <- notify_and_stream(activity) do
395 maybe_federate(activity)
396
397 BackgroundWorker.enqueue("move_following", %{
398 "origin_id" => origin.id,
399 "target_id" => target.id
400 })
401
402 {:ok, activity}
403 else
404 false -> {:error, "Target account must have the origin in `alsoKnownAs`"}
405 err -> err
406 end
407 end
408
409 def fetch_activities_for_context_query(context, opts) do
410 public = [Constants.as_public()]
411
412 recipients =
413 if opts[:user],
414 do: [opts[:user].ap_id | User.following(opts[:user])] ++ public,
415 else: public
416
417 from(activity in Activity)
418 |> maybe_preload_objects(opts)
419 |> maybe_preload_bookmarks(opts)
420 |> maybe_set_thread_muted_field(opts)
421 |> restrict_blocked(opts)
422 |> restrict_recipients(recipients, opts[:user])
423 |> restrict_filtered(opts)
424 |> where(
425 [activity],
426 fragment(
427 "?->>'type' = ? and ?->>'context' = ?",
428 activity.data,
429 "Create",
430 activity.data,
431 ^context
432 )
433 )
434 |> exclude_poll_votes(opts)
435 |> exclude_id(opts)
436 |> order_by([activity], desc: activity.id)
437 end
438
439 @spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
440 def fetch_activities_for_context(context, opts \\ %{}) do
441 context
442 |> fetch_activities_for_context_query(opts)
443 |> Repo.all()
444 end
445
446 @spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
447 FlakeId.Ecto.CompatType.t() | nil
448 def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
449 context
450 |> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
451 |> restrict_visibility(%{visibility: "direct"})
452 |> limit(1)
453 |> select([a], a.id)
454 |> Repo.one()
455 end
456
457 @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
458 def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
459 opts = Map.delete(opts, :user)
460
461 [Constants.as_public()]
462 |> fetch_activities_query(opts)
463 |> restrict_unlisted(opts)
464 |> Pagination.fetch_paginated(opts, pagination)
465 end
466
467 @spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
468 def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
469 opts
470 |> Map.put(:restrict_unlisted, true)
471 |> fetch_public_or_unlisted_activities(pagination)
472 end
473
474 @valid_visibilities ~w[direct unlisted public private]
475
476 defp restrict_visibility(query, %{visibility: visibility})
477 when is_list(visibility) do
478 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
479 from(
480 a in query,
481 where:
482 fragment(
483 "activity_visibility(?, ?, ?) = ANY (?)",
484 a.actor,
485 a.recipients,
486 a.data,
487 ^visibility
488 )
489 )
490 else
491 Logger.error("Could not restrict visibility to #{visibility}")
492 end
493 end
494
495 defp restrict_visibility(query, %{visibility: visibility})
496 when visibility in @valid_visibilities do
497 from(
498 a in query,
499 where:
500 fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
501 )
502 end
503
504 defp restrict_visibility(_query, %{visibility: visibility})
505 when visibility not in @valid_visibilities do
506 Logger.error("Could not restrict visibility to #{visibility}")
507 end
508
509 defp restrict_visibility(query, _visibility), do: query
510
511 defp exclude_visibility(query, %{exclude_visibilities: visibility})
512 when is_list(visibility) do
513 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
514 from(
515 a in query,
516 where:
517 not fragment(
518 "activity_visibility(?, ?, ?) = ANY (?)",
519 a.actor,
520 a.recipients,
521 a.data,
522 ^visibility
523 )
524 )
525 else
526 Logger.error("Could not exclude visibility to #{visibility}")
527 query
528 end
529 end
530
531 defp exclude_visibility(query, %{exclude_visibilities: visibility})
532 when visibility in @valid_visibilities do
533 from(
534 a in query,
535 where:
536 not fragment(
537 "activity_visibility(?, ?, ?) = ?",
538 a.actor,
539 a.recipients,
540 a.data,
541 ^visibility
542 )
543 )
544 end
545
546 defp exclude_visibility(query, %{exclude_visibilities: visibility})
547 when visibility not in [nil | @valid_visibilities] do
548 Logger.error("Could not exclude visibility to #{visibility}")
549 query
550 end
551
552 defp exclude_visibility(query, _visibility), do: query
553
554 defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
555 do: query
556
557 defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: true}}, _),
558 do: query
559
560 defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
561 from(
562 a in query,
563 where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
564 )
565 end
566
567 defp restrict_thread_visibility(query, _, _), do: query
568
569 def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
570 params =
571 params
572 |> Map.put(:user, reading_user)
573 |> Map.put(:actor_id, user.ap_id)
574
575 %{
576 godmode: params[:godmode],
577 reading_user: reading_user
578 }
579 |> user_activities_recipients()
580 |> fetch_activities(params)
581 |> Enum.reverse()
582 end
583
584 def fetch_user_activities(user, reading_user, params \\ %{}) do
585 params =
586 params
587 |> Map.put(:type, ["Create", "Announce"])
588 |> Map.put(:user, reading_user)
589 |> Map.put(:actor_id, user.ap_id)
590 |> Map.put(:pinned_activity_ids, user.pinned_activities)
591
592 params =
593 if User.blocks?(reading_user, user) do
594 params
595 else
596 params
597 |> Map.put(:blocking_user, reading_user)
598 |> Map.put(:muting_user, reading_user)
599 end
600
601 %{
602 godmode: params[:godmode],
603 reading_user: reading_user
604 }
605 |> user_activities_recipients()
606 |> fetch_activities(params)
607 |> Enum.reverse()
608 end
609
610 def fetch_statuses(reading_user, params) do
611 params = Map.put(params, :type, ["Create", "Announce"])
612
613 %{
614 godmode: params[:godmode],
615 reading_user: reading_user
616 }
617 |> user_activities_recipients()
618 |> fetch_activities(params, :offset)
619 |> Enum.reverse()
620 end
621
622 defp user_activities_recipients(%{godmode: true}), do: []
623
624 defp user_activities_recipients(%{reading_user: reading_user}) do
625 if reading_user do
626 [Constants.as_public(), reading_user.ap_id | User.following(reading_user)]
627 else
628 [Constants.as_public()]
629 end
630 end
631
632 defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do
633 raise "Can't use the child object without preloading!"
634 end
635
636 defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do
637 from(
638 [activity, object] in query,
639 where:
640 fragment(
641 "?->>'type' != ? or ?->>'actor' != ?",
642 activity.data,
643 "Announce",
644 object.data,
645 ^actor
646 )
647 )
648 end
649
650 defp restrict_announce_object_actor(query, _), do: query
651
652 defp restrict_since(query, %{since_id: ""}), do: query
653
654 defp restrict_since(query, %{since_id: since_id}) do
655 from(activity in query, where: activity.id > ^since_id)
656 end
657
658 defp restrict_since(query, _), do: query
659
660 defp restrict_tag_reject(_query, %{tag_reject: _tag_reject, skip_preload: true}) do
661 raise "Can't use the child object without preloading!"
662 end
663
664 defp restrict_tag_reject(query, %{tag_reject: [_ | _] = tag_reject}) do
665 from(
666 [_activity, object] in query,
667 where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
668 )
669 end
670
671 defp restrict_tag_reject(query, _), do: query
672
673 defp restrict_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do
674 raise "Can't use the child object without preloading!"
675 end
676
677 defp restrict_tag_all(query, %{tag_all: [_ | _] = tag_all}) do
678 from(
679 [_activity, object] in query,
680 where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
681 )
682 end
683
684 defp restrict_tag_all(query, _), do: query
685
686 defp restrict_tag(_query, %{tag: _tag, skip_preload: true}) do
687 raise "Can't use the child object without preloading!"
688 end
689
690 defp restrict_tag(query, %{tag: tag}) when is_list(tag) do
691 from(
692 [_activity, object] in query,
693 where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
694 )
695 end
696
697 defp restrict_tag(query, %{tag: tag}) when is_binary(tag) do
698 from(
699 [_activity, object] in query,
700 where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
701 )
702 end
703
704 defp restrict_tag(query, _), do: query
705
706 defp restrict_recipients(query, [], _user), do: query
707
708 defp restrict_recipients(query, recipients, nil) do
709 from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
710 end
711
712 defp restrict_recipients(query, recipients, user) do
713 from(
714 activity in query,
715 where: fragment("? && ?", ^recipients, activity.recipients),
716 or_where: activity.actor == ^user.ap_id
717 )
718 end
719
720 defp restrict_local(query, %{local_only: true}) do
721 from(activity in query, where: activity.local == true)
722 end
723
724 defp restrict_local(query, _), do: query
725
726 defp restrict_actor(query, %{actor_id: actor_id}) do
727 from(activity in query, where: activity.actor == ^actor_id)
728 end
729
730 defp restrict_actor(query, _), do: query
731
732 defp restrict_type(query, %{type: type}) when is_binary(type) do
733 from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
734 end
735
736 defp restrict_type(query, %{type: type}) do
737 from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
738 end
739
740 defp restrict_type(query, _), do: query
741
742 defp restrict_state(query, %{state: state}) do
743 from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
744 end
745
746 defp restrict_state(query, _), do: query
747
748 defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
749 from(
750 [_activity, object] in query,
751 where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
752 )
753 end
754
755 defp restrict_favorited_by(query, _), do: query
756
757 defp restrict_media(_query, %{only_media: _val, skip_preload: true}) do
758 raise "Can't use the child object without preloading!"
759 end
760
761 defp restrict_media(query, %{only_media: true}) do
762 from(
763 [activity, object] in query,
764 where: fragment("(?)->>'type' = ?", activity.data, "Create"),
765 where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
766 )
767 end
768
769 defp restrict_media(query, _), do: query
770
771 defp restrict_replies(query, %{exclude_replies: true}) do
772 from(
773 [_activity, object] in query,
774 where: fragment("?->>'inReplyTo' is null", object.data)
775 )
776 end
777
778 defp restrict_replies(query, %{
779 reply_filtering_user: %User{} = user,
780 reply_visibility: "self"
781 }) do
782 from(
783 [activity, object] in query,
784 where:
785 fragment(
786 "?->>'inReplyTo' is null OR ? = ANY(?)",
787 object.data,
788 ^user.ap_id,
789 activity.recipients
790 )
791 )
792 end
793
794 defp restrict_replies(query, %{
795 reply_filtering_user: %User{} = user,
796 reply_visibility: "following"
797 }) do
798 from(
799 [activity, object] in query,
800 where:
801 fragment(
802 """
803 ?->>'type' != 'Create' -- This isn't a Create
804 OR ?->>'inReplyTo' is null -- this isn't a reply
805 OR ? && array_remove(?, ?) -- The recipient is us or one of our friends,
806 -- unless they are the author (because authors
807 -- are also part of the recipients). This leads
808 -- to a bug that self-replies by friends won't
809 -- show up.
810 OR ? = ? -- The actor is us
811 """,
812 activity.data,
813 object.data,
814 ^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
815 activity.recipients,
816 activity.actor,
817 activity.actor,
818 ^user.ap_id
819 )
820 )
821 end
822
823 defp restrict_replies(query, _), do: query
824
825 defp restrict_reblogs(query, %{exclude_reblogs: true}) do
826 from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
827 end
828
829 defp restrict_reblogs(query, _), do: query
830
831 defp restrict_muted(query, %{with_muted: true}), do: query
832
833 defp restrict_muted(query, %{muting_user: %User{} = user} = opts) do
834 mutes = opts[:muted_users_ap_ids] || User.muted_users_ap_ids(user)
835
836 query =
837 from([activity] in query,
838 where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
839 where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
840 )
841
842 unless opts[:skip_preload] do
843 from([thread_mute: tm] in query, where: is_nil(tm.user_id))
844 else
845 query
846 end
847 end
848
849 defp restrict_muted(query, _), do: query
850
851 defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
852 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
853 domain_blocks = user.domain_blocks || []
854
855 following_ap_ids = User.get_friends_ap_ids(user)
856
857 query =
858 if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
859
860 from(
861 [activity, object: o] in query,
862 where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
863 where:
864 fragment(
865 "((not (? && ?)) or ? = ?)",
866 activity.recipients,
867 ^blocked_ap_ids,
868 activity.actor,
869 ^user.ap_id
870 ),
871 where:
872 fragment(
873 "recipients_contain_blocked_domains(?, ?) = false",
874 activity.recipients,
875 ^domain_blocks
876 ),
877 where:
878 fragment(
879 "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
880 activity.data,
881 activity.data,
882 ^blocked_ap_ids
883 ),
884 where:
885 fragment(
886 "(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)",
887 activity.actor,
888 ^domain_blocks,
889 activity.actor,
890 ^following_ap_ids
891 ),
892 where:
893 fragment(
894 "(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)",
895 o.data,
896 ^domain_blocks,
897 o.data,
898 ^following_ap_ids
899 )
900 )
901 end
902
903 defp restrict_blocked(query, _), do: query
904
905 defp restrict_unlisted(query, %{restrict_unlisted: true}) do
906 from(
907 activity in query,
908 where:
909 fragment(
910 "not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
911 activity.data,
912 ^[Constants.as_public()]
913 )
914 )
915 end
916
917 defp restrict_unlisted(query, _), do: query
918
919 defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do
920 from(activity in query, where: activity.id in ^ids)
921 end
922
923 defp restrict_pinned(query, _), do: query
924
925 defp restrict_muted_reblogs(query, %{muting_user: %User{} = user} = opts) do
926 muted_reblogs = opts[:reblog_muted_users_ap_ids] || User.reblog_muted_users_ap_ids(user)
927
928 from(
929 activity in query,
930 where:
931 fragment(
932 "not ( ?->>'type' = 'Announce' and ? = ANY(?))",
933 activity.data,
934 activity.actor,
935 ^muted_reblogs
936 )
937 )
938 end
939
940 defp restrict_muted_reblogs(query, _), do: query
941
942 defp restrict_instance(query, %{instance: instance}) do
943 users =
944 from(
945 u in User,
946 select: u.ap_id,
947 where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
948 )
949 |> Repo.all()
950
951 from(activity in query, where: activity.actor in ^users)
952 end
953
954 defp restrict_instance(query, _), do: query
955
956 defp restrict_filtered(query, %{user: %User{} = user}) do
957 case Filter.compose_regex(user) do
958 nil ->
959 query
960
961 regex ->
962 from([activity, object] in query,
963 where:
964 fragment("not(?->>'content' ~* ?)", object.data, ^regex) or
965 activity.actor == ^user.ap_id
966 )
967 end
968 end
969
970 defp restrict_filtered(query, %{blocking_user: %User{} = user}) do
971 restrict_filtered(query, %{user: user})
972 end
973
974 defp restrict_filtered(query, _), do: query
975
976 defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
977
978 defp exclude_poll_votes(query, _) do
979 if has_named_binding?(query, :object) do
980 from([activity, object: o] in query,
981 where: fragment("not(?->>'type' = ?)", o.data, "Answer")
982 )
983 else
984 query
985 end
986 end
987
988 defp exclude_chat_messages(query, %{include_chat_messages: true}), do: query
989
990 defp exclude_chat_messages(query, _) do
991 if has_named_binding?(query, :object) do
992 from([activity, object: o] in query,
993 where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage")
994 )
995 else
996 query
997 end
998 end
999
1000 defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query
1001
1002 defp exclude_invisible_actors(query, _opts) do
1003 invisible_ap_ids =
1004 User.Query.build(%{invisible: true, select: [:ap_id]})
1005 |> Repo.all()
1006 |> Enum.map(fn %{ap_id: ap_id} -> ap_id end)
1007
1008 from([activity] in query, where: activity.actor not in ^invisible_ap_ids)
1009 end
1010
1011 defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do
1012 from(activity in query, where: activity.id != ^id)
1013 end
1014
1015 defp exclude_id(query, _), do: query
1016
1017 defp maybe_preload_objects(query, %{skip_preload: true}), do: query
1018
1019 defp maybe_preload_objects(query, _) do
1020 query
1021 |> Activity.with_preloaded_object()
1022 end
1023
1024 defp maybe_preload_bookmarks(query, %{skip_preload: true}), do: query
1025
1026 defp maybe_preload_bookmarks(query, opts) do
1027 query
1028 |> Activity.with_preloaded_bookmark(opts[:user])
1029 end
1030
1031 defp maybe_preload_report_notes(query, %{preload_report_notes: true}) do
1032 query
1033 |> Activity.with_preloaded_report_notes()
1034 end
1035
1036 defp maybe_preload_report_notes(query, _), do: query
1037
1038 defp maybe_set_thread_muted_field(query, %{skip_preload: true}), do: query
1039
1040 defp maybe_set_thread_muted_field(query, opts) do
1041 query
1042 |> Activity.with_set_thread_muted_field(opts[:muting_user] || opts[:user])
1043 end
1044
1045 defp maybe_order(query, %{order: :desc}) do
1046 query
1047 |> order_by(desc: :id)
1048 end
1049
1050 defp maybe_order(query, %{order: :asc}) do
1051 query
1052 |> order_by(asc: :id)
1053 end
1054
1055 defp maybe_order(query, _), do: query
1056
1057 defp fetch_activities_query_ap_ids_ops(opts) do
1058 source_user = opts[:muting_user]
1059 ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
1060
1061 ap_id_relationships =
1062 if opts[:blocking_user] && opts[:blocking_user] == source_user do
1063 [:block | ap_id_relationships]
1064 else
1065 ap_id_relationships
1066 end
1067
1068 preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
1069
1070 restrict_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
1071 restrict_muted_opts = Map.merge(%{muted_users_ap_ids: preloaded_ap_ids[:mute]}, opts)
1072
1073 restrict_muted_reblogs_opts =
1074 Map.merge(%{reblog_muted_users_ap_ids: preloaded_ap_ids[:reblog_mute]}, opts)
1075
1076 {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts}
1077 end
1078
1079 def fetch_activities_query(recipients, opts \\ %{}) do
1080 {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} =
1081 fetch_activities_query_ap_ids_ops(opts)
1082
1083 config = %{
1084 skip_thread_containment: Config.get([:instance, :skip_thread_containment])
1085 }
1086
1087 Activity
1088 |> maybe_preload_objects(opts)
1089 |> maybe_preload_bookmarks(opts)
1090 |> maybe_preload_report_notes(opts)
1091 |> maybe_set_thread_muted_field(opts)
1092 |> maybe_order(opts)
1093 |> restrict_recipients(recipients, opts[:user])
1094 |> restrict_replies(opts)
1095 |> restrict_tag(opts)
1096 |> restrict_tag_reject(opts)
1097 |> restrict_tag_all(opts)
1098 |> restrict_since(opts)
1099 |> restrict_local(opts)
1100 |> restrict_actor(opts)
1101 |> restrict_type(opts)
1102 |> restrict_state(opts)
1103 |> restrict_favorited_by(opts)
1104 |> restrict_blocked(restrict_blocked_opts)
1105 |> restrict_muted(restrict_muted_opts)
1106 |> restrict_filtered(opts)
1107 |> restrict_media(opts)
1108 |> restrict_visibility(opts)
1109 |> restrict_thread_visibility(opts, config)
1110 |> restrict_reblogs(opts)
1111 |> restrict_pinned(opts)
1112 |> restrict_muted_reblogs(restrict_muted_reblogs_opts)
1113 |> restrict_instance(opts)
1114 |> restrict_announce_object_actor(opts)
1115 |> restrict_filtered(opts)
1116 |> Activity.restrict_deactivated_users()
1117 |> exclude_poll_votes(opts)
1118 |> exclude_chat_messages(opts)
1119 |> exclude_invisible_actors(opts)
1120 |> exclude_visibility(opts)
1121 end
1122
1123 def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
1124 list_memberships = Pleroma.List.memberships(opts[:user])
1125
1126 fetch_activities_query(recipients ++ list_memberships, opts)
1127 |> Pagination.fetch_paginated(opts, pagination)
1128 |> Enum.reverse()
1129 |> maybe_update_cc(list_memberships, opts[:user])
1130 end
1131
1132 @doc """
1133 Fetch favorites activities of user with order by sort adds to favorites
1134 """
1135 @spec fetch_favourites(User.t(), map(), Pagination.type()) :: list(Activity.t())
1136 def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
1137 user.ap_id
1138 |> Activity.Queries.by_actor()
1139 |> Activity.Queries.by_type("Like")
1140 |> Activity.with_joined_object()
1141 |> Object.with_joined_activity()
1142 |> select([like, object, activity], %{activity | object: object, pagination_id: like.id})
1143 |> order_by([like, _, _], desc_nulls_last: like.id)
1144 |> Pagination.fetch_paginated(
1145 Map.merge(params, %{skip_order: true}),
1146 pagination
1147 )
1148 end
1149
1150 defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do
1151 Enum.map(activities, fn
1152 %{data: %{"bcc" => [_ | _] = bcc}} = activity ->
1153 if Enum.any?(bcc, &(&1 in list_memberships)) do
1154 update_in(activity.data["cc"], &[user_ap_id | &1])
1155 else
1156 activity
1157 end
1158
1159 activity ->
1160 activity
1161 end)
1162 end
1163
1164 defp maybe_update_cc(activities, _, _), do: activities
1165
1166 defp fetch_activities_bounded_query(query, recipients, recipients_with_public) do
1167 from(activity in query,
1168 where:
1169 fragment("? && ?", activity.recipients, ^recipients) or
1170 (fragment("? && ?", activity.recipients, ^recipients_with_public) and
1171 ^Constants.as_public() in activity.recipients)
1172 )
1173 end
1174
1175 def fetch_activities_bounded(
1176 recipients,
1177 recipients_with_public,
1178 opts \\ %{},
1179 pagination \\ :keyset
1180 ) do
1181 fetch_activities_query([], opts)
1182 |> fetch_activities_bounded_query(recipients, recipients_with_public)
1183 |> Pagination.fetch_paginated(opts, pagination)
1184 |> Enum.reverse()
1185 end
1186
1187 @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
1188 def upload(file, opts \\ []) do
1189 with {:ok, data} <- Upload.store(file, opts) do
1190 obj_data = Maps.put_if_present(data, "actor", opts[:actor])
1191
1192 Repo.insert(%Object{data: obj_data})
1193 end
1194 end
1195
1196 @spec get_actor_url(any()) :: binary() | nil
1197 defp get_actor_url(url) when is_binary(url), do: url
1198 defp get_actor_url(%{"href" => href}) when is_binary(href), do: href
1199
1200 defp get_actor_url(url) when is_list(url) do
1201 url
1202 |> List.first()
1203 |> get_actor_url()
1204 end
1205
1206 defp get_actor_url(_url), do: nil
1207
1208 defp object_to_user_data(data) do
1209 avatar =
1210 data["icon"]["url"] &&
1211 %{
1212 "type" => "Image",
1213 "url" => [%{"href" => data["icon"]["url"]}]
1214 }
1215
1216 banner =
1217 data["image"]["url"] &&
1218 %{
1219 "type" => "Image",
1220 "url" => [%{"href" => data["image"]["url"]}]
1221 }
1222
1223 fields =
1224 data
1225 |> Map.get("attachment", [])
1226 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1227 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1228
1229 emojis =
1230 data
1231 |> Map.get("tag", [])
1232 |> Enum.filter(fn
1233 %{"type" => "Emoji"} -> true
1234 _ -> false
1235 end)
1236 |> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} ->
1237 {String.trim(name, ":"), url}
1238 end)
1239
1240 is_locked = data["manuallyApprovesFollowers"] || false
1241 capabilities = data["capabilities"] || %{}
1242 accepts_chat_messages = capabilities["acceptsChatMessages"]
1243 data = Transmogrifier.maybe_fix_user_object(data)
1244 discoverable = data["discoverable"] || false
1245 invisible = data["invisible"] || false
1246 actor_type = data["type"] || "Person"
1247
1248 public_key =
1249 if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
1250 data["publicKey"]["publicKeyPem"]
1251 else
1252 nil
1253 end
1254
1255 shared_inbox =
1256 if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
1257 data["endpoints"]["sharedInbox"]
1258 else
1259 nil
1260 end
1261
1262 user_data = %{
1263 ap_id: data["id"],
1264 uri: get_actor_url(data["url"]),
1265 ap_enabled: true,
1266 banner: banner,
1267 fields: fields,
1268 emoji: emojis,
1269 is_locked: is_locked,
1270 discoverable: discoverable,
1271 invisible: invisible,
1272 avatar: avatar,
1273 name: data["name"],
1274 follower_address: data["followers"],
1275 following_address: data["following"],
1276 bio: data["summary"] || "",
1277 actor_type: actor_type,
1278 also_known_as: Map.get(data, "alsoKnownAs", []),
1279 public_key: public_key,
1280 inbox: data["inbox"],
1281 shared_inbox: shared_inbox,
1282 accepts_chat_messages: accepts_chat_messages
1283 }
1284
1285 # nickname can be nil because of virtual actors
1286 if data["preferredUsername"] do
1287 Map.put(
1288 user_data,
1289 :nickname,
1290 "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
1291 )
1292 else
1293 Map.put(user_data, :nickname, nil)
1294 end
1295 end
1296
1297 def fetch_follow_information_for_user(user) do
1298 with {:ok, following_data} <-
1299 Fetcher.fetch_and_contain_remote_object_from_id(user.following_address,
1300 force_http: true
1301 ),
1302 {:ok, hide_follows} <- collection_private(following_data),
1303 {:ok, followers_data} <-
1304 Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address, force_http: true),
1305 {:ok, hide_followers} <- collection_private(followers_data) do
1306 {:ok,
1307 %{
1308 hide_follows: hide_follows,
1309 follower_count: normalize_counter(followers_data["totalItems"]),
1310 following_count: normalize_counter(following_data["totalItems"]),
1311 hide_followers: hide_followers
1312 }}
1313 else
1314 {:error, _} = e -> e
1315 e -> {:error, e}
1316 end
1317 end
1318
1319 defp normalize_counter(counter) when is_integer(counter), do: counter
1320 defp normalize_counter(_), do: 0
1321
1322 def maybe_update_follow_information(user_data) do
1323 with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
1324 {_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
1325 {_, true} <-
1326 {:collections_available,
1327 !!(user_data[:following_address] && user_data[:follower_address])},
1328 {:ok, info} <-
1329 fetch_follow_information_for_user(user_data) do
1330 info = Map.merge(user_data[:info] || %{}, info)
1331
1332 user_data
1333 |> Map.put(:info, info)
1334 else
1335 {:user_type_check, false} ->
1336 user_data
1337
1338 {:collections_available, false} ->
1339 user_data
1340
1341 {:enabled, false} ->
1342 user_data
1343
1344 e ->
1345 Logger.error(
1346 "Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
1347 )
1348
1349 user_data
1350 end
1351 end
1352
1353 defp collection_private(%{"first" => %{"type" => type}})
1354 when type in ["CollectionPage", "OrderedCollectionPage"],
1355 do: {:ok, false}
1356
1357 defp collection_private(%{"first" => first}) do
1358 with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
1359 Fetcher.fetch_and_contain_remote_object_from_id(first) do
1360 {:ok, false}
1361 else
1362 {:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true}
1363 {:error, _} = e -> e
1364 e -> {:error, e}
1365 end
1366 end
1367
1368 defp collection_private(_data), do: {:ok, true}
1369
1370 def user_data_from_user_object(data) do
1371 with {:ok, data} <- MRF.filter(data) do
1372 {:ok, object_to_user_data(data)}
1373 else
1374 e -> {:error, e}
1375 end
1376 end
1377
1378 def fetch_and_prepare_user_from_ap_id(ap_id, opts \\ []) do
1379 with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id, opts),
1380 {:ok, data} <- user_data_from_user_object(data) do
1381 {:ok, maybe_update_follow_information(data)}
1382 else
1383 # If this has been deleted, only log a debug and not an error
1384 {:error, "Object has been deleted" = e} ->
1385 Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
1386 {:error, e}
1387
1388 {:error, {:reject, reason} = e} ->
1389 Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
1390 {:error, e}
1391
1392 {:error, e} ->
1393 Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
1394 {:error, e}
1395 end
1396 end
1397
1398 def maybe_handle_clashing_nickname(data) do
1399 with nickname when is_binary(nickname) <- data[:nickname],
1400 %User{} = old_user <- User.get_by_nickname(nickname),
1401 {_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do
1402 Logger.info(
1403 "Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{
1404 data[:ap_id]
1405 }, renaming."
1406 )
1407
1408 old_user
1409 |> User.remote_user_changeset(%{nickname: "#{old_user.id}.#{old_user.nickname}"})
1410 |> User.update_and_set_cache()
1411 else
1412 {:ap_id_comparison, true} ->
1413 Logger.info(
1414 "Found an old user for #{data[:nickname]}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
1415 )
1416
1417 _ ->
1418 nil
1419 end
1420 end
1421
1422 def make_user_from_ap_id(ap_id, opts \\ []) do
1423 user = User.get_cached_by_ap_id(ap_id)
1424
1425 if user && !User.ap_enabled?(user) do
1426 Transmogrifier.upgrade_user_from_ap_id(ap_id)
1427 else
1428 with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, opts) do
1429 if user do
1430 user
1431 |> User.remote_user_changeset(data)
1432 |> User.update_and_set_cache()
1433 else
1434 maybe_handle_clashing_nickname(data)
1435
1436 data
1437 |> User.remote_user_changeset()
1438 |> Repo.insert()
1439 |> User.set_cache()
1440 end
1441 end
1442 end
1443 end
1444
1445 def make_user_from_nickname(nickname) do
1446 with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
1447 make_user_from_ap_id(ap_id)
1448 else
1449 _e -> {:error, "No AP id in WebFinger"}
1450 end
1451 end
1452
1453 # filter out broken threads
1454 defp contain_broken_threads(%Activity{} = activity, %User{} = user) do
1455 entire_thread_visible_for_user?(activity, user)
1456 end
1457
1458 # do post-processing on a specific activity
1459 def contain_activity(%Activity{} = activity, %User{} = user) do
1460 contain_broken_threads(activity, user)
1461 end
1462
1463 def fetch_direct_messages_query do
1464 Activity
1465 |> restrict_type(%{type: "Create"})
1466 |> restrict_visibility(%{visibility: "direct"})
1467 |> order_by([activity], asc: activity.id)
1468 end
1469 end