common_api.ex 15.8 KB
Newer Older
1
# Pleroma: A lightweight social networking server
2
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

lain's avatar
lain committed
5
defmodule Pleroma.Web.CommonAPI do
Haelwenn's avatar
Haelwenn committed
6
  alias Pleroma.Activity
7
  alias Pleroma.ActivityExpiration
8
  alias Pleroma.Conversation.Participation
9
  alias Pleroma.FollowingRelationship
Haelwenn's avatar
Haelwenn committed
10
  alias Pleroma.Object
11
  alias Pleroma.ThreadMute
12
  alias Pleroma.User
13
  alias Pleroma.UserRelationship
lain's avatar
lain committed
14
  alias Pleroma.Web.ActivityPub.ActivityPub
15
  alias Pleroma.Web.ActivityPub.Builder
16
  alias Pleroma.Web.ActivityPub.Pipeline
17
  alias Pleroma.Web.ActivityPub.Utils
18
  alias Pleroma.Web.ActivityPub.Visibility
19

20
  import Pleroma.Web.Gettext
21
  import Pleroma.Web.CommonAPI.Utils
lain's avatar
lain committed
22

23
  require Pleroma.Constants
24
  require Logger
25

26
  def follow(follower, followed) do
minibikini's avatar
minibikini committed
27
28
    timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])

29
30
    with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
         {:ok, activity} <- ActivityPub.follow(follower, followed),
