transmogrifier.ex 35.7 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

5
6
7
8
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
  @moduledoc """
  A module to handle coding from internal to wire ActivityPub and back.
  """
Haelwenn's avatar
Haelwenn committed
9
  alias Pleroma.Activity
10
  alias Pleroma.FollowingRelationship
Haelwenn's avatar
Haelwenn committed
11
  alias Pleroma.Object
rinpatch's avatar
rinpatch committed
12
  alias Pleroma.Object.Containment
Haelwenn's avatar
Haelwenn committed
13
  alias Pleroma.Repo
14
  alias Pleroma.User
Haelwenn's avatar
Haelwenn committed
15
16
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
17
  alias Pleroma.Web.ActivityPub.Visibility
18
  alias Pleroma.Web.Federator
19
  alias Pleroma.Workers.TransmogrifierWorker
20

lain's avatar
lain committed
21
22
  import Ecto.Query

23
  require Logger
24
  require Pleroma.Constants
25

26
27
28
  @doc """
  Modifies an incoming AP object (mastodon format) to our internal format.
  """
29
  def fix_object(object, options \\ []) do
30
    object
31
    |> strip_internal_fields
32
    |> fix_actor
33
    |> fix_url
34
    |> fix_attachments
lain's avatar
lain committed
35
    |> fix_context
36
    |> fix_in_reply_to(options)
lain's avatar
lain committed
37
    |> fix_emoji
38
    |> fix_tag
39
    |> fix_content_map
40
    |> fix_addressing
41
    |> fix_summary
42
    |> fix_type(options)
43
44
45
  end

  def fix_summary(%{"summary" => nil} = object) do
46
    Map.put(object, "summary", "")
47
48
49
50
51
52
53
  end

  def fix_summary(%{"summary" => _} = object) do
    # summary is present, nothing to do
    object
  end

54
  def fix_summary(object), do: Map.put(object, "summary", "")
55
56

  def fix_addressing_list(map, field) do
57
58
59
60
61
62
63
64
65
    cond do
      is_binary(map[field]) ->
        Map.put(map, field, [map[field]])

      is_nil(map[field]) ->
        Map.put(map, field, [])

      true ->
        map
66
67
68
    end
  end

69
70
71
72
73
  def fix_explicit_addressing(
        %{"to" => to, "cc" => cc} = object,
        explicit_mentions,
        follower_collection
      ) do
74
    explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
75

76
    explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
77
78
79

    final_cc =
      (cc ++ explicit_cc)
80
      |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
81
82
83
84
85
86
87
      |> Enum.uniq()

    object
    |> Map.put("to", explicit_to)
    |> Map.put("cc", final_cc)
  end

88
  def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
89

90
91
  # if directMessage flag is set to true, leave the addressing alone
  def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
92

93
  def fix_explicit_addressing(object) do
94
    explicit_mentions = Utils.determine_explicit_mentions(object)
95

96
97
98
99
    %User{follower_address: follower_collection} =
      object
      |> Containment.get_actor()
      |> User.get_cached_by_ap_id()
100

101
102
103
104
105
106
    explicit_mentions =
      explicit_mentions ++
        [
          Pleroma.Constants.as_public(),
          follower_collection
        ]
107

108
    fix_explicit_addressing(object, explicit_mentions, follower_collection)
lain's avatar
lain committed
109
110
  end

111
112
113
114
115
116
117
  # if as:Public is addressed, then make sure the followers collection is also addressed
  # so that the activities will be delivered to local users.
  def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
    recipients = to ++ cc

    if followers_collection not in recipients do
      cond do
118
        Pleroma.Constants.as_public() in cc ->
119
120
121
          to = to ++ [followers_collection]
          Map.put(object, "to", to)

122
        Pleroma.Constants.as_public() in to ->
123
124
125
126
127
128
          cc = cc ++ [followers_collection]
          Map.put(object, "cc", cc)

        true ->
          object
      end
129
    else
130
      object
131
132
133
    end
  end

134
135
  def fix_implicit_addressing(object, _), do: object

136
  def fix_addressing(object) do
