transmogrifier.ex 35.5 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)
159
    depth = (options[:depth] || 0) + 1
lain's avatar
lain committed
160

161
    if Federator.allowed_thread_distance?(depth) do
162
163
164
165
166
167
168
169
      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
170
        e ->
feld's avatar
feld committed
171
          Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
172
173
174
175
          object
      end
    else
      object
lain's avatar
lain committed
176
177
    end
  end
lain's avatar
lain committed
178

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

181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
  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
197
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
198
199
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

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

205
  def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
lain's avatar
lain committed
206
    attachments =
207
      Enum.map(attachment, fn data ->
208
209
210
211
212
213
214
        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
215
      end)
lain's avatar
lain committed
216

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

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

226
  def fix_attachments(object), do: object
227

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

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

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

    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
244
245
246
247
248
249
250
251
252
    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

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

  def fix_url(object), do: object

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

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

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

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

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

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

281
  def fix_emoji(object), do: object
282

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

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

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

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

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

300
  def fix_tag(object), do: object
301

302
303
304
305
306
  # 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)

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

  def fix_content_map(object), do: object

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

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

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

Maksim's avatar
Maksim committed
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
351
  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

352
353
354
355
356
357
358
359
360
361
362
  # 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

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

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

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

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

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

408
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
409
         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
410
      object = fix_object(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
      with {:ok, created_activity} <- ActivityPub.create(params) do
428
429
430
431
432
433
434
435
436
        reply_depth = (options[:depth] || 0) + 1

        if Federator.allowed_thread_distance?(reply_depth) do
          for reply_id <- replies(object) do
            Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
              "id" => reply_id,
              "depth" => reply_depth
            })
          end
437
438
439
440
        end

        {:ok, created_activity}
      end
441
    else
lain's avatar
lain committed
442
      %Activity{} = activity -> {:ok, activity}
443
444
445
446
      _e -> :error
    end
  end

447
448
449
450
451
452
453
454
455
456
457
  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
458
459
      reply_depth = (options[:depth] || 0) + 1
      options = Keyword.put(options, :depth, reply_depth)
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
      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
478
  def handle_incoming(
479
480
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
        _options
lain's avatar
lain committed
481
      ) do
482
483
484
485
    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})),
