transmogrifier.ex 30.4 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
10
  alias Pleroma.Activity
  alias Pleroma.Object
rinpatch's avatar
rinpatch committed
11
  alias Pleroma.Object.Containment
Haelwenn's avatar
Haelwenn committed
12
  alias Pleroma.Repo
13
  alias Pleroma.User
Haelwenn's avatar
Haelwenn committed
14
15
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
16
  alias Pleroma.Web.ActivityPub.Visibility
17
  alias Pleroma.Web.Federator
18
  alias Pleroma.Workers.Transmogrifier, as: TransmogrifierWorker
19

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

22
  require Logger
23
  require Pleroma.Constants
24

25
26
  defdelegate worker_args(queue), to: Pleroma.Workers.Helper

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

  def fix_summary(%{"summary" => nil} = object) do
    object
    |> Map.put("summary", "")
  end

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

  def fix_summary(object) do
    object
    |> Map.put("summary", "")
59
60
61
  end

  def fix_addressing_list(map, field) do
62
63
64
65
66
67
68
69
70
    cond do
      is_binary(map[field]) ->
        Map.put(map, field, [map[field]])

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

      true ->
        map
71
72
73
    end
  end

74
75
76
77
78
  def fix_explicit_addressing(
        %{"to" => to, "cc" => cc} = object,
        explicit_mentions,
        follower_collection
      ) do
79
80
81
82
83
84
85
86
87
88
    explicit_to =
      to
      |> Enum.filter(fn x -> x in explicit_mentions end)

    explicit_cc =
      to
      |> Enum.filter(fn x -> x not in explicit_mentions end)

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

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

97
  def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
98

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

102
  def fix_explicit_addressing(object) do
103
104
105
106
    explicit_mentions =
      object
      |> Utils.determine_explicit_mentions()

107
108
    follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address

109
    explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
110

111
    fix_explicit_addressing(object, explicit_mentions, follower_collection)
lain's avatar
lain committed
112
113
  end

114
115
116
117
118
119
120
  # 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
121
        Pleroma.Constants.as_public() in cc ->
122
123
124
          to = to ++ [followers_collection]
          Map.put(object, "to", to)

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

        true ->
          object
      end
132
    else
133
      object
134
135
136
    end
  end

137
138
  def fix_implicit_addressing(object, _), do: object

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

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

152
153
  def fix_actor(%{"attributedTo" => actor} = object) do
    object
154
    |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
155
156
  end

157
158
159
  def fix_in_reply_to(object, options \\ [])

  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
160
161
      when not is_nil(in_reply_to) do
    in_reply_to_id =
162
      cond do
163
164
165
166
167
168
169
170
171
        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)

172
        # Maybe I should output an error too?
173
174
        true ->
          ""
175
176
      end

177
    object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
lain's avatar
lain committed
178

179
    if Federator.allowed_incoming_reply_depth?(options[:depth]) do
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
      case get_obj_helper(in_reply_to_id, options) do
        {:ok, replied_object} ->
          with %Activity{} = _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
            e ->
              Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
              object
          end

        e ->
          Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
          object
      end
    else
      object
lain's avatar
lain committed
201
202
    end
  end
lain's avatar
lain committed
203

204
  def fix_in_reply_to(object, _options), do: object
lain's avatar
lain committed
205

lain's avatar
lain committed
206
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
207
208
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

lain's avatar
lain committed
209
    object
Haelwenn's avatar
Haelwenn committed
210
211
    |> Map.put("context", context)
    |> Map.put("conversation", context)
lain's avatar
lain committed
212
213
  end

214
  def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
lain's avatar
lain committed
215
    attachments =
216
      attachment
lain's avatar
lain committed
217
      |> Enum.map(fn data ->
218
219
220
221
222
223
224
225
        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
226
      end)
lain's avatar
lain committed
227
228
229

    object
    |> Map.put("attachment", attachments)
230
231
  end

232
  def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
233
234
    Map.put(object, "attachment", [attachment])
    |> fix_attachments()
235
236
  end

237
  def fix_attachments(object), do: object
238

239
240
241
242
243
  def fix_url(%{"url" => url} = object) when is_map(url) do
    object
    |> Map.put("url", url["href"])
  end

244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
  def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
    first_element = Enum.at(url, 0)

    link_element =
      url
      |> Enum.filter(fn x -> is_map(x) end)
      |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
      |> Enum.at(0)

    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
