transmogrifier.ex 27.4 KB
Newer Older
1
# Pleroma: A lightweight social networking server
Haelwenn's avatar
Haelwenn committed
2
# Copyright © 2017-2021 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.EctoType.ActivityPub.ObjectValidators
11
  alias Pleroma.Maps
Haelwenn's avatar
Haelwenn committed
12
  alias Pleroma.Object
rinpatch's avatar
rinpatch committed
13
  alias Pleroma.Object.Containment
Haelwenn's avatar
Haelwenn committed
14
  alias Pleroma.Repo
15
  alias Pleroma.User
Haelwenn's avatar
Haelwenn committed
16
  alias Pleroma.Web.ActivityPub.ActivityPub
17
  alias Pleroma.Web.ActivityPub.Builder
18
19
  alias Pleroma.Web.ActivityPub.ObjectValidator
  alias Pleroma.Web.ActivityPub.Pipeline
Haelwenn's avatar
Haelwenn committed
20
  alias Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
21
  alias Pleroma.Web.ActivityPub.Visibility
22
  alias Pleroma.Web.Federator
23
  alias Pleroma.Workers.TransmogrifierWorker
24

lain's avatar
lain committed
25
26
  import Ecto.Query

27
  require Logger
28
  require Pleroma.Constants
29

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

  def fix_summary(%{"summary" => nil} = object) do
50
    Map.put(object, "summary", "")
51
52
53
54
55
56
57
  end

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

58
  def fix_summary(object), do: Map.put(object, "summary", "")
59
60

  def fix_addressing_list(map, field) do
61
62
    addrs = map[field]

63
    cond do
64
65
      is_list(addrs) ->
        Map.put(map, field, Enum.filter(addrs, &is_binary/1))
66

67
68
      is_binary(addrs) ->
        Map.put(map, field, [addrs])
69
70

      true ->
71
        Map.put(map, field, [])
72
73
74
    end
  end

75
76
77
  # if directMessage flag is set to true, leave the addressing alone
  def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
    do: object
78

79
80
81
82
83
84
  def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collection) do
    explicit_mentions =
      Utils.determine_explicit_mentions(object) ++
        [Pleroma.Constants.as_public(), follower_collection]

    explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
85
    explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
86
87
88

    final_cc =
      (cc ++ explicit_cc)
89
      |> Enum.filter(& &1)
90
      |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
91
92
93
94
95
96
97
      |> Enum.uniq()

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

98
99
100
101
102
103
104
  # 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
105
        Pleroma.Constants.as_public() in cc ->
106
107
108
          to = to ++ [followers_collection]
          Map.put(object, "to", to)

109
        Pleroma.Constants.as_public() in to ->
110
111
112
113
114
115
          cc = cc ++ [followers_collection]
          Map.put(object, "cc", cc)

        true ->
          object
      end
116
    else
117
      object
118
119
120
    end
  end

121
  def fix_addressing(object) do
122
123
124
125
    {:ok, %User{follower_address: follower_collection}} =
      object
      |> Containment.get_actor()
      |> User.get_or_fetch_by_ap_id()
126

127
    object
128
129
130
131
    |> fix_addressing_list("to")
    |> fix_addressing_list("cc")
    |> fix_addressing_list("bto")
    |> fix_addressing_list("bcc")
132
133
    |> fix_explicit_addressing(follower_collection)
    |> fix_implicit_addressing(follower_collection)
lain's avatar
lain committed
134
135
  end

136
  def fix_actor(%{"attributedTo" => actor} = object) do
137
138
139
140
141
142
    actor = Containment.get_actor(%{"actor" => actor})

    # TODO: Remove actor field for Objects
    object
    |> Map.put("actor", actor)
    |> Map.put("attributedTo", actor)
143
144
  end

145
146
147
  def fix_in_reply_to(object, options \\ [])

  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
148
      when not is_nil(in_reply_to) do
149
    in_reply_to_id = prepare_in_reply_to(in_reply_to)