Alexander Strizhakov's avatar
Alexander Strizhakov committed
137
    {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
138
139
    followers_collection = User.ap_followers(user)

140
    object
141
142
143
144
    |> fix_addressing_list("to")
    |> fix_addressing_list("cc")
    |> fix_addressing_list("bto")
    |> fix_addressing_list("bcc")
145
    |> fix_explicit_addressing()
146
    |> fix_implicit_addressing(followers_collection)
lain's avatar
lain committed
147
148
  end

149
  def fix_actor(%{"attributedTo" => actor} = object) do
150
    Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
151
152
  end

153
154
155
  def fix_in_reply_to(object, options \\ [])

  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
156
      when not is_nil(in_reply_to) do
157
    in_reply_to_id = prepare_in_reply_to(in_reply_to)
158
    object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
lain's avatar
lain committed
159

160
    if Federator.allowed_incoming_reply_depth?(options[:depth]) do
161
162
163
164
165
166
167
168
      with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
           %Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
        object
        |> Map.put("inReplyTo", replied_object.data["id"])
        |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
        |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
        |> Map.put("context", replied_object.data["context"] || object["conversation"])
      else
169
        e ->
feld's avatar
feld committed
170
          Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
171
172
173
174
          object
      end
    else
      object
lain's avatar
lain committed
175
176
    end
  end
lain's avatar
lain committed
177

178
  def fix_in_reply_to(object, _options), do: object
lain's avatar
lain committed
179

180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
  defp prepare_in_reply_to(in_reply_to) do
    cond do
      is_bitstring(in_reply_to) ->
        in_reply_to

      is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
        in_reply_to["id"]

      is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
        Enum.at(in_reply_to, 0)

      true ->
        ""
    end
  end

lain's avatar
lain committed
196
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
197
198
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

lain's avatar
lain committed
199
    object
Haelwenn's avatar
Haelwenn committed
200
201
    |> Map.put("context", context)
    |> Map.put("conversation", context)
lain's avatar
lain committed
202
203
  end

204
  def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
lain's avatar
lain committed
205
    attachments =
206
      Enum.map(attachment, fn data ->
207
208
209
210
211
212
213
        media_type = data["mediaType"] || data["mimeType"]
        href = data["url"] || data["href"]
        url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]

        data
        |> Map.put("mediaType", media_type)
        |> Map.put("url", url)
lain's avatar
lain committed
214
      end)
lain's avatar
lain committed
215

216
    Map.put(object, "attachment", attachments)
217
218
  end

219
  def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
Maksim's avatar
Maksim committed
220
221
222
    object
    |> Map.put("attachment", [attachment])
    |> fix_attachments()
223
224
  end

225
  def fix_attachments(object), do: object
226

227
  def fix_url(%{"url" => url} = object) when is_map(url) do
228
    Map.put(object, "url", url["href"])
229
230
  end

231
232
233
  def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
    first_element = Enum.at(url, 0)

234
    link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
235
236
237
238
239
240
241
242

    object
    |> Map.put("attachment", [first_element])
    |> Map.put("url", link_element["href"])
  end

  def fix_url(%{"type" => object_type, "url" => url} = object)
      when object_type != "Video" and is_list(url) do
243
244
245
246
247
248
249
250
251
    first_element = Enum.at(url, 0)

    url_string =
      cond do
        is_bitstring(first_element) -> first_element
        is_map(first_element) -> first_element["href"] || ""
        true -> ""
      end

252
    Map.put(object, "url", url_string)
253
254
255
256
  end

  def fix_url(object), do: object

257
  def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
lain's avatar
lain committed
258
    emoji =
259
260
      tags
      |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
lain's avatar
lain committed
261
      |> Enum.reduce(%{}, fn data, mapping ->
262
        name = String.trim(data["name"], ":")
lain's avatar
lain committed
263

264
        Map.put(mapping, name, data["icon"]["url"])
lain's avatar
lain committed
265
      end)
lain's avatar
lain committed
266
267
268
269

    # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
    emoji = Map.merge(object["emoji"] || %{}, emoji)

270
    Map.put(object, "emoji", emoji)
lain's avatar
lain committed
271
272
  end

273
274
275
276
  def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
    name = String.trim(tag["name"], ":")
    emoji = %{name => tag["icon"]["url"]}

277
    Map.put(object, "emoji", emoji)
278
279
  end

280
  def fix_emoji(object), do: object
281

282
  def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
lain's avatar
lain committed
283
    tags =
284
      tag
lain's avatar
lain committed
285
286
      |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
      |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
287

288
    Map.put(object, "tag", tag ++ tags)
289
290
  end

291
292
  def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
    combined = [tag, String.slice(hashtag, 1..-1)]
293

294
    Map.put(object, "tag", combined)
295
296
  end

297
298
  def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])

299
  def fix_tag(object), do: object
300