260
261
262
263
264
265
266
267
268
    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

269
270
    object
    |> Map.put("url", url_string)
271
272
273
274
  end

  def fix_url(object), do: object

275
  def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
lain's avatar
lain committed
276
277
278
279
280
    emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)

    emoji =
      emoji
      |> Enum.reduce(%{}, fn data, mapping ->
281
        name = String.trim(data["name"], ":")
lain's avatar
lain committed
282

lain's avatar
lain committed
283
284
        mapping |> Map.put(name, data["icon"]["url"])
      end)
lain's avatar
lain committed
285
286
287
288
289
290
291
292

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

    object
    |> Map.put("emoji", emoji)
  end

293
294
295
296
297
298
299
300
  def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
    name = String.trim(tag["name"], ":")
    emoji = %{name => tag["icon"]["url"]}

    object
    |> Map.put("emoji", emoji)
  end

301
  def fix_emoji(object), do: object
302

303
  def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
lain's avatar
lain committed
304
    tags =
305
      tag
lain's avatar
lain committed
306
307
      |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
      |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
308

309
    combined = tag ++ tags
310
311
312
313
314

    object
    |> Map.put("tag", combined)
  end

315
316
  def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
    combined = [tag, String.slice(hashtag, 1..-1)]
317
318
319
320
321

    object
    |> Map.put("tag", combined)
  end

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

324
  def fix_tag(object), do: object
325

326
327
328
329
330
331
332
333
334
335
336
  # 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)

    object
    |> Map.put("content", content)
  end

  def fix_content_map(object), do: object

337
338
  def fix_type(object, options \\ [])

339
340
  def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
      when is_binary(reply_id) do
341
    reply =
342
343
344
      with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
           {:ok, object} <- get_obj_helper(reply_id, options) do
        object
345
      end
346

347
    if reply && reply.data["type"] == "Question" do
348
349
350
351
352
353
      Map.put(object, "type", "Answer")
    else
      object
    end
  end

354
  def fix_type(object, _), do: object
355

Maksim's avatar
Maksim committed
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
  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

382
383
  def handle_incoming(data, options \\ [])

384
385
  # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
  # with nil ID.
386
  def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
    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.
         %User{} = account <-
           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),

         # Remove the reported user from the object list.
         statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
      params = %{
        actor: actor,
        context: context,
        account: account,
        statuses: statuses,
        content: content,
        additional: %{
          "cc" => [account.ap_id]
        }
      }

      ActivityPub.flag(params)
    end
  end

418
  # disallow objects with bogus IDs
419
420
  def handle_incoming(%{"id" => nil}, _options), do: :error
  def handle_incoming(%{"id" => ""}, _options), do: :error
421
  # length of https:// = 8, should validate better, but good enough for now.
422
423
  def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
    do: :error
424

425
426
427
  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
428
429
430
431
  def handle_incoming(
        %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
        options
      )
432
      when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
433
    actor = Containment.get_actor(data)
434
435
436
437

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

439
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
440
         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
441
442
      options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
      object = fix_object(data["object"], options)
443

444
445
446
447
      params = %{
        to: data["to"],
        object: object,
        actor: user,
lain's avatar
lain committed
448
        context: object["conversation"],
449
450
        local: false,
        published: data["published"],
lain's avatar
lain committed
451
452
453
        additional:
          Map.take(data, [
            "cc",
454
            "directMessage",
lain's avatar
lain committed
455
456
            "id"
          ])
457
458
459
460
      }

      ActivityPub.create(params)
    else
lain's avatar
lain committed
461
      %Activity{} = activity -> {:ok, activity}
462
463
464
465
      _e -> :error
    end
  end

lain's avatar
lain committed
466
  def handle_incoming(
467
468
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
        _options
lain's avatar
lain committed
469
      ) do
470
471
472
473
    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})),
