transmogrifier.ex 33.9 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.TransmogrifierWorker
19
20
  alias Pleroma.Web.ActivityPub.ObjectValidator
  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
21

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

24
  require Logger
25
  require Pleroma.Constants
26

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
  end

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

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

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

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

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

      true ->
        map
67
68
69
    end
  end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      true ->
        ""
    end
  end

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

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

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

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

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

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

226
  def fix_attachments(object), do: object
227

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

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

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

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

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

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

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

  def fix_url(object), do: object

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

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

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

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

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

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

281
  def fix_emoji(object), do: object
282

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

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

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

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

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

300
  def fix_tag(object), do: object
301

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

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

  def fix_content_map(object), do: object

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

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

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

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

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

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

      _ ->
        {:error, nil}
    end
  end

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

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

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

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

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

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

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

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

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

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
464
  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
465
  def handle_incoming(
466
467
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
        _options
lain's avatar
lain committed
468
      ) do
469
470
471
472
    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})),
473
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
474
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
475
           {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
476
477
           {_, false} <- {:user_locked, User.locked?(followed)},
           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
478
479
           {_, {:ok, _}} <-
             {:follow_state_update, Utils.update_follow_state_for_all(activity, "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
490
491
492
493
494
495
496
497

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

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

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

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

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

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

  def handle_incoming(
542
        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
543
        _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
             object: follow_activity.data["id"],
556
557
             local: false,
             activity_id: id
lain's avatar
lain committed
558
           }) do
559
560
      User.unfollow(follower, followed)

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

567
568
569
570
571
572
573
  def handle_incoming(%{"type" => "Like"} = data, _options) do
    with {_, %{changes: cast_data}} <- {:casting_data, LikeValidator.cast_data(data)},
         cast_data <- ObjectValidator.stringify_keys(cast_data),
         :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
         {_, {:ok, cast_data}} <- {:maybe_add_context, maybe_add_context_from_object(cast_data)},
         {_, {:ok, cast_data}} <-
           {:maybe_add_recipients, maybe_add_recipients_from_object(cast_data)},
574
         {_, {:ok, activity, _meta}} <-
575
           {:common_pipeline, ActivityPub.common_pipeline(cast_data, local: false)} do
lain's avatar
lain committed
576
577
      {:ok, activity}
    else
578
      e -> {:error, e}
lain's avatar
lain committed
579
580
581
    end
  end

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

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

rinpatch's avatar
rinpatch committed
606
607
      banner = new_user_data[:info][:banner]
      locked = new_user_data[:info][:locked] || false
608
609
610
611
612
613
      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
614
615
616
617

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

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

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

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

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

          nil ->
            :error
        end

      _e ->
        :error
lain's avatar
lain committed
669
670
671
    end
  end

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

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

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

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

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

763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
  # 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

781
  def handle_incoming(_, _), do: :error
782

783
  @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
784
  def get_obj_helper(id, options \\ []) do
Maksim's avatar
Maksim committed
785
786
787
    case Object.normalize(id, true, options) do
      %Object{} = object -> {:ok, object}
      _ -> nil
788
    end
789
790
  end

791
  @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
Thibaut Girka's avatar
Thibaut Girka committed
792
  def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
793
794
        ap_id: ap_id
      })
Thibaut Girka's avatar
Thibaut Girka committed
795
      when attributed_to == ap_id do
796
797
798
799
800
    with {:ok, activity} <-
           handle_incoming(%{
             "type" => "Create",
             "to" => data["to"],
             "cc" => data["cc"],
Thibaut Girka's avatar
Thibaut Girka committed
801
             "actor" => attributed_to,
802
803
804
805
806
807
808
809
810
811
812
813
             "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

814
815
816
817
  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)
818
819
820
821
    else
      _e -> object
    end
  end
lain's avatar
lain committed
822

823
824
825
  def set_reply_to_uri(obj), do: obj

  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
826
827
  def prepare_object(object) do
    object
lain's avatar
lain committed
828
    |> set_sensitive
lain's avatar
lain committed
829
    |> add_hashtags
lain's avatar
lain committed
830
    |> add_mention_tags
lain's avatar
lain committed
831
    |> add_emoji_tags
lain's avatar
lain committed
832
    |> add_attributed_to
lain's avatar
lain committed
833
    |> prepare_attachments
lain's avatar
lain committed
834
    |> set_conversation
835
    |> set_reply_to_uri
836
837
    |> strip_internal_fields
    |> strip_internal_tags
838
    |> set_type
lain's avatar
lain committed
839
840
  end

feld's avatar
feld committed
841
842
843
844
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
845

846
847
  def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
      when activity_type in ["Create", "Listen"] do
lain's avatar
lain committed
848
    object =
minibikini's avatar
minibikini committed
849
850
851
      object_id
      |> Object.normalize()
      |> Map.get(:data)
lain's avatar
lain committed
852
853
854
855
856
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
857
      |> Map.merge(Utils.make_json_ld_header())
minibikini's avatar
minibikini committed
858
      |> Map.delete("bcc")
lain's avatar
lain committed
859
860
861
862

    {:ok, data}
  end

863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
  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
884
885
886
  # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  # because of course it does.
  def prepare_outgoing(%{"type" => "Accept"} = data) do
887
    with follow_activity <- Activity.normalize(data["object"]) do
kaniini's avatar
kaniini committed
888
889
890
891
892
893
894
895
896
897
      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
898
        |> Map.merge(Utils.make_json_ld_header())
kaniini's avatar
kaniini committed
899
900
901
902
903

      {:ok, data}
    end
  end

904
  def prepare_outgoing(%{"type" => "Reject"} = data) do
905
    with follow_activity <- Activity.normalize(data["object"]) do
906
907
908
909
910
911
912
913
914
915
      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
916
        |> Map.merge(Utils.make_json_ld_header())
917
918
919
920
921

      {:ok, data}
    end
  end

feld's avatar
feld committed
922
  def prepare_outgoing(%{"type" => _type} = data) do
lain's avatar
lain committed
923
924
    data =
      data
925
      |> strip_internal_fields
lain's avatar
lain committed
926
      |> maybe_fix_object_url
lain's avatar
lain committed
927
      |> Map.merge(Utils.make_json_ld_header())
928
929
930
931

    {:ok, data}
  end

932
933
934
935
936
937
  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)
938
    else
939
940
941
942
943
944
      {:fetch, e} ->
        Logger.error("Couldn't fetch #{object} #{inspect(e)}")
        data

      _ ->
        data
945
946
947
    end
  end

948
949
  def maybe_fix_object_url(data), do: data