301
302
303
304
305
  # content map usually only has one language so this will do for now.
  def fix_content_map(%{"contentMap" => content_map} = object) do
    content_groups = Map.to_list(content_map)
    {_, content} = Enum.at(content_groups, 0)

306
    Map.put(object, "content", content)
307
308
309
310
  end

  def fix_content_map(object), do: object

311
312
  def fix_type(object, options \\ [])

313
314
  def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
      when is_binary(reply_id) do
315
316
    with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
         {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
317
318
      Map.put(object, "type", "Answer")
    else
319
      _ -> object
320
321
322
    end
  end

323
  def fix_type(object, _), do: object
324

Maksim's avatar
Maksim committed
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
  defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
    with true <- id =~ "follows",
         %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
         %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
      {:ok, activity}
    else
      _ -> {:error, nil}
    end
  end

  defp mastodon_follow_hack(_, _), do: {:error, nil}

  defp get_follow_activity(follow_object, followed) do
    with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
         {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
      {:ok, activity}
    else
      # Can't find the activity. This might a Mastodon 2.3 "Accept"
      {:activity, nil} ->
        mastodon_follow_hack(follow_object, followed)

      _ ->
        {:error, nil}
    end
  end

351
352
353
354
355
356
357
358
359
360
361
  # Reduce the object list to find the reported user.
  defp get_reported(objects) do
    Enum.reduce_while(objects, nil, fn ap_id, _ ->
      with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
        {:halt, user}
      else
        _ -> {:cont, nil}
      end
    end)
  end

362
363
  def handle_incoming(data, options \\ [])

364
365
  # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
  # with nil ID.
366
  def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
367
368
369
370
    with context <- data["context"] || Utils.generate_context_id(),
         content <- data["content"] || "",
         %User{} = actor <- User.get_cached_by_ap_id(actor),
         # Reduce the object list to find the reported user.
371
         %User{} = account <- get_reported(objects),
372
373
         # Remove the reported user from the object list.
         statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
374
      %{
375
376
377
378
379
        actor: actor,
        context: context,
        account: account,
        statuses: statuses,
        content: content,
380
        additional: %{"cc" => [account.ap_id]}
381
      }
382
      |> ActivityPub.flag()
383
384
385
    end
  end

386
  # disallow objects with bogus IDs
387
388
  def handle_incoming(%{"id" => nil}, _options), do: :error
  def handle_incoming(%{"id" => ""}, _options), do: :error
389
  # length of https:// = 8, should validate better, but good enough for now.
rinpatch's avatar
rinpatch committed
390
  def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
391
    do: :error
392

393
394
395
  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
396
397
398
399
  def handle_incoming(
        %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
        options
      )
400
      when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer"] do
401
    actor = Containment.get_actor(data)
402
403
404
405

    data =
      Map.put(data, "actor", actor)
      |> fix_addressing
406

407
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
408
         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
409
410
      options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
      object = fix_object(data["object"], options)
411

412
413
414
415
      params = %{
        to: data["to"],
        object: object,
        actor: user,
lain's avatar
lain committed
416
        context: object["conversation"],
417
418
        local: false,
        published: data["published"],
lain's avatar
lain committed
419
420
421
        additional:
          Map.take(data, [
            "cc",
422
            "directMessage",
lain's avatar
lain committed
423
424
            "id"
          ])
425
426
      }

427
428
429
430
431
432
433
      with {:ok, created_activity} <- ActivityPub.create(params) do
        for reply_id <- replies(object) do
          Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{"id" => reply_id})
        end

        {:ok, created_activity}
      end
434
    else
lain's avatar
lain committed
435
      %Activity{} = activity -> {:ok, activity}
436
437
438
439
      _e -> :error
    end
  end

440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
  def handle_incoming(
        %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
        options
      ) do
    actor = Containment.get_actor(data)

    data =
      Map.put(data, "actor", actor)
      |> fix_addressing

    with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
      options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
      object = fix_object(object, options)

      params = %{
        to: data["to"],
        object: object,
        actor: user,
        context: nil,
        local: false,
        published: data["published"],
        additional: Map.take(data, ["cc", "id"])
      }

      ActivityPub.listen(params)
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
470
  def handle_incoming(
471
472
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
        _options
lain's avatar
lain committed
473
      ) do
474
475
476
477
    with %User{local: true} = followed <-
           User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
         {:ok, %User{} = follower} <-
           User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
478
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
479
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
480
           {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
481
482
           {_, false} <- {:user_locked, User.locked?(followed)},
           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
483
           {_, {:ok, _}} <-
484
485
             {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
           {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
kaniini's avatar
kaniini committed
486
487
        ActivityPub.accept(%{
          to: [follower.ap_id],
488
          actor: followed,
kaniini's avatar
kaniini committed
489
490
491
          object: data,
          local: true
        })
492
493
      else
        {:user_blocked, true} ->
494
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
495
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
496
497
498
499
500
501
502
503
504

          ActivityPub.reject(%{
            to: [follower.ap_id],
            actor: followed,
            object: data,
            local: true
          })

        {:follow, {:error, _}} ->
505
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
506
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
507
508
509
510
511
512
513
514
515

          ActivityPub.reject(%{
            to: [follower.ap_id],
            actor: followed,
            object: data,
            local: true
          })

        {:user_locked, true} ->
516
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending")
517
          :noop
518
      end
lain's avatar
lain committed
519

520
521
      {:ok, activity}
    else
522
523
      _e ->
        :error
524
525
526
    end
  end

527
  def handle_incoming(
528
        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
529
        _options
530
      ) do
531
    with actor <- Containment.get_actor(data),
532
         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
533
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
534
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
535
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
536
         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
537
538
539
540
541
      ActivityPub.accept(%{
        to: follow_activity.data["to"],
        type: "Accept",
        actor: followed,
        object: follow_activity.data["id"],
542
543
        local: false,
        activity_id: id
544
      })
545
546
    else
      _e -> :error
547
548
549
550
    end
  end

  def handle_incoming(
551
        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
552
        _options
553
      ) do
554
    with actor <- Containment.get_actor(data),
555
         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
556
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
557
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
558
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
559
         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
lain's avatar
lain committed
560
         {:ok, activity} <-
561
           ActivityPub.reject(%{
lain's avatar
lain committed
562
             to: follow_activity.data["to"],
563
             type: "Reject",
564
             actor: followed,
lain's avatar
lain committed
565
             object: follow_activity.data["id"],
566
567
             local: false,
             activity_id: id
lain's avatar
lain committed
568
           }) do
569
      {:ok, activity}
570
571
    else
      _e -> :error
572
573
574
    end
  end

575
576
577
578
579
580
581
582
583
584
  @misskey_reactions %{
    "like" => "👍",
    "love" => "❤️",
    "laugh" => "😆",
    "hmm" => "🤔",
    "surprise" => "😮",
    "congrats" => "🎉",
    "angry" => "💢",
    "confused" => "😥",
    "rip" => "😇",
585
586
    "pudding" => "🍮",
    "star" => "⭐"
587
588
  }

lain's avatar
lain committed
589
  @doc "Rewrite misskey likes into EmojiReacts"
590
591
592
593
594
595
596
597
  def handle_incoming(
        %{
          "type" => "Like",
          "_misskey_reaction" => reaction
        } = data,
        options
      ) do
    data
lain's avatar
lain committed
598
    |> Map.put("type", "EmojiReact")
599
    |> Map.put("content", @misskey_reactions[reaction] || reaction)
600
601
602
    |> handle_incoming(options)
  end

lain's avatar
lain committed
603
  def handle_incoming(
604
605
        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
606
      ) do
607
    with actor <- Containment.get_actor(data),
608
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
609
         {:ok, object} <- get_obj_helper(object_id),
feld's avatar
feld committed
610
         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
lain's avatar
lain committed
611
612
613
614
615
616
      {:ok, activity}
    else
      _e -> :error
    end
  end

617
618
  def handle_incoming(
        %{
lain's avatar
lain committed
619
          "type" => "EmojiReact",
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
          "object" => object_id,
          "actor" => _actor,
          "id" => id,
          "content" => emoji
        } = data,
        _options
      ) do
    with actor <- Containment.get_actor(data),
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
         {:ok, object} <- get_obj_helper(object_id),
         {:ok, activity, _object} <-
           ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
638
  def handle_incoming(
639
640
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
641
      ) do
642
    with actor <- Containment.get_actor(data),
643
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
644
         {:ok, object} <- get_embedded_obj_helper(object_id, actor),
lain's avatar
lain committed
645
         public <- Visibility.is_public?(data),
646
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
lain's avatar
lain committed
647
648
649
650
651
652
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
653
  def handle_incoming(
654
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
655
656
          data,
        _options
657
      )
658
659
660
661
      when object_type in [
             "Person",
             "Application",
             "Service",
662
             "Organization"
663
           ] do
minibikini's avatar
minibikini committed
664
    with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
lain's avatar
lain committed
665
666
667
      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)

      actor
668
      |> User.upgrade_changeset(new_user_data, true)
lain's avatar
lain committed
669
      |> User.update_and_set_cache()
lain's avatar
lain committed
670

lain's avatar
lain committed
671
672
673
674
675
      ActivityPub.update(%{
        local: false,
        to: data["to"] || [],
        cc: data["cc"] || [],
        object: object,
676
677
        actor: actor_id,
        activity_id: data["id"]
lain's avatar
lain committed
678
      })
lain's avatar
lain committed
679
680
681
682
683
684
685
    else
      e ->
        Logger.error(e)
        :error
    end
  end

686
687
688
689
690
  # TODO: We presently assume that any actor on the same origin domain as the object being
  # deleted has the rights to delete that object.  A better way to validate whether or not
  # the object should be deleted is to refetch the object URI, which should return either
  # an error or a tombstone.  This would allow us to verify that a deletion actually took
  # place.
lain's avatar
lain committed
691
  def handle_incoming(
692
        %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
693
        _options
lain's avatar
lain committed
694
      ) do
lain's avatar
lain committed
695
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
696

697
    with actor <- Containment.get_actor(data),
698
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
699
         {:ok, object} <- get_obj_helper(object_id),
700
         :ok <- Containment.contain_origin(actor.ap_id, object.data),
701
702
         {:ok, activity} <-
           ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
lain's avatar
lain committed
703
704
      {:ok, activity}
    else
705
706
707
      nil ->
        case User.get_cached_by_ap_id(object_id) do
          %User{ap_id: ^actor} = user ->
708
            User.delete(user)
709
710
711
712
713
714
715

          nil ->
            :error
        end

      _e ->
        :error
lain's avatar
lain committed
716
717
718
    end
  end

719
  def handle_incoming(
720
721
        %{
          "type" => "Undo",
722
          "object" => %{"type" => "Announce", "object" => object_id},
Maksim's avatar
Maksim committed
723
          "actor" => _actor,
724
          "id" => id
725
726
        } = data,
        _options
727
      ) do
728
    with actor <- Containment.get_actor(data),
729
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
730
         {:ok, object} <- get_obj_helper(object_id),
731
         {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
732
733
      {:ok, activity}
    else
Thog's avatar
Thog committed
734
      _e -> :error
735
736
737
    end
  end

normandy's avatar
normandy committed
738
739
740
741
742
743
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Follow", "object" => followed},
          "actor" => follower,
          "id" => id
744
745
        } = _data,
        _options
normandy's avatar
normandy committed
746
      ) do
normandy's avatar
normandy committed
747
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
748
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
normandy's avatar
normandy committed
749
         {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
normandy's avatar
normandy committed
750
751
752
      User.unfollow(follower, followed)
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
753
      _e -> :error
normandy's avatar
normandy committed
754
755
756
    end
  end

757
758
759
  def handle_incoming(
        %{
          "type" => "Undo",
lain's avatar
lain committed
760
          "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
          "actor" => _actor,
          "id" => id
        } = data,
        _options
      ) do
    with actor <- Containment.get_actor(data),
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
         {:ok, activity, _} <-
           ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
             activity_id: id,
             local: false
           ) do
      {:ok, activity}
    else
      _e -> :error
    end
  end

normandy's avatar
normandy committed
779
780
781
782
783
784
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Block", "object" => blocked},
          "actor" => blocker,
          "id" => id
785
786
        } = _data,
        _options
normandy's avatar
normandy committed
787
      ) do