474
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
475
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
476
           {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
477
478
           {_, false} <- {:user_locked, User.locked?(followed)},
           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
479
480
           {_, {:ok, _}} <-
             {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
kaniini's avatar
kaniini committed
481
482
        ActivityPub.accept(%{
          to: [follower.ap_id],
483
          actor: followed,
kaniini's avatar
kaniini committed
484
485
486
          object: data,
          local: true
        })
487
488
      else
        {:user_blocked, true} ->
489
          {:ok, _} = Utils.update_follow_state_for_all(activity, "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
501
502
503
504
505
506
507
508
509

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

        {:user_locked, true} ->
          :noop
510
      end
lain's avatar
lain committed
511

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

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

  def handle_incoming(
542
543
        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
        _options
544
      ) do
545
    with actor <- Containment.get_actor(data),
546
         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
547
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
548
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
549
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
lain's avatar
lain committed
550
         {:ok, activity} <-
551
           ActivityPub.reject(%{
lain's avatar
lain committed
552
             to: follow_activity.data["to"],
553
             type: "Reject",
554
             actor: followed,
lain's avatar
lain committed
555
556
557
             object: follow_activity.data["id"],
             local: false
           }) do
558
559
      User.unfollow(follower, followed)

560
      {:ok, activity}
561
562
    else
      _e -> :error
563
564
565
    end
  end

lain's avatar
lain committed
566
  def handle_incoming(
567
568
        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
569
      ) do
570
    with actor <- Containment.get_actor(data),
571
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
572
         {:ok, object} <- get_obj_helper(object_id),
feld's avatar
feld committed
573
         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
lain's avatar
lain committed
574
575
576
577
578
579
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
580
  def handle_incoming(
581
582
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
583
      ) do
584
    with actor <- Containment.get_actor(data),
585
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
586
         {:ok, object} <- get_obj_helper(object_id),
lain's avatar
lain committed
587
         public <- Visibility.is_public?(data),
588
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
lain's avatar
lain committed
589
590
591
592
593
594
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
595
  def handle_incoming(
596
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
597
598
          data,
        _options
599
600
      )
      when object_type in ["Person", "Application", "Service", "Organization"] do
minibikini's avatar
minibikini committed
601
    with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
lain's avatar
lain committed
602
603
      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)

rinpatch's avatar
rinpatch committed
604
605
      banner = new_user_data[:info][:banner]
      locked = new_user_data[:info][:locked] || false
606
607
608
609
610
611
      attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []

      fields =
        attachment
        |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
        |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
lain's avatar
lain committed
612
613
614
615

      update_data =
        new_user_data
        |> Map.take([:name, :bio, :avatar])
616
        |> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
lain's avatar
lain committed
617
618

      actor
619
      |> User.upgrade_changeset(update_data, true)
lain's avatar
lain committed
620
      |> User.update_and_set_cache()
lain's avatar
lain committed
621

lain's avatar
lain committed
622
623
624
625
626
627
628
      ActivityPub.update(%{
        local: false,
        to: data["to"] || [],
        cc: data["cc"] || [],
        object: object,
        actor: actor_id
      })
lain's avatar
lain committed
629
630
631
632
633
634
635
    else
      e ->
        Logger.error(e)
        :error
    end
  end

636
637
638
639
640
  # 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
641
  def handle_incoming(
642
        %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
643
        _options
lain's avatar
lain committed
644
      ) do
lain's avatar
lain committed
645
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
646

647
    with actor <- Containment.get_actor(data),
648
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
649
         {:ok, object} <- get_obj_helper(object_id),
650
         :ok <- Containment.contain_origin(actor.ap_id, object.data),
lain's avatar
lain committed
651
652
653
         {:ok, activity} <- ActivityPub.delete(object, false) do
      {:ok, activity}
    else
654
655
656
      nil ->
        case User.get_cached_by_ap_id(object_id) do
          %User{ap_id: ^actor} = user ->
657
            User.delete(user)
658
659
660
661
662
663
664

          nil ->
            :error
        end

      _e ->
        :error
lain's avatar
lain committed
665
666
667
    end
  end

668
  def handle_incoming(
669
670
        %{
          "type" => "Undo",
671
          "object" => %{"type" => "Announce", "object" => object_id},
Maksim's avatar
Maksim committed
672
          "actor" => _actor,
673
          "id" => id
674
675
        } = data,
        _options
676
      ) do
677
    with actor <- Containment.get_actor(data),
678
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
679
         {:ok, object} <- get_obj_helper(object_id),
680
         {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
681
682
      {:ok, activity}
    else
Thog's avatar
Thog committed
683
      _e -> :error
684
685
686
    end
  end

normandy's avatar
normandy committed
687
688
689
690
691
692
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Follow", "object" => followed},
          "actor" => follower,
          "id" => id
693
694
        } = _data,
        _options
normandy's avatar
normandy committed
695
      ) do
normandy's avatar
normandy committed
696
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
697
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
normandy's avatar
normandy committed
698
         {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
normandy's avatar
normandy committed
699
700
701
      User.unfollow(follower, followed)
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
702
      _e -> :error
normandy's avatar
normandy committed
703
704
705
    end
  end

normandy's avatar
normandy committed
706
707
708
709
710
711
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Block", "object" => blocked},
          "actor" => blocker,
          "id" => id
712
713
        } = _data,
        _options
normandy's avatar
normandy committed
714
      ) do
