transmogrifier.ex 34.1 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
      }

      ActivityPub.create(params)
    else
lain's avatar
lain committed
429
      %Activity{} = activity -> {:ok, activity}
430
431
432
433
      _e -> :error
    end
  end

434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
  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
464
  def handle_incoming(
465
466
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
        _options
lain's avatar
lain committed
467
      ) do
468
469
470
471
    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})),
472
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
473
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
474
           {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
475
476
           {_, false} <- {:user_locked, User.locked?(followed)},
           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
477
           {_, {:ok, _}} <-
478
479
             {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
           {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
kaniini's avatar
kaniini committed
480
481
        ActivityPub.accept(%{
          to: [follower.ap_id],
482
          actor: followed,
kaniini's avatar
kaniini committed
483
484
485
          object: data,
          local: true
        })
486
487
      else
        {:user_blocked, true} ->
488
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
489
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
490
491
492
493
494
495
496
497
498

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

        {:follow, {:error, _}} ->
499
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
500
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
501
502
503
504
505
506
507
508
509

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

        {:user_locked, true} ->
510
          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending")
511
          :noop
512
      end
lain's avatar
lain committed
513

514
515
      {:ok, activity}
    else
516
517
      _e ->
        :error
518
519
520
    end
  end

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

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

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

  @doc "Rewrite misskey likes into EmojiReactions"
  def handle_incoming(
        %{
          "type" => "Like",
          "_misskey_reaction" => reaction
        } = data,
        options
      ) do
    data
    |> Map.put("type", "EmojiReaction")
593
    |> Map.put("content", @misskey_reactions[reaction] || reaction)
594
595
596
    |> handle_incoming(options)
  end

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

611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
  def handle_incoming(
        %{
          "type" => "EmojiReaction",
          "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
632
  def handle_incoming(
633
634
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
635
      ) do
636
    with actor <- Containment.get_actor(data),
637
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
638
         {:ok, object} <- get_embedded_obj_helper(object_id, actor),
lain's avatar
lain committed
639
         public <- Visibility.is_public?(data),
640
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
lain's avatar
lain committed
641
642
643
644
645
646
      {:ok, activity}
    else
      _e -> :error
    end
  end

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

      actor
662
      |> User.upgrade_changeset(new_user_data, true)
lain's avatar
lain committed
663
      |> User.update_and_set_cache()
lain's avatar
lain committed
664

lain's avatar
lain committed
665
666
667
668
669
      ActivityPub.update(%{
        local: false,
        to: data["to"] || [],
        cc: data["cc"] || [],
        object: object,
670
671
        actor: actor_id,
        activity_id: data["id"]
lain's avatar
lain committed
672
      })
lain's avatar
lain committed
673
674
675
676
677
678
679
    else
      e ->
        Logger.error(e)
        :error
    end
  end

680
681
682
683
684
  # 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
685
  def handle_incoming(
686
        %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
687
        _options
lain's avatar
lain committed
688
      ) do
lain's avatar
lain committed
689
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
690

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

          nil ->
            :error
        end

      _e ->
        :error
lain's avatar
lain committed
710
711
712
    end
  end

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

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

751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "EmojiReaction", "id" => reaction_activity_id},
          "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
773
774
775
776
777
778
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Block", "object" => blocked},
          "actor" => blocker,
          "id" => id
779
780
        } = _data,
        _options
normandy's avatar
normandy committed
781
      ) do
782
    with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
783
         {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
784
         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
normandy's avatar
normandy committed
785
      User.unblock(blocker, blocked)
normandy's avatar
normandy committed
786
787
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
788
      _e -> :error
normandy's avatar
normandy committed
789
790
791
    end
  end

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

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

826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
  # 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
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
  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

862
  def handle_incoming(_, _), do: :error
863

864
  @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
865
  def get_obj_helper(id, options \\ []) do
Maksim's avatar
Maksim committed
866
867
868
    case Object.normalize(id, true, options) do
      %Object{} = object -> {:ok, object}
      _ -> nil
869
    end
870
871
  end

872
  @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
Thibaut Girka's avatar
Thibaut Girka committed
873
  def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
874
875
        ap_id: ap_id
      })
Thibaut Girka's avatar
Thibaut Girka committed
876
      when attributed_to == ap_id do
877
878
879
880
881
    with {:ok, activity} <-
           handle_incoming(%{
             "type" => "Create",
             "to" => data["to"],
             "cc" => data["cc"],
Thibaut Girka's avatar
Thibaut Girka committed
882
             "actor" => attributed_to,
883
884
885
886
887
888
889
890
891
892
893
894
             "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

895
896
897
898
  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)
899
900
901
902
    else
      _e -> object
    end
  end
lain's avatar
lain committed
903

904
905
906
  def set_reply_to_uri(obj), do: obj

  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
907
908
  def prepare_object(object) do
    object
lain's avatar
lain committed
909
    |> set_sensitive
lain's avatar
lain committed
910
    |> add_hashtags
lain's avatar
lain committed
911
    |> add_mention_tags
lain's avatar
lain committed
912
    |> add_emoji_tags
lain's avatar
lain committed
913
    |> add_attributed_to
lain's avatar
lain committed
914
    |> prepare_attachments
lain's avatar
lain committed
915
    |> set_conversation
916
    |> set_reply_to_uri
917
918
    |> strip_internal_fields
    |> strip_internal_tags
919
    |> set_type
lain's avatar
lain committed
920
921
  end

feld's avatar
feld committed
922
923
924
925
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
926

927
928
  def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
      when activity_type in ["Create", "Listen"] do
lain's avatar
lain committed
929
    object =
minibikini's avatar
minibikini committed
930
931
932
      object_id
      |> Object.normalize()
      |> Map.get(:data)
lain's avatar
lain committed
933
934
935
936
937
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
938
      |> Map.merge(Utils.make_json_ld_header())
minibikini's avatar
minibikini committed
939
      |> Map.delete("bcc")
lain's avatar
lain committed
940
941
942
943

    {:ok, data}
  end

944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
  def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
    object =
      object_id
      |> Object.normalize()

    data =
      if Visibility.is_private?(object) && object.data["actor"] == ap_id do
        data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
      else
        data |> maybe_fix_object_url
      end

    data =
      data
      |> strip_internal_fields
      |> Map.merge(Utils.make_json_ld_header())
      |> Map.delete("bcc")

    {:ok, data}
  end

kaniini's avatar
kaniini committed
965
966
967
  # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  # because of course it does.
  def prepare_outgoing(%{"type" => "Accept"} = data) do
968
    with follow_activity <- Activity.normalize(data["object"]) do