Unfollow should also unsubscribe
[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.Bookmark
8 alias Pleroma.Formatter
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(%{"visibility" => visibility}, in_reply_to)
176 when visibility in ~w{public unlisted private direct},
177 do: {visibility, get_replied_to_visibility(in_reply_to)}
178
179 def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
180 visibility = get_replied_to_visibility(in_reply_to)
181 {visibility, visibility}
182 end
183
184 def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)}
185
186 def get_replied_to_visibility(nil), do: nil
187
188 def get_replied_to_visibility(activity) do
189 with %Object{} = object <- Object.normalize(activity) do
190 Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
191 end
192 end
193
194 def post(user, %{"status" => status} = data) do
195 limit = Pleroma.Config.get([:instance, :limit])
196
197 with status <- String.trim(status),
198 attachments <- attachments_from_ids(data),
199 in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
200 {visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to),
201 {_, false} <-
202 {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
203 {content_html, mentions, tags} <-
204 make_content_html(
205 status,
206 attachments,
207 data,
208 visibility
209 ),
210 mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
211 addressed_users <- get_addressed_users(mentioned_users, data["to"]),
212 {poll, poll_emoji} <- make_poll_data(data),
213 {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
214 context <- make_context(in_reply_to),
215 cw <- data["spoiler_text"] || "",
216 sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
217 full_payload <- String.trim(status <> cw),
218 :ok <- validate_character_limit(full_payload, attachments, limit),
219 object <-
220 make_note_data(
221 user.ap_id,
222 to,
223 context,
224 content_html,
225 attachments,
226 in_reply_to,
227 tags,
228 cw,
229 cc,
230 sensitive,
231 poll
232 ),
233 object <-
234 Map.put(
235 object,
236 "emoji",
237 Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
238 ) do
239 res =
240 ActivityPub.create(
241 %{
242 to: to,
243 actor: user,
244 context: context,
245 object: object,
246 additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
247 },
248 Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
249 )
250
251 res
252 else
253 {:private_to_public, true} ->
254 {:error, dgettext("errors", "The message visibility must be direct")}
255
256 {:error, _} = e ->
257 e
258
259 e ->
260 {:error, e}
261 end
262 end
263
264 # Updates the emojis for a user based on their profile
265 def update(user) do
266 user =
267 with emoji <- emoji_from_profile(user),
268 source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
269 info_cng <- User.Info.set_source_data(user.info, source_data),
270 change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
271 {:ok, user} <- User.update_and_set_cache(change) do
272 user
273 else
274 _e ->
275 user
276 end
277
278 ActivityPub.update(%{
279 local: true,
280 to: [user.follower_address],
281 cc: [],
282 actor: user.ap_id,
283 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
284 })
285 end
286
287 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
288 with %Activity{
289 actor: ^user_ap_id,
290 data: %{
291 "type" => "Create"
292 },
293 object: %Object{
294 data: %{
295 "type" => "Note"
296 }
297 }
298 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
299 true <- Visibility.is_public?(activity),
300 %{valid?: true} = info_changeset <-
301 User.Info.add_pinnned_activity(user.info, activity),
302 changeset <-
303 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
304 {:ok, _user} <- User.update_and_set_cache(changeset) do
305 {:ok, activity}
306 else
307 %{errors: [pinned_activities: {err, _}]} ->
308 {:error, err}
309
310 _ ->
311 {:error, dgettext("errors", "Could not pin")}
312 end
313 end
314
315 def unpin(id_or_ap_id, user) do
316 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
317 %{valid?: true} = info_changeset <-
318 User.Info.remove_pinnned_activity(user.info, activity),
319 changeset <-
320 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
321 {:ok, _user} <- User.update_and_set_cache(changeset) do
322 {:ok, activity}
323 else
324 %{errors: [pinned_activities: {err, _}]} ->
325 {:error, err}
326
327 _ ->
328 {:error, dgettext("errors", "Could not unpin")}
329 end
330 end
331
332 def add_mute(user, activity) do
333 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
334 {:ok, activity}
335 else
336 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
337 end
338 end
339
340 def remove_mute(user, activity) do
341 ThreadMute.remove_mute(user.id, activity.data["context"])
342 {:ok, activity}
343 end
344
345 def thread_muted?(%{id: nil} = _user, _activity), do: false
346
347 def thread_muted?(user, activity) do
348 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
349 false
350 else
351 _ -> true
352 end
353 end
354
355 def bookmarked?(user, activity) do
356 with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
357 true
358 else
359 _ ->
360 false
361 end
362 end
363
364 def report(user, data) do
365 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
366 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
367 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
368 {:ok, statuses} <- get_report_statuses(account, data),
369 {:ok, activity} <-
370 ActivityPub.flag(%{
371 context: Utils.generate_context_id(),
372 actor: user,
373 account: account,
374 statuses: statuses,
375 content: content_html,
376 forward: data["forward"] || false
377 }) do
378 {:ok, activity}
379 else
380 {:error, err} -> {:error, err}
381 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
382 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
383 end
384 end
385
386 def update_report_state(activity_id, state) do
387 with %Activity{} = activity <- Activity.get_by_id(activity_id),
388 {:ok, activity} <- Utils.update_report_state(activity, state) do
389 {:ok, activity}
390 else
391 nil -> {:error, :not_found}
392 {:error, reason} -> {:error, reason}
393 _ -> {:error, dgettext("errors", "Could not update state")}
394 end
395 end
396
397 def update_activity_scope(activity_id, opts \\ %{}) do
398 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
399 {:ok, activity} <- toggle_sensitive(activity, opts),
400 {:ok, activity} <- set_visibility(activity, opts) do
401 {:ok, activity}
402 else
403 nil -> {:error, :not_found}
404 {:error, reason} -> {:error, reason}
405 end
406 end
407
408 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
409 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
410 end
411
412 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
413 when is_boolean(sensitive) do
414 new_data = Map.put(object.data, "sensitive", sensitive)
415
416 {:ok, object} =
417 object
418 |> Object.change(%{data: new_data})
419 |> Object.update_and_set_cache()
420
421 {:ok, Map.put(activity, :object, object)}
422 end
423
424 defp toggle_sensitive(activity, _), do: {:ok, activity}
425
426 defp set_visibility(activity, %{"visibility" => visibility}) do
427 Utils.update_activity_visibility(activity, visibility)
428 end
429
430 defp set_visibility(activity, _), do: {:ok, activity}
431
432 def hide_reblogs(user, muted) do
433 ap_id = muted.ap_id
434
435 if ap_id not in user.info.muted_reblogs do
436 info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
437 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
438 User.update_and_set_cache(changeset)
439 end
440 end
441
442 def show_reblogs(user, muted) do
443 ap_id = muted.ap_id
444
445 if ap_id in user.info.muted_reblogs do
446 info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
447 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
448 User.update_and_set_cache(changeset)
449 end
450 end
451 end