715
    with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
716
         {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
717
         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
normandy's avatar
normandy committed
718
      User.unblock(blocker, blocked)
normandy's avatar
normandy committed
719
720
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
721
      _e -> :error
normandy's avatar
normandy committed
722
723
724
    end
  end

725
  def handle_incoming(
726
727
        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
        _options
728
      ) do
729
    with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
0x1C3B00DA's avatar
0x1C3B00DA committed
730
         {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
731
         {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
732
      User.unfollow(blocker, blocked)
733
      User.block(blocker, blocked)
normandy's avatar
normandy committed
734
735
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
736
      _e -> :error
normandy's avatar
normandy committed
737
738
    end
  end
739

Thog's avatar
Thog committed
740
741
742
743
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Like", "object" => object_id},
Maksim's avatar
Maksim committed
744
          "actor" => _actor,
Thog's avatar
Thog committed
745
          "id" => id
746
747
        } = data,
        _options
Thog's avatar
Thog committed
748
      ) do
749
    with actor <- Containment.get_actor(data),
750
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
751
         {:ok, object} <- get_obj_helper(object_id),
Thog's avatar
Thog committed
752
753
754
         {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
      {:ok, activity}
    else
Thog's avatar
Thog committed
755
      _e -> :error
Thog's avatar
Thog committed
756
757
758
    end
  end

759
  def handle_incoming(_, _), do: :error
760

761
762
  def get_obj_helper(id, options \\ []) do
    if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
763
764
  end

765
766
767
768
  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)
769
770
771
772
    else
      _e -> object
    end
  end
lain's avatar
lain committed
773

774
775
776
  def set_reply_to_uri(obj), do: obj

  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
777
778
  def prepare_object(object) do
    object
lain's avatar
lain committed
779
    |> set_sensitive
lain's avatar
lain committed
780
    |> add_hashtags
lain's avatar
lain committed
781
    |> add_mention_tags
lain's avatar
lain committed
782
    |> add_emoji_tags
lain's avatar
lain committed
783
    |> add_attributed_to
lain's avatar
lain committed
784
    |> prepare_attachments
lain's avatar
lain committed
785
    |> set_conversation
786
    |> set_reply_to_uri
787
788
    |> strip_internal_fields
    |> strip_internal_tags
789
    |> set_type
lain's avatar
lain committed
790
791
  end

feld's avatar
feld committed
792
793
794
795
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
796

797
  def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
lain's avatar
lain committed
798
    object =
minibikini's avatar
minibikini committed
799
800
801
      object_id
      |> Object.normalize()
      |> Map.get(:data)
lain's avatar
lain committed
802
803
804
805
806
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
807
      |> Map.merge(Utils.make_json_ld_header())
minibikini's avatar
minibikini committed
808
      |> Map.delete("bcc")
lain's avatar
lain committed
809
810
811
812

    {:ok, data}
  end

kaniini's avatar
kaniini committed
813
814
815
  # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  # because of course it does.
  def prepare_outgoing(%{"type" => "Accept"} = data) do
816
    with follow_activity <- Activity.normalize(data["object"]) do
kaniini's avatar
kaniini committed
817
818
819
820
821
822
823
824
825
826
      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
827
        |> Map.merge(Utils.make_json_ld_header())
kaniini's avatar
kaniini committed
828
829
830
831
832

      {:ok, data}
    end
  end

833
  def prepare_outgoing(%{"type" => "Reject"} = data) do
834
    with follow_activity <- Activity.normalize(data["object"]) do
835
836
837
838
839
840
841
842
843
844
      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
845
        |> Map.merge(Utils.make_json_ld_header())
846
847
848
849
850

      {:ok, data}
    end
  end

feld's avatar
feld committed
851
  def prepare_outgoing(%{"type" => _type} = data) do
lain's avatar
lain committed
852
853
    data =
      data
854
      |> strip_internal_fields
lain's avatar
lain committed
855
      |> maybe_fix_object_url
lain's avatar
lain committed
856
      |> Map.merge(Utils.make_json_ld_header())
857
858
859
860

    {:ok, data}
  end

861
862
  def maybe_fix_object_url(data) do
    if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
863
      case get_obj_helper(data["object"]) do
864
865
        {:ok, relative_object} ->
          if relative_object.data["external_url"] do
feld's avatar
feld committed
866
            _data =
lain's avatar
lain committed
867
868
              data
              |> Map.put("object", relative_object.data["external_url"])
869
870
871
          else
            data
          end
lain's avatar
lain committed
872

873
874
875
876
877
878
879
880
881
        e ->
          Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
          data
      end
    else
      data
    end
  end

lain's avatar
lain committed
882
  def add_hashtags(object) do
lain's avatar
lain committed
883
884
    tags =
      (object["tag"] || [])
885
886
887
888
889
890
891
892
893
894
895
896
      |> 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
897
      end)
