object: add support for preloading objects when walking an activity graph in normal...
[akkoma] / lib / pleroma / activity.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.Activity do
6 use Ecto.Schema
7
8 alias Pleroma.Activity
9 alias Pleroma.Notification
10 alias Pleroma.Object
11 alias Pleroma.Repo
12
13 import Ecto.Query
14
15 @type t :: %__MODULE__{}
16 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
17
18 # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
19 @mastodon_notification_types %{
20 "Create" => "mention",
21 "Follow" => "follow",
22 "Announce" => "reblog",
23 "Like" => "favourite"
24 }
25
26 @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
27 into: %{},
28 do: {v, k}
29
30 schema "activities" do
31 field(:data, :map)
32 field(:local, :boolean, default: true)
33 field(:actor, :string)
34 field(:recipients, {:array, :string})
35 has_many(:notifications, Notification, on_delete: :delete_all)
36
37 # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
38 # The foreign key is embedded in a jsonb field.
39 #
40 # To use it, you probably want to do an inner join and a preload:
41 #
42 # ```
43 # |> join(:inner, [activity], o in Object,
44 # fragment("(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", o.data, activity.data))
45 # |> preload([activity, object], [object: object])
46 # ```
47 has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
48
49 timestamps()
50 end
51
52 def get_by_ap_id(ap_id) do
53 Repo.one(
54 from(
55 activity in Activity,
56 where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id))
57 )
58 )
59 end
60
61 def get_by_id(id) do
62 Repo.get(Activity, id)
63 end
64
65 def get_by_id_with_object(id) do
66 from(activity in Activity,
67 where: activity.id == ^id,
68 inner_join: o in Object,
69 on:
70 fragment(
71 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
72 o.data,
73 activity.data
74 ),
75 preload: [object: o]
76 )
77 |> Repo.one()
78 end
79
80 def by_object_ap_id(ap_id) do
81 from(
82 activity in Activity,
83 where:
84 fragment(
85 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
86 activity.data,
87 activity.data,
88 ^to_string(ap_id)
89 )
90 )
91 end
92
93 def create_by_object_ap_id(ap_ids) when is_list(ap_ids) do
94 from(
95 activity in Activity,
96 where:
97 fragment(
98 "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",
99 activity.data,
100 activity.data,
101 ^ap_ids
102 ),
103 where: fragment("(?)->>'type' = 'Create'", activity.data)
104 )
105 end
106
107 def create_by_object_ap_id(ap_id) when is_binary(ap_id) do
108 from(
109 activity in Activity,
110 where:
111 fragment(
112 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
113 activity.data,
114 activity.data,
115 ^to_string(ap_id)
116 ),
117 where: fragment("(?)->>'type' = 'Create'", activity.data)
118 )
119 end
120
121 def create_by_object_ap_id(_), do: nil
122
123 def get_all_create_by_object_ap_id(ap_id) do
124 Repo.all(create_by_object_ap_id(ap_id))
125 end
126
127 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
128 create_by_object_ap_id(ap_id)
129 |> Repo.one()
130 end
131
132 def get_create_by_object_ap_id(_), do: nil
133
134 def create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
135 from(
136 activity in Activity,
137 where:
138 fragment(
139 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
140 activity.data,
141 activity.data,
142 ^to_string(ap_id)
143 ),
144 where: fragment("(?)->>'type' = 'Create'", activity.data),
145 inner_join: o in Object,
146 on:
147 fragment(
148 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
149 o.data,
150 activity.data
151 ),
152 preload: [object: o]
153 )
154 end
155
156 def create_by_object_ap_id_with_object(_), do: nil
157
158 def get_create_by_object_ap_id_with_object(ap_id) do
159 ap_id
160 |> create_by_object_ap_id_with_object()
161 |> Repo.one()
162 end
163
164 def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])
165 def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)
166 def normalize(_), do: nil
167
168 def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do
169 get_create_by_object_ap_id(ap_id)
170 end
171
172 def get_in_reply_to_activity(_), do: nil
173
174 def delete_by_ap_id(id) when is_binary(id) do
175 by_object_ap_id(id)
176 |> select([u], u)
177 |> Repo.delete_all()
178 |> elem(1)
179 |> Enum.find(fn
180 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
181 _ -> nil
182 end)
183 end
184
185 def delete_by_ap_id(_), do: nil
186
187 for {ap_type, type} <- @mastodon_notification_types do
188 def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
189 do: unquote(type)
190 end
191
192 def mastodon_notification_type(%Activity{}), do: nil
193
194 def from_mastodon_notification_type(type) do
195 Map.get(@mastodon_to_ap_notification_types, type)
196 end
197
198 def all_by_actor_and_id(actor, status_ids \\ [])
199 def all_by_actor_and_id(_actor, []), do: []
200
201 def all_by_actor_and_id(actor, status_ids) do
202 Activity
203 |> where([s], s.id in ^status_ids)
204 |> where([s], s.actor == ^actor)
205 |> Repo.all()
206 end
207 end