150
    depth = (options[:depth] || 0) + 1
lain's avatar
lain committed
151

152
    if Federator.allowed_thread_distance?(depth) do
153
      with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
minibikini's avatar
minibikini committed
154
           %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
155
156
157
        object
        |> Map.put("inReplyTo", replied_object.data["id"])
        |> Map.put("context", replied_object.data["context"] || object["conversation"])
158
        |> Map.drop(["conversation", "inReplyToAtomUri"])
159
      else
160
        e ->
161
          Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
162
163
164
165
          object
      end
    else
      object
lain's avatar
lain committed
166
167
    end
  end
lain's avatar
lain committed
168

169
  def fix_in_reply_to(object, _options), do: object
lain's avatar
lain committed
170

171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
  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
187
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
188
189
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

lain's avatar
lain committed
190
    object
Haelwenn's avatar
Haelwenn committed
191
    |> Map.put("context", context)
192
    |> Map.drop(["conversation"])
lain's avatar
lain committed
193
194
  end

195
  def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
lain's avatar
lain committed
196
    attachments =
197
      Enum.map(attachment, fn data ->
198
199
200
201
202
203
204
205
206
        url =
          cond do
            is_list(data["url"]) -> List.first(data["url"])
            is_map(data["url"]) -> data["url"]
            true -> nil
          end

        media_type =
          cond do
207
208
209
            is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
            MIME.valid?(data["mediaType"]) -> data["mediaType"]
            MIME.valid?(data["mimeType"]) -> data["mimeType"]
210
211
212
213
214
215
216
217
            true -> nil
          end

        href =
          cond do
            is_map(url) && is_binary(url["href"]) -> url["href"]
            is_binary(data["url"]) -> data["url"]
            is_binary(data["href"]) -> data["href"]
Haelwenn's avatar
Haelwenn committed
218
            true -> nil
219
220
          end

Haelwenn's avatar
Haelwenn committed
221
222
        if href do
          attachment_url =
Haelwenn's avatar
Haelwenn committed
223
224
225
226
            %{
              "href" => href,
              "type" => Map.get(url || %{}, "type", "Link")
            }
Haelwenn's avatar
Haelwenn committed
227
            |> Maps.put_if_present("mediaType", media_type)
228

Haelwenn's avatar
Haelwenn committed
229
230
231
232
          %{
            "url" => [attachment_url],
            "type" => data["type"] || "Document"
          }
Haelwenn's avatar
Haelwenn committed
233
234
          |> Maps.put_if_present("mediaType", media_type)
          |> Maps.put_if_present("name", data["name"])
235
          |> Maps.put_if_present("blurhash", data["blurhash"])
Haelwenn's avatar
Haelwenn committed
236
237
238
        else
          nil
        end
lain's avatar
lain committed
239
      end)
Haelwenn's avatar
Haelwenn committed
240
      |> Enum.filter(& &1)
lain's avatar
lain committed
241

242
    Map.put(object, "attachment", attachments)
243
244
  end

245
  def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
Maksim's avatar
Maksim committed
246
247
248
    object
    |> Map.put("attachment", [attachment])
    |> fix_attachments()
249
250
  end

251
  def fix_attachments(object), do: object
252

253
  def fix_url(%{"url" => url} = object) when is_map(url) do
254
    Map.put(object, "url", url["href"])
255
256
  end

Haelwenn's avatar
Haelwenn committed
257
  def fix_url(%{"url" => url} = object) when is_list(url) do
258
259
260
261
262
263
264
265
266
    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

267
    Map.put(object, "url", url_string)
268
269
270
271
  end

  def fix_url(object), do: object

272
  def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
lain's avatar
lain committed
273
    emoji =
274
      tags
275
      |> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
lain's avatar
lain committed
276
      |> Enum.reduce(%{}, fn data, mapping ->
277
        name = String.trim(data["name"], ":")
lain's avatar
lain committed
278

279
        Map.put(mapping, name, data["icon"]["url"])
lain's avatar
lain committed
280
      end)