788
    with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
789
         {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
790
         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
normandy's avatar
normandy committed
791
      User.unblock(blocker, blocked)
normandy's avatar
normandy committed
792
793
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
794
      _e -> :error
normandy's avatar
normandy committed
795
796
797
    end
  end

798
  def handle_incoming(
799
800
        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
        _options
801
      ) do
802
    with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
0x1C3B00DA's avatar
0x1C3B00DA committed
803
         {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
804
         {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
805
      User.unfollow(blocker, blocked)
806
      User.block(blocker, blocked)
normandy's avatar
normandy committed
807
808
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
809
      _e -> :error
normandy's avatar
normandy committed
810
811
    end
  end
812

Thog's avatar
Thog committed
813
814
815
816
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Like", "object" => object_id},
Maksim's avatar
Maksim committed
817
          "actor" => _actor,
Thog's avatar
Thog committed
818
          "id" => id
819
820
        } = data,
        _options
Thog's avatar
Thog committed
821
      ) do
822
    with actor <- Containment.get_actor(data),
823
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
824
         {:ok, object} <- get_obj_helper(object_id),
Thog's avatar
Thog committed
825
826
827
         {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
      {:ok, activity}
    else
Thog's avatar
Thog committed
828
      _e -> :error
Thog's avatar
Thog committed
829
830
831
    end
  end

832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
  # For Undos that don't have the complete object attached, try to find it in our database.
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => object
        } = activity,
        options
      )
      when is_binary(object) do
    with %Activity{data: data} <- Activity.get_by_ap_id(object) do
      activity
      |> Map.put("object", data)
      |> handle_incoming(options)
    else
      _e -> :error
    end
  end

