d34bb72859b242cb3f867a47c1f0daaa844fcabb
[akkoma] / lib / pleroma / web / common_api / common_api.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.CommonAPI do
6 alias Pleroma.Activity
7 alias Pleroma.ActivityExpiration
8 alias Pleroma.Conversation.Participation
9 alias Pleroma.Object
10 alias Pleroma.ThreadMute
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Utils
14 alias Pleroma.Web.ActivityPub.Visibility
15
16 import Pleroma.Web.Gettext
17 import Pleroma.Web.CommonAPI.Utils
18
19 def follow(follower, followed) do
20 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
21 {:ok, activity} <- ActivityPub.follow(follower, followed),
22 {:ok, follower, followed} <-
23 User.wait_and_refresh(
24 Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
25 follower,
26 followed
27 ) do
28 {:ok, follower, followed, activity}
29 end
30 end
31
32 def unfollow(follower, unfollowed) do
33 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
34 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
35 {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
36 {:ok, follower}
37 end
38 end
39
40 def accept_follow_request(follower, followed) do
41 with {:ok, follower} <- User.follow(follower, followed),
42 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
43 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
44 {:ok, _activity} <-
45 ActivityPub.accept(%{
46 to: [follower.ap_id],
47 actor: followed,
48 object: follow_activity.data["id"],
49 type: "Accept"
50 }) do
51 {:ok, follower}
52 end
53 end
54
55 def reject_follow_request(follower, followed) do
56 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
57 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
58 {:ok, _activity} <-
59 ActivityPub.reject(%{
60 to: [follower.ap_id],
61 actor: followed,
62 object: follow_activity.data["id"],
63 type: "Reject"
64 }) do
65 {:ok, follower}
66 end
67 end
68
69 def delete(activity_id, user) do
70 with %Activity{data: %{"object" => _}} = activity <-
71 Activity.get_by_id_with_object(activity_id),
72 %Object{} = object <- Object.normalize(activity),
73 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
74 {:ok, _} <- unpin(activity_id, user),
75 {:ok, delete} <- ActivityPub.delete(object) do
76 {:ok, delete}
77 else
78 _ ->
79 {:error, dgettext("errors", "Could not delete")}
80 end
81 end
82
83 def repeat(id_or_ap_id, user) do
84 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
85 object <- Object.normalize(activity),
86 nil <- Utils.get_existing_announce(user.ap_id, object) do
87 ActivityPub.announce(user, object)
88 else
89 _ ->
90 {:error, dgettext("errors", "Could not repeat")}
91 end
92 end
93
94 def unrepeat(id_or_ap_id, user) do
95 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
96 object <- Object.normalize(activity) do
97 ActivityPub.unannounce(user, object)
98 else
99 _ ->
100 {:error, dgettext("errors", "Could not unrepeat")}
101 end
102 end
103
104 def favorite(id_or_ap_id, user) do
105 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
106 object <- Object.normalize(activity),
107 nil <- Utils.get_existing_like(user.ap_id, object) do
108 ActivityPub.like(user, object)
109 else
110 _ ->
111 {:error, dgettext("errors", "Could not favorite")}
112 end
113 end
114
115 def unfavorite(id_or_ap_id, user) do
116 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
117 object <- Object.normalize(activity) do
118 ActivityPub.unlike(user, object)
119 else
120 _ ->
121 {:error, dgettext("errors", "Could not unfavorite")}
122 end
123 end
124
125 def vote(user, object, choices) do
126 with "Question" <- object.data["type"],
127 {:author, false} <- {:author, object.data["actor"] == user.ap_id},
128 {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
129 {options, max_count} <- get_options_and_max_count(object),
130 option_count <- Enum.count(options),
131 {:choice_check, {choices, true}} <-
132 {:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
133 {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
134 answer_activities =
135 Enum.map(choices, fn index ->
136 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
137
138 {:ok, activity} =
139 ActivityPub.create(%{
140 to: answer_data["to"],
141 actor: user,
142 context: object.data["context"],
143 object: answer_data,
144 additional: %{"cc" => answer_data["cc"]}
145 })
146
147 activity
148 end)
149
150 object = Object.get_cached_by_ap_id(object.data["id"])
151 {:ok, answer_activities, object}
152 else
153 {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
154 {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
155 {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
156 {:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
157 end
158 end
159
160 defp get_options_and_max_count(object) do
161 if Map.has_key?(object.data, "anyOf") do
162 {object.data["anyOf"], Enum.count(object.data["anyOf"])}
163 else
164 {object.data["oneOf"], 1}
165 end
166 end
167
168 defp normalize_and_validate_choice_indices(choices, count) do
169 Enum.map_reduce(choices, true, fn index, valid ->
170 index = if is_binary(index), do: String.to_integer(index), else: index
171 {index, if(valid, do: index < count, else: valid)}
172 end)
173 end
174
175 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
176
177 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
178 when visibility in ~w{public unlisted private direct},
179 do: {visibility, get_replied_to_visibility(in_reply_to)}
180
181 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
182 visibility = {:list, String.to_integer(list_id)}
183 {visibility, get_replied_to_visibility(in_reply_to)}
184 end
185
186 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
187 visibility = get_replied_to_visibility(in_reply_to)
188 {visibility, visibility}
189 end
190
191 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
192
193 def get_replied_to_visibility(nil), do: nil
194
195 def get_replied_to_visibility(activity) do
196 with %Object{} = object <- Object.normalize(activity) do
197 Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
198 end
199 end
200
201 def check_expiry_date({:ok, nil} = res), do: res
202
203 def check_expiry_date({:ok, in_seconds}) do
204 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
205
206 if ActivityExpiration.expires_late_enough?(expiry) do
207 {:ok, expiry}
208 else
209 {:error, "Expiry date is too soon"}
210 end
211 end
212
213 def check_expiry_date(expiry_str) do
214 Ecto.Type.cast(:integer, expiry_str)
215 |> check_expiry_date()
216 end
217
218 def post(user, %{"status" => _} = data) do
219 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
220 draft.changes
221 |> ActivityPub.create(draft.preview?)
222 |> maybe_create_activity_expiration(draft.expires_at)
223 end
224 end
225
226 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
227 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
228 {:ok, activity}
229 end
230 end
231
232 defp maybe_create_activity_expiration(result, _), do: result
233
234 # Updates the emojis for a user based on their profile
235 def update(user) do
236 emoji = emoji_from_profile(user)
237 source_data = user.info |> Map.get(:source_data, {}) |> Map.put("tag", emoji)
238
239 user =
240 with {:ok, user} <- User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
241 user
242 else
243 _e -> user
244 end
245
246 ActivityPub.update(%{
247 local: true,
248 to: [user.follower_address],
249 cc: [],
250 actor: user.ap_id,
251 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
252 })
253 end
254
255 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
256 with %Activity{
257 actor: ^user_ap_id,
258 data: %{
259 "type" => "Create"
260 },
261 object: %Object{
262 data: %{
263 "type" => "Note"
264 }
265 }
266 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
267 true <- Visibility.is_public?(activity),
268 {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
269 {:ok, activity}
270 else
271 {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
272 _ -> {:error, dgettext("errors", "Could not pin")}
273 end
274 end
275
276 def unpin(id_or_ap_id, user) do
277 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
278 {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
279 {:ok, activity}
280 else
281 %{errors: [pinned_activities: {err, _}]} -> {:error, err}
282 _ -> {:error, dgettext("errors", "Could not unpin")}
283 end
284 end
285
286 def add_mute(user, activity) do
287 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
288 {:ok, activity}
289 else
290 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
291 end
292 end
293
294 def remove_mute(user, activity) do
295 ThreadMute.remove_mute(user.id, activity.data["context"])
296 {:ok, activity}
297 end
298
299 def thread_muted?(%{id: nil} = _user, _activity), do: false
300
301 def thread_muted?(user, activity) do
302 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
303 false
304 else
305 _ -> true
306 end
307 end
308
309 def report(user, data) do
310 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
311 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
312 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
313 {:ok, statuses} <- get_report_statuses(account, data),
314 {:ok, activity} <-
315 ActivityPub.flag(%{
316 context: Utils.generate_context_id(),
317 actor: user,
318 account: account,
319 statuses: statuses,
320 content: content_html,
321 forward: data["forward"] || false
322 }) do
323 {:ok, activity}
324 else
325 {:error, err} -> {:error, err}
326 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
327 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
328 end
329 end
330
331 def update_report_state(activity_id, state) do
332 with %Activity{} = activity <- Activity.get_by_id(activity_id),
333 {:ok, activity} <- Utils.update_report_state(activity, state) do
334 {:ok, activity}
335 else
336 nil -> {:error, :not_found}
337 {:error, reason} -> {:error, reason}
338 _ -> {:error, dgettext("errors", "Could not update state")}
339 end
340 end
341
342 def update_activity_scope(activity_id, opts \\ %{}) do
343 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
344 {:ok, activity} <- toggle_sensitive(activity, opts),
345 {:ok, activity} <- set_visibility(activity, opts) do
346 {:ok, activity}
347 else
348 nil -> {:error, :not_found}
349 {:error, reason} -> {:error, reason}
350 end
351 end
352
353 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
354 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
355 end
356
357 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
358 when is_boolean(sensitive) do
359 new_data = Map.put(object.data, "sensitive", sensitive)
360
361 {:ok, object} =
362 object
363 |> Object.change(%{data: new_data})
364 |> Object.update_and_set_cache()
365
366 {:ok, Map.put(activity, :object, object)}
367 end
368
369 defp toggle_sensitive(activity, _), do: {:ok, activity}
370
371 defp set_visibility(activity, %{"visibility" => visibility}) do
372 Utils.update_activity_visibility(activity, visibility)
373 end
374
375 defp set_visibility(activity, _), do: {:ok, activity}
376
377 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
378 if ap_id not in user.info.muted_reblogs do
379 User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
380 end
381 end
382
383 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
384 if ap_id in user.info.muted_reblogs do
385 User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
386 end
387 end
388 end