lain's avatar
lain committed
281

282
    Map.put(object, "emoji", emoji)
lain's avatar
lain committed
283
284
  end

285
286
287
288
  def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
    name = String.trim(tag["name"], ":")
    emoji = %{name => tag["icon"]["url"]}

289
    Map.put(object, "emoji", emoji)
290
291
  end

292
  def fix_emoji(object), do: object
293

294
  def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
lain's avatar
lain committed
295
    tags =
296
      tag
lain's avatar
lain committed
297
      |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
298
299
300
      |> Enum.map(fn
        %{"name" => "#" <> hashtag} -> String.downcase(hashtag)
        %{"name" => hashtag} -> String.downcase(hashtag)
301
      end)
302

303
    Map.put(object, "tag", tag ++ tags)
304
305
  end

306
307
308
309
  def fix_tag(%{"tag" => %{} = tag} = object) do
    object
    |> Map.put("tag", [tag])
    |> fix_tag
310
311
  end

312
  def fix_tag(object), do: object
313

314
315
316
317
318
  # 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)

319
    Map.put(object, "content", content)
320
321
322
323
  end

  def fix_content_map(object), do: object

324
325
  def fix_type(object, options \\ [])

326
327
  def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
      when is_binary(reply_id) do
328
    with true <- Federator.allowed_thread_distance?(options[:depth]),