minibikini's avatar
minibikini committed
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
  def handle_incoming(
        %{
          "type" => "Move",
          "actor" => origin_actor,
          "object" => origin_actor,
          "target" => target_actor
        },
        _options
      ) do
    with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
         {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
         true <- origin_actor in target_user.also_known_as do
      ActivityPub.move(origin_user, target_user, false)
    else
      _e -> :error
    end
  end

868
  def handle_incoming(_, _), do: :error
869

870
  @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
871
  def get_obj_helper(id, options \\ []) do
Maksim's avatar
Maksim committed
872
873
874
    case Object.normalize(id, true, options) do
      %Object{} = object -> {:ok, object}
      _ -> nil
875
    end
876
877
  end

878
  @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
Thibaut Girka's avatar
Thibaut Girka committed
879
  def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
880
881
        ap_id: ap_id
      })
Thibaut Girka's avatar
Thibaut Girka committed
882
      when attributed_to == ap_id do
883
884
885
886
887
    with {:ok, activity} <-
           handle_incoming(%{
             "type" => "Create",
             "to" => data["to"],
             "cc" => data["cc"],
Thibaut Girka's avatar
Thibaut Girka committed
888
             "actor" => attributed_to,
889
890
891
892
893
894
895
896
897
898
899
900
             "object" => data
           }) do
      {:ok, Object.normalize(activity)}
    else
      _ -> get_obj_helper(object_id)
    end
  end

  def get_embedded_obj_helper(object_id, _) do
    get_obj_helper(object_id)
  end

