ActivityPub: Use is_unicode_emoji? function.
[akkoma] / lib / pleroma / web / activity_pub / utils.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Utils do
6 alias Ecto.Changeset
7 alias Ecto.UUID
8 alias Pleroma.Activity
9 alias Pleroma.Notification
10 alias Pleroma.Object
11 alias Pleroma.Repo
12 alias Pleroma.User
13 alias Pleroma.Web
14 alias Pleroma.Web.ActivityPub.Visibility
15 alias Pleroma.Web.Endpoint
16 alias Pleroma.Web.Router.Helpers
17
18 import Ecto.Query
19
20 require Logger
21 require Pleroma.Constants
22
23 @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
24 @supported_report_states ~w(open closed resolved)
25 @valid_visibilities ~w(public unlisted private direct)
26
27 # Some implementations send the actor URI as the actor field, others send the entire actor object,
28 # so figure out what the actor's URI is based on what we have.
29 def get_ap_id(%{"id" => id} = _), do: id
30 def get_ap_id(id), do: id
31
32 def normalize_params(params) do
33 Map.put(params, "actor", get_ap_id(params["actor"]))
34 end
35
36 def determine_explicit_mentions(%{"tag" => tag} = _object) when is_list(tag) do
37 tag
38 |> Enum.filter(fn x -> is_map(x) end)
39 |> Enum.filter(fn x -> x["type"] == "Mention" end)
40 |> Enum.map(fn x -> x["href"] end)
41 end
42
43 def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
44 Map.put(object, "tag", [tag])
45 |> determine_explicit_mentions()
46 end
47
48 def determine_explicit_mentions(_), do: []
49
50 defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
51 defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
52 defp recipient_in_collection(_, _), do: false
53
54 def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
55 cond do
56 recipient_in_collection(ap_id, params["to"]) ->
57 true
58
59 recipient_in_collection(ap_id, params["cc"]) ->
60 true
61
62 recipient_in_collection(ap_id, params["bto"]) ->
63 true
64
65 recipient_in_collection(ap_id, params["bcc"]) ->
66 true
67
68 # if the message is unaddressed at all, then assume it is directly addressed
69 # to the recipient
70 !params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
71 true
72
73 # if the message is sent from somebody the user is following, then assume it
74 # is addressed to the recipient
75 User.following?(recipient, actor) ->
76 true
77
78 true ->
79 false
80 end
81 end
82
83 defp extract_list(target) when is_binary(target), do: [target]
84 defp extract_list(lst) when is_list(lst), do: lst
85 defp extract_list(_), do: []
86
87 def maybe_splice_recipient(ap_id, params) do
88 need_splice =
89 !recipient_in_collection(ap_id, params["to"]) &&
90 !recipient_in_collection(ap_id, params["cc"])
91
92 cc_list = extract_list(params["cc"])
93
94 if need_splice do
95 params
96 |> Map.put("cc", [ap_id | cc_list])
97 else
98 params
99 end
100 end
101
102 def make_json_ld_header do
103 %{
104 "@context" => [
105 "https://www.w3.org/ns/activitystreams",
106 "#{Web.base_url()}/schemas/litepub-0.1.jsonld",
107 %{
108 "@language" => "und"
109 }
110 ]
111 }
112 end
113
114 def make_date do
115 DateTime.utc_now() |> DateTime.to_iso8601()
116 end
117
118 def generate_activity_id do
119 generate_id("activities")
120 end
121
122 def generate_context_id do
123 generate_id("contexts")
124 end
125
126 def generate_object_id do
127 Helpers.o_status_url(Endpoint, :object, UUID.generate())
128 end
129
130 def generate_id(type) do
131 "#{Web.base_url()}/#{type}/#{UUID.generate()}"
132 end
133
134 def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
135 fake_create_activity = %{
136 "to" => object["to"],
137 "cc" => object["cc"],
138 "type" => "Create",
139 "object" => object
140 }
141
142 Notification.get_notified_from_activity(%Activity{data: fake_create_activity}, false)
143 end
144
145 def get_notified_from_object(object) do
146 Notification.get_notified_from_activity(%Activity{data: object}, false)
147 end
148
149 def create_context(context) do
150 context = context || generate_id("contexts")
151
152 # Ecto has problems accessing the constraint inside the jsonb,
153 # so we explicitly check for the existed object before insert
154 object = Object.get_cached_by_ap_id(context)
155
156 with true <- is_nil(object),
157 changeset <- Object.context_mapping(context),
158 {:ok, inserted_object} <- Repo.insert(changeset) do
159 inserted_object
160 else
161 _ ->
162 object
163 end
164 end
165
166 @doc """
167 Enqueues an activity for federation if it's local
168 """
169 @spec maybe_federate(any()) :: :ok
170 def maybe_federate(%Activity{local: true} = activity) do
171 if Pleroma.Config.get!([:instance, :federating]) do
172 priority =
173 case activity.data["type"] do
174 "Delete" -> 10
175 "Create" -> 1
176 _ -> 5
177 end
178
179 Pleroma.Web.Federator.publish(activity, priority)
180 end
181
182 :ok
183 end
184
185 def maybe_federate(_), do: :ok
186
187 @doc """
188 Adds an id and a published data if they aren't there,
189 also adds it to an included object
190 """
191 def lazy_put_activity_defaults(map, fake \\ false) do
192 map =
193 unless fake do
194 %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
195
196 map
197 |> Map.put_new_lazy("id", &generate_activity_id/0)
198 |> Map.put_new_lazy("published", &make_date/0)
199 |> Map.put_new("context", context)
200 |> Map.put_new("context_id", context_id)
201 else
202 map
203 |> Map.put_new("id", "pleroma:fakeid")
204 |> Map.put_new_lazy("published", &make_date/0)
205 |> Map.put_new("context", "pleroma:fakecontext")
206 |> Map.put_new("context_id", -1)
207 end
208
209 if is_map(map["object"]) do
210 object = lazy_put_object_defaults(map["object"], map, fake)
211 %{map | "object" => object}
212 else
213 map
214 end
215 end
216
217 @doc """
218 Adds an id and published date if they aren't there.
219 """
220 def lazy_put_object_defaults(map, activity \\ %{}, fake)
221
222 def lazy_put_object_defaults(map, activity, true = _fake) do
223 map
224 |> Map.put_new_lazy("published", &make_date/0)
225 |> Map.put_new("id", "pleroma:fake_object_id")
226 |> Map.put_new("context", activity["context"])
227 |> Map.put_new("fake", true)
228 |> Map.put_new("context_id", activity["context_id"])
229 end
230
231 def lazy_put_object_defaults(map, activity, _fake) do
232 map
233 |> Map.put_new_lazy("id", &generate_object_id/0)
234 |> Map.put_new_lazy("published", &make_date/0)
235 |> Map.put_new("context", activity["context"])
236 |> Map.put_new("context_id", activity["context_id"])
237 end
238
239 @doc """
240 Inserts a full object if it is contained in an activity.
241 """
242 def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
243 when is_map(object_data) and type in @supported_object_types do
244 with {:ok, object} <- Object.create(object_data) do
245 map =
246 map
247 |> Map.put("object", object.data["id"])
248
249 {:ok, map, object}
250 end
251 end
252
253 def insert_full_object(map), do: {:ok, map, nil}
254
255 #### Like-related helpers
256
257 @doc """
258 Returns an existing like if a user already liked an object
259 """
260 @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
261 def get_existing_like(actor, %{data: %{"id" => id}}) do
262 actor
263 |> Activity.Queries.by_actor()
264 |> Activity.Queries.by_object_id(id)
265 |> Activity.Queries.by_type("Like")
266 |> Activity.Queries.limit(1)
267 |> Repo.one()
268 end
269
270 @doc """
271 Returns like activities targeting an object
272 """
273 def get_object_likes(%{data: %{"id" => id}}) do
274 id
275 |> Activity.Queries.by_object_id()
276 |> Activity.Queries.by_type("Like")
277 |> Repo.all()
278 end
279
280 def make_emoji_reaction_data(user, object, emoji, activity_id) do
281 make_like_data(user, object, activity_id)
282 |> Map.put("type", "EmojiReaction")
283 |> Map.put("content", emoji)
284 end
285
286 @spec make_like_data(User.t(), map(), String.t()) :: map()
287 def make_like_data(
288 %User{ap_id: ap_id} = actor,
289 %{data: %{"actor" => object_actor_id, "id" => id}} = object,
290 activity_id
291 ) do
292 object_actor = User.get_cached_by_ap_id(object_actor_id)
293
294 to =
295 if Visibility.is_public?(object) do
296 [actor.follower_address, object.data["actor"]]
297 else
298 [object.data["actor"]]
299 end
300
301 cc =
302 (object.data["to"] ++ (object.data["cc"] || []))
303 |> List.delete(actor.ap_id)
304 |> List.delete(object_actor.follower_address)
305
306 %{
307 "type" => "Like",
308 "actor" => ap_id,
309 "object" => id,
310 "to" => to,
311 "cc" => cc,
312 "context" => object.data["context"]
313 }
314 |> maybe_put("id", activity_id)
315 end
316
317 @spec update_element_in_object(String.t(), list(any), Object.t()) ::
318 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
319 def update_element_in_object(property, element, object) do
320 length =
321 if is_map(element) do
322 element
323 |> Map.values()
324 |> List.flatten()
325 |> length()
326 else
327 element
328 |> length()
329 end
330
331 data =
332 Map.merge(
333 object.data,
334 %{"#{property}_count" => length, "#{property}s" => element}
335 )
336
337 object
338 |> Changeset.change(data: data)
339 |> Object.update_and_set_cache()
340 end
341
342 @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
343 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
344
345 def add_emoji_reaction_to_object(
346 %Activity{data: %{"content" => emoji, "actor" => actor}},
347 object
348 ) do
349 reactions = object.data["reactions"] || %{}
350 emoji_actors = reactions[emoji] || []
351 new_emoji_actors = [actor | emoji_actors] |> Enum.uniq()
352 new_reactions = Map.put(reactions, emoji, new_emoji_actors)
353 update_element_in_object("reaction", new_reactions, object)
354 end
355
356 @spec add_like_to_object(Activity.t(), Object.t()) ::
357 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
358 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
359 [actor | fetch_likes(object)]
360 |> Enum.uniq()
361 |> update_likes_in_object(object)
362 end
363
364 @spec remove_like_from_object(Activity.t(), Object.t()) ::
365 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
366 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
367 object
368 |> fetch_likes()
369 |> List.delete(actor)
370 |> update_likes_in_object(object)
371 end
372
373 defp update_likes_in_object(likes, object) do
374 update_element_in_object("like", likes, object)
375 end
376
377 defp fetch_likes(object) do
378 if is_list(object.data["likes"]) do
379 object.data["likes"]
380 else
381 []
382 end
383 end
384
385 #### Follow-related helpers
386
387 @doc """
388 Updates a follow activity's state (for locked accounts).
389 """
390 def update_follow_state_for_all(
391 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
392 state
393 ) do
394 try do
395 Ecto.Adapters.SQL.query!(
396 Repo,
397 "UPDATE activities SET data = jsonb_set(data, '{state}', $1) WHERE data->>'type' = 'Follow' AND data->>'actor' = $2 AND data->>'object' = $3 AND data->>'state' = 'pending'",
398 [state, actor, object]
399 )
400
401 User.set_follow_state_cache(actor, object, state)
402 activity = Activity.get_by_id(activity.id)
403 {:ok, activity}
404 rescue
405 e ->
406 {:error, e}
407 end
408 end
409
410 def update_follow_state(
411 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
412 state
413 ) do
414 with new_data <-
415 activity.data
416 |> Map.put("state", state),
417 changeset <- Changeset.change(activity, data: new_data),
418 {:ok, activity} <- Repo.update(changeset),
419 _ <- User.set_follow_state_cache(actor, object, state) do
420 {:ok, activity}
421 end
422 end
423
424 @doc """
425 Makes a follow activity data for the given follower and followed
426 """
427 def make_follow_data(
428 %User{ap_id: follower_id},
429 %User{ap_id: followed_id} = _followed,
430 activity_id
431 ) do
432 %{
433 "type" => "Follow",
434 "actor" => follower_id,
435 "to" => [followed_id],
436 "cc" => [Pleroma.Constants.as_public()],
437 "object" => followed_id,
438 "state" => "pending"
439 }
440 |> maybe_put("id", activity_id)
441 end
442
443 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
444 query =
445 from(
446 activity in Activity,
447 where:
448 fragment(
449 "? ->> 'type' = 'Follow'",
450 activity.data
451 ),
452 where: activity.actor == ^follower_id,
453 # this is to use the index
454 where:
455 fragment(
456 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
457 activity.data,
458 activity.data,
459 ^followed_id
460 ),
461 order_by: [fragment("? desc nulls last", activity.id)],
462 limit: 1
463 )
464
465 Repo.one(query)
466 end
467
468 #### Announce-related helpers
469
470 @doc """
471 Retruns an existing announce activity if the notice has already been announced
472 """
473 def get_existing_announce(actor, %{data: %{"id" => id}}) do
474 query =
475 from(
476 activity in Activity,
477 where: activity.actor == ^actor,
478 # this is to use the index
479 where:
480 fragment(
481 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
482 activity.data,
483 activity.data,
484 ^id
485 ),
486 where: fragment("(?)->>'type' = 'Announce'", activity.data)
487 )
488
489 Repo.one(query)
490 end
491
492 @doc """
493 Make announce activity data for the given actor and object
494 """
495 # for relayed messages, we only want to send to subscribers
496 def make_announce_data(
497 %User{ap_id: ap_id} = user,
498 %Object{data: %{"id" => id}} = object,
499 activity_id,
500 false
501 ) do
502 %{
503 "type" => "Announce",
504 "actor" => ap_id,
505 "object" => id,
506 "to" => [user.follower_address],
507 "cc" => [],
508 "context" => object.data["context"]
509 }
510 |> maybe_put("id", activity_id)
511 end
512
513 def make_announce_data(
514 %User{ap_id: ap_id} = user,
515 %Object{data: %{"id" => id}} = object,
516 activity_id,
517 true
518 ) do
519 %{
520 "type" => "Announce",
521 "actor" => ap_id,
522 "object" => id,
523 "to" => [user.follower_address, object.data["actor"]],
524 "cc" => [Pleroma.Constants.as_public()],
525 "context" => object.data["context"]
526 }
527 |> maybe_put("id", activity_id)
528 end
529
530 @doc """
531 Make unannounce activity data for the given actor and object
532 """
533 def make_unannounce_data(
534 %User{ap_id: ap_id} = user,
535 %Activity{data: %{"context" => context}} = activity,
536 activity_id
537 ) do
538 %{
539 "type" => "Undo",
540 "actor" => ap_id,
541 "object" => activity.data,
542 "to" => [user.follower_address, activity.data["actor"]],
543 "cc" => [Pleroma.Constants.as_public()],
544 "context" => context
545 }
546 |> maybe_put("id", activity_id)
547 end
548
549 def make_unlike_data(
550 %User{ap_id: ap_id} = user,
551 %Activity{data: %{"context" => context}} = activity,
552 activity_id
553 ) do
554 %{
555 "type" => "Undo",
556 "actor" => ap_id,
557 "object" => activity.data,
558 "to" => [user.follower_address, activity.data["actor"]],
559 "cc" => [Pleroma.Constants.as_public()],
560 "context" => context
561 }
562 |> maybe_put("id", activity_id)
563 end
564
565 def add_announce_to_object(
566 %Activity{
567 data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}
568 },
569 object
570 ) do
571 announcements =
572 if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
573
574 with announcements <- [actor | announcements] |> Enum.uniq() do
575 update_element_in_object("announcement", announcements, object)
576 end
577 end
578
579 def add_announce_to_object(_, object), do: {:ok, object}
580
581 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
582 announcements =
583 if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
584
585 with announcements <- announcements |> List.delete(actor) do
586 update_element_in_object("announcement", announcements, object)
587 end
588 end
589
590 #### Unfollow-related helpers
591
592 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
593 %{
594 "type" => "Undo",
595 "actor" => follower.ap_id,
596 "to" => [followed.ap_id],
597 "object" => follow_activity.data
598 }
599 |> maybe_put("id", activity_id)
600 end
601
602 #### Block-related helpers
603 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
604 query =
605 from(
606 activity in Activity,
607 where:
608 fragment(
609 "? ->> 'type' = 'Block'",
610 activity.data
611 ),
612 where: activity.actor == ^blocker_id,
613 # this is to use the index
614 where:
615 fragment(
616 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
617 activity.data,
618 activity.data,
619 ^blocked_id
620 ),
621 order_by: [fragment("? desc nulls last", activity.id)],
622 limit: 1
623 )
624
625 Repo.one(query)
626 end
627
628 def make_block_data(blocker, blocked, activity_id) do
629 %{
630 "type" => "Block",
631 "actor" => blocker.ap_id,
632 "to" => [blocked.ap_id],
633 "object" => blocked.ap_id
634 }
635 |> maybe_put("id", activity_id)
636 end
637
638 def make_unblock_data(blocker, blocked, block_activity, activity_id) do
639 %{
640 "type" => "Undo",
641 "actor" => blocker.ap_id,
642 "to" => [blocked.ap_id],
643 "object" => block_activity.data
644 }
645 |> maybe_put("id", activity_id)
646 end
647
648 #### Create-related helpers
649
650 def make_create_data(params, additional) do
651 published = params.published || make_date()
652
653 %{
654 "type" => "Create",
655 "to" => params.to |> Enum.uniq(),
656 "actor" => params.actor.ap_id,
657 "object" => params.object,
658 "published" => published,
659 "context" => params.context
660 }
661 |> Map.merge(additional)
662 end
663
664 #### Flag-related helpers
665
666 def make_flag_data(params, additional) do
667 status_ap_ids =
668 Enum.map(params.statuses || [], fn
669 %Activity{} = act -> act.data["id"]
670 act when is_map(act) -> act["id"]
671 act when is_binary(act) -> act
672 end)
673
674 object = [params.account.ap_id] ++ status_ap_ids
675
676 %{
677 "type" => "Flag",
678 "actor" => params.actor.ap_id,
679 "content" => params.content,
680 "object" => object,
681 "context" => params.context,
682 "state" => "open"
683 }
684 |> Map.merge(additional)
685 end
686
687 @doc """
688 Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
689 the first one to `pages_left` pages.
690 If the amount of pages is higher than the collection has, it returns whatever was there.
691 """
692 def fetch_ordered_collection(from, pages_left, acc \\ []) do
693 with {:ok, response} <- Tesla.get(from),
694 {:ok, collection} <- Jason.decode(response.body) do
695 case collection["type"] do
696 "OrderedCollection" ->
697 # If we've encountered the OrderedCollection and not the page,
698 # just call the same function on the page address
699 fetch_ordered_collection(collection["first"], pages_left)
700
701 "OrderedCollectionPage" ->
702 if pages_left > 0 do
703 # There are still more pages
704 if Map.has_key?(collection, "next") do
705 # There are still more pages, go deeper saving what we have into the accumulator
706 fetch_ordered_collection(
707 collection["next"],
708 pages_left - 1,
709 acc ++ collection["orderedItems"]
710 )
711 else
712 # No more pages left, just return whatever we already have
713 acc ++ collection["orderedItems"]
714 end
715 else
716 # Got the amount of pages needed, add them all to the accumulator
717 acc ++ collection["orderedItems"]
718 end
719
720 _ ->
721 {:error, "Not an OrderedCollection or OrderedCollectionPage"}
722 end
723 end
724 end
725
726 #### Report-related helpers
727
728 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
729 with new_data <- Map.put(activity.data, "state", state),
730 changeset <- Changeset.change(activity, data: new_data),
731 {:ok, activity} <- Repo.update(changeset) do
732 {:ok, activity}
733 end
734 end
735
736 def update_report_state(_, _), do: {:error, "Unsupported state"}
737
738 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
739 [to, cc, recipients] =
740 activity
741 |> get_updated_targets(visibility)
742 |> Enum.map(&Enum.uniq/1)
743
744 object_data =
745 activity.object.data
746 |> Map.put("to", to)
747 |> Map.put("cc", cc)
748
749 {:ok, object} =
750 activity.object
751 |> Object.change(%{data: object_data})
752 |> Object.update_and_set_cache()
753
754 activity_data =
755 activity.data
756 |> Map.put("to", to)
757 |> Map.put("cc", cc)
758
759 activity
760 |> Map.put(:object, object)
761 |> Activity.change(%{data: activity_data, recipients: recipients})
762 |> Repo.update()
763 end
764
765 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
766
767 defp get_updated_targets(
768 %Activity{data: %{"to" => to} = data, recipients: recipients},
769 visibility
770 ) do
771 cc = Map.get(data, "cc", [])
772 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
773 public = Pleroma.Constants.as_public()
774
775 case visibility do
776 "public" ->
777 to = [public | List.delete(to, follower_address)]
778 cc = [follower_address | List.delete(cc, public)]
779 recipients = [public | recipients]
780 [to, cc, recipients]
781
782 "private" ->
783 to = [follower_address | List.delete(to, public)]
784 cc = List.delete(cc, public)
785 recipients = List.delete(recipients, public)
786 [to, cc, recipients]
787
788 "unlisted" ->
789 to = [follower_address | List.delete(to, public)]
790 cc = [public | List.delete(cc, follower_address)]
791 recipients = recipients ++ [follower_address, public]
792 [to, cc, recipients]
793
794 _ ->
795 [to, cc, recipients]
796 end
797 end
798
799 def get_existing_votes(actor, %{data: %{"id" => id}}) do
800 query =
801 from(
802 [activity, object: object] in Activity.with_preloaded_object(Activity),
803 where: fragment("(?)->>'type' = 'Create'", activity.data),
804 where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
805 where:
806 fragment(
807 "(?)->>'inReplyTo' = ?",
808 object.data,
809 ^to_string(id)
810 ),
811 where: fragment("(?)->>'type' = 'Answer'", object.data)
812 )
813
814 Repo.all(query)
815 end
816
817 defp maybe_put(map, _key, nil), do: map
818 defp maybe_put(map, key, value), do: Map.put(map, key, value)
819 end