329
         {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
330
331
      Map.put(object, "type", "Answer")
    else
332
      _ -> object
333
334
335
    end
  end

336
  def fix_type(object, _), do: object
337

338
339
340
341
342
343
344
345
346
347
348
  # 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

349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
  # Compatibility wrapper for Mastodon votes
  defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do
    handle_incoming(data)
  end

  defp handle_create(%{"object" => object} = data, user) do
    %{
      to: data["to"],
      object: object,
      actor: user,
      context: object["context"],
      local: false,
      published: data["published"],
      additional:
        Map.take(data, [
          "cc",
          "directMessage",
          "id"
        ])
    }
    |> ActivityPub.create()
  end

372
373
  def handle_incoming(data, options \\ [])

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

396
  # disallow objects with bogus IDs
397
398
  def handle_incoming(%{"id" => nil}, _options), do: :error
  def handle_incoming(%{"id" => ""}, _options), do: :error
399
  # length of https:// = 8, should validate better, but good enough for now.
rinpatch's avatar
rinpatch committed
400
  def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
401
    do: :error
402

403
404
405
  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
406
407
408
409
  def handle_incoming(
        %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
        options
      )
Haelwenn's avatar
Haelwenn committed
410
      when objtype in ~w{Note Page} do
411
    actor = Containment.get_actor(data)
412

413
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
414
415
416
417
418
419
420
421
         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do
      data =
        data
        |> Map.put("object", fix_object(object, options))
        |> Map.put("actor", actor)
        |> fix_addressing()

      with {:ok, created_activity} <- handle_create(data, user) do
422
423
424
425
426
427
428
429
430
        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
431
432
433
434
        end

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

441
442
443
444
445
446
447
448
449
450
451
  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
452
453
      reply_depth = (options[:depth] || 0) + 1
      options = Keyword.put(options, :depth, reply_depth)
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
      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

472
473
474
475
476
477
478
479
480
481
  @misskey_reactions %{
    "like" => "👍",
    "love" => "❤️",
    "laugh" => "😆",
    "hmm" => "🤔",
    "surprise" => "😮",
    "congrats" => "🎉",
    "angry" => "💢",
    "confused" => "😥",
    "rip" => "😇",
482
483
    "pudding" => "🍮",
    "star" => "⭐"
484
485
  }

lain's avatar
lain committed
486
  @doc "Rewrite misskey likes into EmojiReacts"
487
488
489
490
491
492
493
494
  def handle_incoming(
        %{
          "type" => "Like",
          "_misskey_reaction" => reaction
        } = data,
        options
      ) do
    data
lain's avatar
lain committed
495
    |> Map.put("type", "EmojiReact")
496
    |> Map.put("content", @misskey_reactions[reaction] || reaction)
497
498
499
    |> handle_incoming(options)
  end

Haelwenn's avatar
Haelwenn committed
500
  def handle_incoming(
501
        %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
Haelwenn's avatar
Haelwenn committed
502
        _options
Haelwenn's avatar
Haelwenn committed
503
      )
Haelwenn's avatar
Haelwenn committed
504
      when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do
505
506
    data = Map.put(data, "object", strip_internal_fields(data["object"]))

Haelwenn's avatar
Haelwenn committed
507
    with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
508
         nil <- Activity.get_create_by_object_ap_id(obj_id),
Haelwenn's avatar
Haelwenn committed
509
510
         {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
      {:ok, activity}
511
512
513
    else
      %Activity{} = activity -> {:ok, activity}
      e -> e
Haelwenn's avatar
Haelwenn committed
514
515
516
    end
  end

517
  def handle_incoming(%{"type" => type} = data, _options)
518
      when type in ~w{Like EmojiReact Announce} do
lain's avatar
lain committed
519
520
521
    with :ok <- ObjectValidator.fetch_actor_and_object(data),
         {:ok, activity, _meta} <-
           Pipeline.common_pipeline(data, local: false) do
lain's avatar
lain committed
522
523
      {:ok, activity}
    else
524
      e -> {:error, e}
lain's avatar
lain committed
525
526
527
    end
  end

lain's avatar
lain committed
528
  def handle_incoming(
529
        %{"type" => type} = data,
530
        _options
531
      )
532
      when type in ~w{Update Block Follow Accept Reject} do
533
    with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
534
535
         {:ok, activity, _} <-
           Pipeline.common_pipeline(data, local: false) do
536
      {:ok, activity}
lain's avatar
lain committed
537
538
539
    end
  end

lain's avatar
lain committed
540
  def handle_incoming(
541
        %{"type" => "Delete"} = data,
542
        _options
lain's avatar
lain committed
543
      ) do
544
545
    with {:ok, activity, _} <-
           Pipeline.common_pipeline(data, local: false) do
lain's avatar
lain committed
546
      {:ok, activity}
547
    else
Haelwenn's avatar
Haelwenn committed
548
      {:error, {:validate, _}} = e ->
549
        # Check if we have a create activity for this
550
        with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
551
552
553
554
555
556
557
558
559
             %Activity{data: %{"actor" => actor}} <-
               Activity.create_by_object_ap_id(object_id) |> Repo.one(),
             # We have one, insert a tombstone and retry
             {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
             {:ok, _tombstone} <- Object.create(tombstone_data) do
          handle_incoming(data)
        else
          _ -> e
        end
lain's avatar
lain committed
560
561
562
    end
  end

normandy's avatar
normandy committed
563
564
565
566
567
568
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Follow", "object" => followed},
          "actor" => follower,
          "id" => id
569
570
        } = _data,
        _options
normandy's avatar
normandy committed
571
      ) do
normandy's avatar
normandy committed
572
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
573
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
normandy's avatar
normandy committed
574
         {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
normandy's avatar
normandy committed
575
576
577
      User.unfollow(follower, followed)
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
578
      _e -> :error
normandy's avatar
normandy committed
579
580
581
    end
  end

582
583
584
  def handle_incoming(
        %{
          "type" => "Undo",
585
          "object" => %{"type" => type}
586
587
        } = data,
        _options
588
      )
589
      when type in ["Like", "EmojiReact", "Announce", "Block"] do
590
    with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
591
592
593
594
      {:ok, activity}
    end
  end

595
  # For Undos that don't have the complete object attached, try to find it in our database.
normandy's avatar
normandy committed
596
597
598
  def handle_incoming(
        %{
          "type" => "Undo",
599
600
601
602
603
604
605
606
607
          "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)
normandy's avatar
normandy committed
608
    else
Maksim's avatar
Maksim committed
609
      _e -> :error
normandy's avatar
normandy committed
610
611
612
    end
  end

minibikini's avatar
minibikini committed
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
  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

631
  def handle_incoming(_, _), do: :error
632

633
  @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
634
  def get_obj_helper(id, options \\ []) do
635
636
637
    options = Keyword.put(options, :fetch, true)

    case Object.normalize(id, options) do
Maksim's avatar
Maksim committed
638
639
      %Object{} = object -> {:ok, object}
      _ -> nil
640
    end
641
642
  end

643
  @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
Thibaut Girka's avatar
Thibaut Girka committed
644
  def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
645
646
        ap_id: ap_id
      })
Thibaut Girka's avatar
Thibaut Girka committed
647
      when attributed_to == ap_id do
648
649
650
651
652
    with {:ok, activity} <-
           handle_incoming(%{
             "type" => "Create",
             "to" => data["to"],
             "cc" => data["cc"],
Thibaut Girka's avatar
Thibaut Girka committed
653
             "actor" => attributed_to,
654
655
             "object" => data
           }) do
656
      {:ok, Object.normalize(activity, fetch: false)}
657
658
659
660
661
662
663
664
665
    else
      _ -> get_obj_helper(object_id)
    end
  end

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

666
667
668
669
  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)
670
671
672
673
    else
      _e -> object
    end
  end
lain's avatar
lain committed
674

675
676
  def set_reply_to_uri(obj), do: obj

677
678
679
680
  @doc """
  Serialized Mastodon-compatible `replies` collection containing _self-replies_.
  Based on Mastodon's ActivityPub::NoteSerializer#replies.
  """
681
  def set_replies(obj_data) do
682
    replies_uris =
Ivan Tashkinov's avatar
Ivan Tashkinov committed
683
684
      with limit when limit > 0 <-
             Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
685
686
687
688
           %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
        object
        |> Object.self_replies()
        |> select([o], fragment("?->>'id'", o.data))
689
690
        |> limit(^limit)
        |> Repo.all()
Ivan Tashkinov's avatar
Ivan Tashkinov committed
691
692
      else
        _ -> []
693
694
      end

Ivan Tashkinov's avatar
Ivan Tashkinov committed
695
    set_replies(obj_data, replies_uris)
696
697
  end

Ivan Tashkinov's avatar
Ivan Tashkinov committed
698
  defp set_replies(obj, []) do
699
700
701
702
703
704
    obj
  end

  defp set_replies(obj, replies_uris) do
    replies_collection = %{
      "type" => "Collection",
705
      "items" => replies_uris
706
707
708
709
710
    }

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

Ivan Tashkinov's avatar
Ivan Tashkinov committed
711
712
713
  def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
    items
  end
714

Ivan Tashkinov's avatar
Ivan Tashkinov committed
715
716
  def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
    items
717
718
719
720
  end

  def replies(_), do: []

721
  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
722
723
  def prepare_object(object) do
    object
lain's avatar
lain committed
724
    |> add_hashtags
lain's avatar
lain committed
725
    |> add_mention_tags
lain's avatar
lain committed
726
    |> add_emoji_tags
lain's avatar
lain committed
727
    |> add_attributed_to
lain's avatar
lain committed
728
    |> prepare_attachments
lain's avatar
lain committed
729
    |> set_conversation
730
    |> set_reply_to_uri
731
    |> set_replies
732
733
    |> strip_internal_fields
    |> strip_internal_tags
734
    |> set_type
lain's avatar
lain committed
735
736
  end

feld's avatar
feld committed
737
738
739
740
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
741

742
743
  def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
      when activity_type in ["Create", "Listen"] do
lain's avatar
lain committed
744
    object =
minibikini's avatar
minibikini committed
745
      object_id
746
      |> Object.normalize(fetch: false)
minibikini's avatar
minibikini committed
747
      |> Map.get(:data)
lain's avatar
lain committed
748
749
750
751
752
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
753
      |> Map.merge(Utils.make_json_ld_header())
minibikini's avatar
minibikini committed
754
      |> Map.delete("bcc")
lain's avatar
lain committed
755
756
757
758

    {:ok, data}
  end

759
760
761
  def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
    object =
      object_id
762
      |> Object.normalize(fetch: false)
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779

    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
780
781
782
  # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  # because of course it does.
  def prepare_outgoing(%{"type" => "Accept"} = data) do
783
    with follow_activity <- Activity.normalize(data["object"]) do
kaniini's avatar
kaniini committed
784
785
786
787
788
789
790
791
792
793
      object = %{
        "actor" => follow_activity.actor,
        "object" => follow_activity.data["object"],
        "id" => follow_activity.data["id"],
        "type" => "Follow"
      }

      data =
        data
        |> Map.put("object", object)
lain's avatar
lain committed
794
        |> Map.merge(Utils.make_json_ld_header())
kaniini's avatar
kaniini committed
795
796
797
798
799

      {:ok, data}
    end
  end

800
  def prepare_outgoing(%{"type" => "Reject"} = data) do
801
    with follow_activity <- Activity.normalize(data["object"]) do
802
803
804
805
806
807
808
809
810
811
      object = %{
        "actor" => follow_activity.actor,
        "object" => follow_activity.data["object"],
        "id" => follow_activity.data["id"],
        "type" => "Follow"
      }

      data =
        data
        |> Map.put("object", object)
lain's avatar
lain committed
812
        |> Map.merge(Utils.make_json_ld_header())
813
814
815
816
817

      {:ok, data}
    end
  end

feld's avatar
feld committed
818
  def prepare_outgoing(%{"type" => _type} = data) do
lain's avatar
lain committed
819
820
    data =
      data
821
      |> strip_internal_fields
lain's avatar
lain committed
822
      |> maybe_fix_object_url
lain's avatar
lain committed
823
      |> Map.merge(Utils.make_json_ld_header())
824
825
826
827

    {:ok, data}
  end

828
829
830
831
832
833
  def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
    with false <- String.starts_with?(object, "http"),
         {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
         %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
           relative_object do
      Map.put(data, "object", external_url)
834
    else
835
836
837
838
839
840
      {:fetch, e} ->
        Logger.error("Couldn't fetch #{object} #{inspect(e)}")
        data

      _ ->
        data
841
842
843
    end
  end

844
845
  def maybe_fix_object_url(data), do: data

lain's avatar
lain committed
846
  def add_hashtags(object) do
lain's avatar
lain committed
847
848
    tags =
      (object["tag"] || [])
849
850
851
852
853
854
855
856
857
858
859
860
      |> Enum.map(fn
        # Expand internal representation tags into AS2 tags.
        tag when is_binary(tag) ->
          %{
            "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
            "name" => "##{tag}",
            "type" => "Hashtag"
          }

        # Do not process tags which are already AS2 tag objects.
        tag when is_map(tag) ->
          tag
lain's avatar
lain committed
861
      end)
lain's avatar
lain committed
862

863
    Map.put(object, "tag", tags)
lain's avatar
lain committed
864
865
  end

866
867
  # TODO These should be added on our side on insertion, it doesn't make much
  # sense to regenerate these all the time
lain's avatar
lain committed
868
  def add_mention_tags(object) do
869
870
871
872
873
    to = object["to"] || []
    cc = object["cc"] || []
    mentioned = User.get_users_from_set(to ++ cc, local_only: false)

    mentions = Enum.map(mentioned, &build_mention_tag/1)
lain's avatar
lain committed
874

lain's avatar
lain committed
875
    tags = object["tag"] || []
876
    Map.put(object, "tag", tags ++ mentions)
lain's avatar
lain committed
877
878
  end

Maksim's avatar
Maksim committed
879
880
881
  defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
    %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
  end
Haelwenn's avatar
Haelwenn committed
882

883
  def take_emoji_tags(%User{emoji: emoji}) do
Maksim's avatar
Maksim committed
884
    emoji
885
    |> Map.to_list()
Maksim's avatar
Maksim committed
886
    |> Enum.map(&build_emoji_tag/1)
Haelwenn's avatar
Haelwenn committed
887
888
  end

lain's avatar
lain committed
889
  # TODO: we should probably send mtime instead of unix epoch time for updated
Haelwenn's avatar
Haelwenn committed
890
  def add_emoji_tags(%{"emoji" => emoji} = object) do
lain's avatar
lain committed
891
    tags = object["tag"] || []
lain's avatar
lain committed
892

Maksim's avatar
Maksim committed
893
    out = Enum.map(emoji, &build_emoji_tag/1)
lain's avatar
lain committed
894

895
    Map.put(object, "tag", tags ++ out)
lain's avatar
lain committed
896
897
  end

898
  def add_emoji_tags(object), do: object
Haelwenn's avatar
Haelwenn committed
899

Maksim's avatar
Maksim committed
900
901
  defp build_emoji_tag({name, url}) do
    %{
feld's avatar
feld committed
902
      "icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
Maksim's avatar
Maksim committed
903
904
905
906
907
908
909
      "name" => ":" <> name <> ":",
      "type" => "Emoji",
      "updated" => "1970-01-01T00:00:00Z",
      "id" => url
    }
  end

lain's avatar
lain committed
910
911
912
913
  def set_conversation(object) do
    Map.put(object, "conversation", object["context"])
  end

914
915
916
917
918
919
  def set_type(%{"type" => "Answer"} = object) do
    Map.put(object, "type", "Note")
  end

  def set_type(object), do: object

lain's avatar
lain committed
920
  def add_attributed_to(object) do
921
    attributed_to = object["attributedTo"] || object["actor"]
922
    Map.put(object, "attributedTo", attributed_to)
923
  end
lain's avatar
lain committed
924

925
926
927
  # TODO: Revisit this
  def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object

lain's avatar
lain committed
928
  def prepare_attachments(object) do
lain's avatar
lain committed
929
    attachments =
Maksim's avatar
Maksim committed
930
931
      object
      |> Map.get("attachment", [])
lain's avatar
lain committed
932
933
      |> Enum.map(fn data ->
        [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
Maksim's avatar
Maksim committed
934
935
936
937
938
939
940

        %{
          "url" => href,
          "mediaType" => media_type,
          "name" => data["name"],
          "type" => "Document"
        }
lain's avatar
lain committed
941
      end)
lain's avatar
lain committed
942

943
    Map.put(object, "attachment", attachments)
lain's avatar
lain committed
944
  end
lain's avatar
lain committed
945

946
  def strip_internal_fields(object) do
Maksim's avatar
Maksim committed
947
    Map.drop(object, Pleroma.Constants.object_internal_fields())
948
949
950
  end

  defp strip_internal_tags(%{"tag" => tags} = object) do
951
    tags = Enum.filter(tags, fn x -> is_map(x) end)
952

953
    Map.put(object, "tag", tags)
954
955
956
957
  end

  defp strip_internal_tags(object), do: object

958
  def perform(:user_upgrade, user) do
959
960
    # we pass a fake user so that the followers collection is stripped away
    old_follower_address = User.ap_followers(%User{nickname: user.nickname})
lain's avatar
lain committed
961

962
963
964
965
966
967
968
969
970
971
972
973
    from(
      a in Activity,
      where: ^old_follower_address in a.recipients,
      update: [
        set: [
          recipients:
            fragment(
              "array_replace(?,?,?)",
              a.recipients,
              ^old_follower_address,
              ^user.follower_address
            )
lain's avatar
lain committed
974
        ]
975
976
977
      ]
    )
    |> Repo.update_all([])
lain's avatar
lain committed
978
979
  end

980
  def upgrade_user_from_ap_id(ap_id) do
minibikini's avatar
minibikini committed
981