901
902
903
904
  def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
    with false <- String.starts_with?(in_reply_to, "http"),
         {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
      Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
905
906
907
908
    else
      _e -> object
    end
  end
lain's avatar
lain committed
909

910
911
  def set_reply_to_uri(obj), do: obj

912
913
914
915
916
  @doc """
  Serialized Mastodon-compatible `replies` collection containing _self-replies_.
  Based on Mastodon's ActivityPub::NoteSerializer#replies.
  """
  def set_replies(obj) do
917
    limit = Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0)
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954

    replies_uris =
      with true <- limit > 0 || nil,
           %Activity{} = activity <- Activity.get_create_by_object_ap_id(obj["id"]) do
        activity
        |> Activity.self_replies()
        |> select([a], fragment("?->>'id'", a.data))
        |> limit(^limit)
        |> Repo.all()
      end

    set_replies(obj, replies_uris || [])
  end

  defp set_replies(obj, replies_uris) when replies_uris in [nil, []] do
    obj
  end

  defp set_replies(obj, replies_uris) do
    # Note: stubs (Mastodon doesn't make separate requests via those URIs in FetchRepliesService)
    masto_replies_uri = nil
    masto_replies_next_page_uri = nil

    replies_collection = %{
      "type" => "Collection",
      "id" => masto_replies_uri,
      "first" => %{
        "type" => "Collection",
        "part_of" => masto_replies_uri,
        "items" => replies_uris,
        "next" => masto_replies_next_page_uri
      }
    }

    Map.merge(obj, %{"replies" => replies_collection})
  end

955
  def replies(%{"replies" => replies = %{}}) do
956
957
958
959
960
961
962
    replies =
      if is_map(replies["first"]) do
        replies["first"]
      else
        replies
      end

963
964
965
966
967
    replies["items"] || []
  end

  def replies(_), do: []

968
  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
969
970
  def prepare_object(object) do
    object
lain's avatar
lain committed
971
    |> set_sensitive
lain's avatar
lain committed
972
    |> add_hashtags
lain's avatar
lain committed
973
    |> add_mention_tags
lain's avatar
lain committed
974
    |> add_emoji_tags
lain's avatar
lain committed