lain's avatar
lain committed
898
899
900
901
902

    object
    |> Map.put("tag", tags)
  end

lain's avatar
lain committed
903
  def add_mention_tags(object) do
lain's avatar
lain committed
904
    mentions =
905
906
      object
      |> Utils.get_notified_from_object()
lain's avatar
lain committed
907
908
909
      |> Enum.map(fn user ->
        %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
      end)
lain's avatar
lain committed
910

lain's avatar
lain committed
911
    tags = object["tag"] || []
lain's avatar
lain committed
912
913

    object
lain's avatar
lain committed
914
    |> Map.put("tag", tags ++ mentions)
lain's avatar
lain committed
915
916
  end

Haelwenn's avatar
Haelwenn committed
917
918
919
920
921
922
923
  def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
    user_info = add_emoji_tags(user_info)

    object
    |> Map.put(:info, user_info)
  end

lain's avatar
lain committed
924
  # TODO: we should probably send mtime instead of unix epoch time for updated
Haelwenn's avatar
Haelwenn committed
925
  def add_emoji_tags(%{"emoji" => emoji} = object) do
lain's avatar
lain committed
926
    tags = object["tag"] || []
lain's avatar
lain committed
927
928
929
930
931
932
933
934
935
936
937
938

    out =
      emoji
      |> Enum.map(fn {name, url} ->
        %{
          "icon" => %{"url" => url, "type" => "Image"},
          "name" => ":" <> name <> ":",
          "type" => "Emoji",
          "updated" => "1970-01-01T00:00:00Z",
          "id" => url
        }
      end)
lain's avatar
lain committed
939
940
941
942
943

    object
    |> Map.put("tag", tags ++ out)
  end

Haelwenn's avatar
Haelwenn committed
944
945
946
947
  def add_emoji_tags(object) do
    object
  end

lain's avatar
lain committed
948
949
950
951
  def set_conversation(object) do
    Map.put(object, "conversation", object["context"])
  end

lain's avatar
lain committed
952
953
954
955
956
  def set_sensitive(object) do
    tags = object["tag"] || []
    Map.put(object, "sensitive", "nsfw" in tags)
  end

957
958
959
960
961
962
  def set_type(%{"type" => "Answer"} = object) do
    Map.put(object, "type", "Note")
  end

  def set_type(object), do: object

lain's avatar
lain committed
963
  def add_attributed_to(object) do
964
    attributed_to = object["attributedTo"] || object["actor"]
lain's avatar
lain committed
965
966

    object
967
    |> Map.put("attributedTo", attributed_to)
968
  end
lain's avatar
lain committed
969
970

  def prepare_attachments(object) do
lain's avatar
lain committed
971
972
973
974
975
976
    attachments =
      (object["attachment"] || [])
      |> Enum.map(fn data ->
        [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
        %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
      end)
lain's avatar
lain committed
977
978
979
980

    object
    |> Map.put("attachment", attachments)
  end
lain's avatar
lain committed
981

982
983
984
  defp strip_internal_fields(object) do
    object
    |> Map.drop([
985
      "likes",
986
987
988
989
      "like_count",
      "announcements",
      "announcement_count",
      "emoji",
990
991
      "context_id",
      "deleted_activity_id"
992
993
994
995
996
997
998
999
1000
    ])
  end

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

    object
For faster browsing, not all history is shown. View entire blame