minibikini's avatar
minibikini committed
31
         {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
32
33
34
35
      {:ok, follower, followed, activity}
    end
  end

36
37
  def unfollow(follower, unfollowed) do
    with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
38
         {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
39
         {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
40
41
42
43
44
      {:ok, follower}
    end
  end

  def accept_follow_request(follower, followed) do
45
    with {:ok, follower} <- User.follow(follower, followed),
46
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
47
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
48
         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
49
50
51
52
53
54
55
56
57
58
59
60
61
         {:ok, _activity} <-
           ActivityPub.accept(%{
             to: [follower.ap_id],
             actor: followed,
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
      {:ok, follower}
    end
  end

  def reject_follow_request(follower, followed) do
    with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
62
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
63
         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
64
65
66
67
68
69
70
71
72
73
74
         {:ok, _activity} <-
           ActivityPub.reject(%{
             to: [follower.ap_id],
             actor: followed,
             object: follow_activity.data["id"],
             type: "Reject"
           }) do
      {:ok, follower}
    end
  end

lain's avatar
lain committed
75
  def delete(activity_id, user) do
76
77
    with {_, %Activity{data: %{"object" => _}} = activity} <-
           {:find_activity, Activity.get_by_id_with_object(activity_id)},
78
         %Object{} = object <- Object.normalize(activity),
79
         true <- User.superuser?(user) || user.ap_id == object.data["actor"],
minibikini's avatar
minibikini committed
80
         {:ok, _} <- unpin(activity_id, user),
81
         {:ok, delete} <- ActivityPub.delete(object) do
lain's avatar
lain committed
82
      {:ok, delete}
Sergey Suprunenko's avatar
Sergey Suprunenko committed
83
    else
84
      {:find_activity, _} -> {:error, :not_found}
minibikini's avatar
minibikini committed
85
      _ -> {:error, dgettext("errors", "Could not delete")}
lain's avatar
lain committed
86
87
    end
  end
lain's avatar
lain committed
88

89
  def repeat(id_or_ap_id, user, params \\ %{}) do
90
    with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
91
         object <- Object.normalize(activity),
92
         announce_activity <- Utils.get_existing_announce(user.ap_id, object),
93
         public <- public_announce?(object, params) do
94
95
96
97
98
      if announce_activity do
        {:ok, announce_activity, object}
      else
        ActivityPub.announce(user, object, nil, true, public)
      end
lain's avatar
lain committed
99
    else
100
      {:find_activity, _} -> {:error, :not_found}
minibikini's avatar
minibikini committed
101
      _ -> {:error, dgettext("errors", "Could not repeat")}
lain's avatar
lain committed
102
103
104
    end
  end

normandy's avatar
normandy committed
105
  def unrepeat(id_or_ap_id, user) do
106
    with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
minibikini's avatar
minibikini committed
107
      object = Object.normalize(activity)
normandy's avatar
normandy committed
108
109
      ActivityPub.unannounce(user, object)
    else
110
      {:find_activity, _} -> {:error, :not_found}
minibikini's avatar
minibikini committed
111
      _ -> {:error, dgettext("errors", "Could not unrepeat")}
normandy's avatar
normandy committed
112
113
114
    end
  end

lain's avatar
lain committed
115
  @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
116
  def favorite(%User{} = user, id) do
lain's avatar
lain committed
117
118
119
120
121
122
123
124
125
126
127
128
129
130
    case favorite_helper(user, id) do
      {:ok, _} = res ->
        res

      {:error, :not_found} = res ->
        res

      {:error, e} ->
        Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
        {:error, dgettext("errors", "Could not favorite")}
    end
  end

  def favorite_helper(user, id) do
131
132
133
134
    with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
         {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
         {_, {:ok, %Activity{} = activity, _meta}} <-
           {:common_pipeline,
135
            Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
136
      {:ok, activity}
lain's avatar
lain committed
137
    else
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
      {:find_object, _} ->
        {:error, :not_found}

      {:common_pipeline,
       {
         :error,
         {
           :validate_object,
           {
             :error,
             changeset
           }
         }
       }} = e ->
        if {:object, {"already liked by this actor", []}} in changeset.errors do
          {:ok, :already_liked}
        else
lain's avatar
lain committed
155
          {:error, e}
156
157
        end

158
      e ->
lain's avatar
lain committed
159
        {:error, e}
lain's avatar
lain committed
160
161
162
    end
  end

lain's avatar
lain committed
163
  def unfavorite(id_or_ap_id, user) do
164
    with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
minibikini's avatar
minibikini committed
165
      object = Object.normalize(activity)
lain's avatar
lain committed
166
167
      ActivityPub.unlike(user, object)
    else
168
      {:find_activity, _} -> {:error, :not_found}
minibikini's avatar
minibikini committed
169
      _ -> {:error, dgettext("errors", "Could not unfavorite")}
lain's avatar
lain committed
170
171
172
    end
  end

lain's avatar
lain committed
173
174
175
176
177
178
179
180
181
182
  def react_with_emoji(id, user, emoji) do
    with %Activity{} = activity <- Activity.get_by_id(id),
         object <- Object.normalize(activity) do
      ActivityPub.react_with_emoji(user, object, emoji)
    else
      _ ->
        {:error, dgettext("errors", "Could not add reaction emoji")}
    end
  end

lain's avatar
lain committed
183
184
185
186
187
188
189
190
191
  def unreact_with_emoji(id, user, emoji) do
    with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
      ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
    else
      _ ->
        {:error, dgettext("errors", "Could not remove reaction emoji")}
    end
  end

minibikini's avatar
minibikini committed
192
193
194
195
  def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
    with :ok <- validate_not_author(object, user),
         :ok <- validate_existing_votes(user, object),
         {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
rinpatch's avatar
rinpatch committed
196
197
198
199
      answer_activities =
        Enum.map(choices, fn index ->
          answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])

200
201
202
203
204
205
206
207
208
209
          {:ok, activity} =
            ActivityPub.create(%{
              to: answer_data["to"],
              actor: user,
              context: object.data["context"],
              object: answer_data,
              additional: %{"cc" => answer_data["cc"]}
            })

          activity
rinpatch's avatar
rinpatch committed
210
211
        end)

212
      object = Object.get_cached_by_ap_id(object.data["id"])
rinpatch's avatar
rinpatch committed
213
214
215
216
      {:ok, answer_activities, object}
    end
  end

minibikini's avatar
minibikini committed
217
218
219
220
221
222
223
224
  defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
    do: {:error, dgettext("errors", "Poll's author can't vote")}

  defp validate_not_author(_, _), do: :ok

  defp validate_existing_votes(%{ap_id: ap_id}, object) do
    if Utils.get_existing_votes(ap_id, object) == [] do
      :ok
rinpatch's avatar
rinpatch committed
225
    else
minibikini's avatar
minibikini committed
226
      {:error, dgettext("errors", "Already voted")}
rinpatch's avatar
rinpatch committed
227
228
229
    end
  end

minibikini's avatar
minibikini committed
230
231
  defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
  defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
rinpatch's avatar
rinpatch committed
232

minibikini's avatar
minibikini committed
233
234
235
236
237
238
239
240
241
242
243
244
  defp normalize_and_validate_choices(choices, object) do
    choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
    {options, max_count} = get_options_and_max_count(object)
    count = Enum.count(options)

    with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
         {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
      {:ok, options, choices}
    else
      {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
      {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
    end
245
246
  end

247
  def public_announce?(_, %{"visibility" => visibility})
248
249
250
      when visibility in ~w{public unlisted private direct},
      do: visibility in ~w(public unlisted)

251
  def public_announce?(object, _) do
252
253
254
    Visibility.is_public?(object)
  end

minibikini's avatar
minibikini committed
255
  def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
256
257

  def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
lain's avatar
lain committed
258
      when visibility in ~w{public unlisted private direct},
lain's avatar
lain committed
259
      do: {visibility, get_replied_to_visibility(in_reply_to)}
lain's avatar
lain committed
260

261
  def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
262
263
    visibility = {:list, String.to_integer(list_id)}
    {visibility, get_replied_to_visibility(in_reply_to)}
minibikini's avatar
minibikini committed
264
265
  end

266
  def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
lain's avatar
lain committed
267
268
    visibility = get_replied_to_visibility(in_reply_to)
    {visibility, visibility}
269
  end
lain's avatar
lain committed
270

271
  def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
272

273
  def get_replied_to_visibility(nil), do: nil
274

275
276
  def get_replied_to_visibility(activity) do
    with %Object{} = object <- Object.normalize(activity) do
minibikini's avatar
minibikini committed
277
      Visibility.get_visibility(object)
278
    end
279
  end
lain's avatar
lain committed
280

minibikini's avatar
minibikini committed
281
  def check_expiry_date({:ok, nil} = res), do: res
282

minibikini's avatar
minibikini committed
283
  def check_expiry_date({:ok, in_seconds}) do
284
    expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
285

286
    if ActivityExpiration.expires_late_enough?(expiry) do
287
288
289
290
291
292
      {:ok, expiry}
    else
      {:error, "Expiry date is too soon"}
    end
  end

minibikini's avatar
minibikini committed
293
  def check_expiry_date(expiry_str) do
294
295
296
297
    Ecto.Type.cast(:integer, expiry_str)
    |> check_expiry_date()
  end

kaniini's avatar
kaniini committed
298
299
300
301
302
  def listen(user, %{"title" => _} = data) do
    with visibility <- data["visibility"] || "public",
         {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
         listen_data <-
           Map.take(data, ["album", "artist", "title", "length"])
303
304
           |> Map.put("type", "Audio")
           |> Map.put("to", to)
305
306
           |> Map.put("cc", cc)
           |> Map.put("actor", user.ap_id),
kaniini's avatar
kaniini committed
307
308
309
310
311
312
         {:ok, activity} <-
           ActivityPub.listen(%{
             actor: user,
             to: to,
             object: listen_data,
             context: Utils.generate_context_id(),
313
             additional: %{"cc" => cc}
kaniini's avatar
kaniini committed
314
315
316
317
           }) do
      {:ok, activity}
    end
  end
318

minibikini's avatar
minibikini committed
319
320
321
322
323
  def post(user, %{"status" => _} = data) do
    with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
      draft.changes
      |> ActivityPub.create(draft.preview?)
      |> maybe_create_activity_expiration(draft.expires_at)
lain's avatar
lain committed
324
325
    end
  end
326

minibikini's avatar
minibikini committed
327
328
329
  defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
    with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
      {:ok, activity}
lain's avatar
lain committed
330
331
    end
  end
lain's avatar
lain committed
332

minibikini's avatar
minibikini committed
333
334
  defp maybe_create_activity_expiration(result, _), do: result

lain's avatar
lain committed
335
  # Updates the emojis for a user based on their profile
lain's avatar
lain committed
336
  def update(user) do
337
    emoji = emoji_from_profile(user)
338
    source_data = Map.put(user.source_data, "tag", emoji)
339

340
    user =
341
      case User.update_source_data(user, source_data) do
minibikini's avatar
minibikini committed
342
343
        {:ok, user} -> user
        _ -> user
344
345
      end

lain's avatar
lain committed
346
347
    ActivityPub.update(%{
      local: true,
348
      to: [Pleroma.Constants.as_public(), user.follower_address],
lain's avatar
lain committed
349
350
351
352
      cc: [],
      actor: user.ap_id,
      object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
    })
lain's avatar
lain committed
353
  end
minibikini's avatar
minibikini committed
354

355
356
357
  def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
    with %Activity{
           actor: ^user_ap_id,
minibikini's avatar
minibikini committed
358
           data: %{"type" => "Create"},
rinpatch's avatar
rinpatch committed
359
           object: %Object{data: %{"type" => object_type}}
360
         } = activity <- get_by_id_or_ap_id(id_or_ap_id),
rinpatch's avatar
rinpatch committed
361
         true <- object_type in ["Note", "Article", "Question"],
362
         true <- Visibility.is_public?(activity),
363
         {:ok, _user} <- User.add_pinnned_activity(user, activity) do
minibikini's avatar
minibikini committed
364
365
      {:ok, activity}
    else
366
      {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
minibikini's avatar
minibikini committed
367
      _ -> {:error, dgettext("errors", "Could not pin")}
minibikini's avatar
minibikini committed
368
369
370
371
372
    end
  end

  def unpin(id_or_ap_id, user) do
    with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
373
         {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
minibikini's avatar
minibikini committed
374
375
      {:ok, activity}
    else
376
      {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
minibikini's avatar
minibikini committed
377
      _ -> {:error, dgettext("errors", "Could not unpin")}
minibikini's avatar
minibikini committed
378
379
    end
  end
380
381
382
383
384

  def add_mute(user, activity) do
    with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
      {:ok, activity}
    else
385
      {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
386
387
388
389
390
391
392
393
394
395
396
    end
  end

  def remove_mute(user, activity) do
    ThreadMute.remove_mute(user.id, activity.data["context"])
    {:ok, activity}
  end

  def thread_muted?(%{id: nil} = _user, _activity), do: false

  def thread_muted?(user, activity) do
397
    ThreadMute.exists?(user.id, activity.data["context"])
398
  end
minibikini's avatar
Reports    
minibikini committed
399

minibikini's avatar
minibikini committed
400
401
  def report(user, %{"account_id" => account_id} = data) do
    with {:ok, account} <- get_reported_account(account_id),
minibikini's avatar
minibikini committed
402
         {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
minibikini's avatar
minibikini committed
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
         {:ok, statuses} <- get_report_statuses(account, data) do
      ActivityPub.flag(%{
        context: Utils.generate_context_id(),
        actor: user,
        account: account,
        statuses: statuses,
        content: content_html,
        forward: data["forward"] || false
      })
    end
  end

  def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}

  defp get_reported_account(account_id) do
    case User.get_cached_by_id(account_id) do
      %User{} = account -> {:ok, account}
      _ -> {:error, dgettext("errors", "Account not found")}
minibikini's avatar
Reports    
minibikini committed
421
422
    end
  end
423

424
425
426
427
428
429
430
  def update_report_state(activity_ids, state) when is_list(activity_ids) do
    case Utils.update_report_state(activity_ids, state) do
      :ok -> {:ok, activity_ids}
      _ -> {:error, dgettext("errors", "Could not update state")}
    end
  end

Sergey Suprunenko's avatar
Sergey Suprunenko committed
431
  def update_report_state(activity_id, state) do
minibikini's avatar
minibikini committed
432
433
    with %Activity{} = activity <- Activity.get_by_id(activity_id) do
      Utils.update_report_state(activity, state)
Sergey Suprunenko's avatar
Sergey Suprunenko committed
434
    else
435
436
      nil -> {:error, :not_found}
      _ -> {:error, dgettext("errors", "Could not update state")}
Sergey Suprunenko's avatar
Sergey Suprunenko committed
437
438
439
440
441
    end
  end

  def update_activity_scope(activity_id, opts \\ %{}) do
    with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
minibikini's avatar
minibikini committed
442
443
         {:ok, activity} <- toggle_sensitive(activity, opts) do
      set_visibility(activity, opts)
Sergey Suprunenko's avatar
Sergey Suprunenko committed
444
    else
445
446
      nil -> {:error, :not_found}
      {:error, reason} -> {:error, reason}
Sergey Suprunenko's avatar
Sergey Suprunenko committed
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
    end
  end

  defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
    toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
  end

  defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
       when is_boolean(sensitive) do
    new_data = Map.put(object.data, "sensitive", sensitive)

    {:ok, object} =
      object
      |> Object.change(%{data: new_data})
      |> Object.update_and_set_cache()

    {:ok, Map.put(activity, :object, object)}
  end

  defp toggle_sensitive(activity, _), do: {:ok, activity}

  defp set_visibility(activity, %{"visibility" => visibility}) do
    Utils.update_activity_visibility(activity, visibility)
  end

  defp set_visibility(activity, _), do: {:ok, activity}

474
475
  def hide_reblogs(%User{} = user, %User{} = target) do
    UserRelationship.create_reblog_mute(user, target)
476
477
  end

478
479
  def show_reblogs(%User{} = user, %User{} = target) do
    UserRelationship.delete_reblog_mute(user, target)
480
  end
lain's avatar
lain committed
481
end