486
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
487
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
488
           {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
489
490
           {_, false} <- {:user_locked, User.locked?(followed)},
           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
491
           {_, {:ok, _}} <-
492
493
             {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
           {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
kaniini's avatar
kaniini committed
494
495
        ActivityPub.accept(%{
          to: [follower.ap_id],
496
          actor: followed,
kaniini's avatar
kaniini committed
497
498
499
          object: data,
          local: true
        })
500
501
      else
        {:user_blocked, true} ->
502
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
503
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
504
505
506
507
508
509
510
511
512

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

        {:follow, {:error, _}} ->
513
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
514
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
515
516
517
518
519
520
521
522
523

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

        {:user_locked, true} ->
524
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending")
525
          :noop
526
      end
lain's avatar
lain committed
527

528
529
      {:ok, activity}
    else
530
531
      _e ->
        :error
532
533
534
    end
  end

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

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

583
584
585
586
587
588
589
590
591
592
  @misskey_reactions %{
    "like" => "👍",
    "love" => "❤️",
    "laugh" => "😆",
    "hmm" => "🤔",
    "surprise" => "😮",
    "congrats" => "🎉",
    "angry" => "💢",
    "confused" => "😥",
    "rip" => "😇",
593
594
    "pudding" => "🍮",
    "star" => "⭐"
595
596
  }

lain's avatar
lain committed
597
  @doc "Rewrite misskey likes into EmojiReacts"
598
599
600
601
602
603
604
605
  def handle_incoming(
        %{
          "type" => "Like",
          "_misskey_reaction" => reaction
        } = data,
        options
      ) do
    data
lain's avatar
lain committed
606
    |> Map.put("type", "EmojiReact")
607
    |> Map.put("content", @misskey_reactions[reaction] || reaction)
608
609
610
    |> handle_incoming(options)
  end

lain's avatar
lain committed
611
  def handle_incoming(
612
613
        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
614
      ) do
615
    with actor <- Containment.get_actor(data),
616
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
617
         {:ok, object} <- get_obj_helper(object_id),
feld's avatar
feld committed
618
         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
lain's avatar
lain committed
619
620
621
622
623
624
      {:ok, activity}
    else
      _e -> :error
    end
  end

625
626
  def handle_incoming(
        %{
lain's avatar
lain committed
627
          "type" => "EmojiReact",
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
          "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
646
  def handle_incoming(
647
648
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
649
      ) do
650
    with actor <- Containment.get_actor(data),
651
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
652
         {:ok, object} <- get_embedded_obj_helper(object_id, actor),
lain's avatar
lain committed
653
         public <- Visibility.is_public?(data),
654
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
lain's avatar
lain committed
655
656
657
658
659
660
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
661
  def handle_incoming(
662
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
663
664
          data,
        _options
665
      )
666
667
668
669
      when object_type in [
             "Person",
             "Application",
             "Service",
670
             "Organization"
671
           ] do
minibikini's avatar
minibikini committed
672
    with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
lain's avatar
lain committed
673
674
675
      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)

      actor
676
      |> User.upgrade_changeset(new_user_data, true)
lain's avatar
lain committed
677
      |> User.update_and_set_cache()
lain's avatar
lain committed
678

lain's avatar
lain committed
679
680
681
682
683
      ActivityPub.update(%{
        local: false,
        to: data["to"] || [],
        cc: data["cc"] || [],
        object: object,
684
685
        actor: actor_id,
        activity_id: data["id"]
lain's avatar
lain committed
686
      })
lain's avatar
lain committed
687
688
689
690
691
692
693
    else
      e ->
        Logger.error(e)
        :error
    end
  end

694
695
696
697
698
  # 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
699
  def handle_incoming(
700
        %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
701
        _options
lain's avatar
lain committed
702
      ) do
lain's avatar
lain committed
703
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
704

705
    with actor <- Containment.get_actor(data),
706
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
707
         {:ok, object} <- get_obj_helper(object_id),
708
         :ok <- Containment.contain_origin(actor.ap_id, object.data),
709
710
         {:ok, activity} <-
           ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
lain's avatar
lain committed
711
712
      {:ok, activity}
    else
713
714
715
      nil ->
        case User.get_cached_by_ap_id(object_id) do
          %User{ap_id: ^actor} = user ->
716
            User.delete(user)
717
718
719
720
721
722
723

          nil ->
            :error
        end

      _e ->
        :error
lain's avatar
lain committed
724
725
726
    end
  end

727
  def handle_incoming(
728
729
        %{
          "type" => "Undo",
730
          "object" => %{"type" => "Announce", "object" => object_id},
Maksim's avatar
Maksim committed
731
          "actor" => _actor,
732
          "id" => id
733
734
        } = data,
        _options
735
      ) do
736
    with actor <- Containment.get_actor(data),
737
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
738
         {:ok, object} <- get_obj_helper(object_id),
739
         {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
740
741
      {:ok, activity}
    else
Thog's avatar
Thog committed
742
      _e -> :error
743
744
745
    end
  end

normandy's avatar
normandy committed
746
747
748
749
750
751
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Follow", "object" => followed},
          "actor" => follower,
          "id" => id
752
753
        } = _data,
        _options
normandy's avatar
normandy committed
754
      ) do
normandy's avatar
normandy committed
755
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
756
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
normandy's avatar
normandy committed
757
         {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
normandy's avatar
normandy committed
758
759
760
      User.unfollow(follower, followed)
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
761
      _e -> :error
normandy's avatar
normandy committed
762
763
764
    end
  end

765
766
767
  def handle_incoming(
        %{
          "type" => "Undo",
lain's avatar
lain committed
768
          "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
          "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
787
788
789
790
791
792
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Block", "object" => blocked},
          "actor" => blocker,
          "id" => id
793
794
        } = _data,
        _options
normandy's avatar
normandy committed
795
      ) do
796
    with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
797
         {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
798
         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
normandy's avatar
normandy committed
799
      User.unblock(blocker, blocked)
normandy's avatar
normandy committed
800
801
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
802
      _e -> :error
normandy's avatar
normandy committed
803
804
805
    end
  end

806
  def handle_incoming(
807
808
        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
        _options
809
      ) do
810
    with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
0x1C3B00DA's avatar
0x1C3B00DA committed
811
         {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
812
         {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
813
      User.unfollow(blocker, blocked)
814
      User.block(blocker, blocked)
normandy's avatar
normandy committed
815
816
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
817
      _e -> :error
normandy's avatar
normandy committed
818
819
    end
  end
820

Thog's avatar
Thog committed
821
822
823
824
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Like", "object" => object_id},
Maksim's avatar
Maksim committed
825
          "actor" => _actor,
Thog's avatar
Thog committed
826
          "id" => id
827
828
        } = data,
        _options
Thog's avatar
Thog committed
829
      ) do
830
    with actor <- Containment.get_actor(data),
831
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
832
         {:ok, object} <- get_obj_helper(object_id),
Thog's avatar
Thog committed
833
834
835
         {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
      {:ok, activity}
    else
Thog's avatar
Thog committed
836
      _e -> :error
Thog's avatar
Thog committed
837
838
839
    end
  end

840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
  # 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
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
  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

876
  def handle_incoming(_, _), do: :error
877

878
  @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
879
  def get_obj_helper(id, options \\ []) do
Maksim's avatar
Maksim committed
880
881
882
    case Object.normalize(id, true, options) do
      %Object{} = object -> {:ok, object}
      _ -> nil
883
    end
884
885
  end

886
  @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
Thibaut Girka's avatar
Thibaut Girka committed
887
  def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
888
889
        ap_id: ap_id
      })
Thibaut Girka's avatar
Thibaut Girka committed
890
      when attributed_to == ap_id do
891
892
893
894
895
    with {:ok, activity} <-
           handle_incoming(%{
             "type" => "Create",
             "to" => data["to"],
             "cc" => data["cc"],
Thibaut Girka's avatar
Thibaut Girka committed
896
             "actor" => attributed_to,
897
898
899
900
901
902
903
904
905
906
907
908
             "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

909
910
911
912
  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)
913
914
915
916
    else
      _e -> object
    end
  end
lain's avatar
lain committed
917

918
919
  def set_reply_to_uri(obj), do: obj

920
921
922
923
  @doc """
  Serialized Mastodon-compatible `replies` collection containing _self-replies_.
  Based on Mastodon's ActivityPub::NoteSerializer#replies.
  """
924
  def set_replies(obj_data) do
925
    replies_uris =
Ivan Tashkinov's avatar
Ivan Tashkinov committed
926
927
      with limit when limit > 0 <-
             Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
928
929
930
931
           %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
        object
        |> Object.self_replies()
        |> select([o], fragment("?->>'id'", o.data))
932
933
        |> limit(^limit)
        |> Repo.all()
Ivan Tashkinov's avatar
Ivan Tashkinov committed
934
935
      else
        _ -> []
936
937
      end

Ivan Tashkinov's avatar
Ivan Tashkinov committed
938
    set_replies(obj_data, replies_uris)
939
940
  end

Ivan Tashkinov's avatar
Ivan Tashkinov committed
941
  defp set_replies(obj, []) do
942
943
944
945
946
947
    obj
  end

  defp set_replies(obj, replies_uris) do
    replies_collection = %{</