transmogrifier.ex 29.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

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

21
  require Logger
22
  require Pleroma.Constants
23

24
25
26
  @doc """
  Modifies an incoming AP object (mastodon format) to our internal format.
  """
27
  def fix_object(object, options \\ []) do
28
    object
29
    |> strip_internal_fields
30
    |> fix_actor
31
    |> fix_url
32
    |> fix_attachments
lain's avatar
lain committed
33
    |> fix_context
34
    |> fix_in_reply_to(options)
lain's avatar
lain committed
35
    |> fix_emoji
36
    |> fix_tag
37
    |> fix_content_map
38
    |> fix_addressing
39
    |> fix_summary
40
    |> fix_type(options)
41
42
43
  end

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

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

52
  def fix_summary(object), do: Map.put(object, "summary", "")
53
54

  def fix_addressing_list(map, field) do
55
56
57
58
59
60
61
62
63
    cond do
      is_binary(map[field]) ->
        Map.put(map, field, [map[field]])

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

      true ->
        map
64
65
66
    end
  end

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

74
    explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
75
76
77

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

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

86
  def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
87

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

91
  def fix_explicit_addressing(object) do
92
    explicit_mentions = Utils.determine_explicit_mentions(object)
93

94
95
96
97
    %User{follower_address: follower_collection} =
      object
      |> Containment.get_actor()
      |> User.get_cached_by_ap_id()
98

99
100
101
102
103
104
    explicit_mentions =
      explicit_mentions ++
        [
          Pleroma.Constants.as_public(),
          follower_collection
        ]
105

106
    fix_explicit_addressing(object, explicit_mentions, follower_collection)
lain's avatar
lain committed
107
108
  end

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

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

        true ->
          object
      end
127
    else
128
      object
129
130
131
    end
  end

132
133
  def fix_implicit_addressing(object, _), do: object

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

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

147
  def fix_actor(%{"attributedTo" => actor} = object) do
148
    Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
149
150
  end

151
152
153
  def fix_in_reply_to(object, options \\ [])

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

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

176
  def fix_in_reply_to(object, _options), do: object
lain's avatar
lain committed
177

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

lain's avatar
lain committed
197
    object
Haelwenn's avatar
Haelwenn committed
198
199
    |> Map.put("context", context)
    |> Map.put("conversation", context)
lain's avatar
lain committed
200
201
  end

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

215
    Map.put(object, "attachment", attachments)
216
217
  end

218
  def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
219
    fix_attachments(Map.put(object, "attachment", [attachment]))
220
221
  end

222
  def fix_attachments(object), do: object
223

224
  def fix_url(%{"url" => url} = object) when is_map(url) do
225
    Map.put(object, "url", url["href"])
226
227
  end

228
229
230
  def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
    first_element = Enum.at(url, 0)

231
    link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
232
233
234
235
236
237
238
239

    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
240
241
242
243
244
245
246
247
248
    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

249
    Map.put(object, "url", url_string)
250
251
252
253
  end

  def fix_url(object), do: object

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

261
        Map.put(mapping, name, data["icon"]["url"])
lain's avatar
lain committed
262
      end)
lain's avatar
lain committed
263
264
265
266

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

267
    Map.put(object, "emoji", emoji)
lain's avatar
lain committed
268
269
  end

270
271
272
273
  def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
    name = String.trim(tag["name"], ":")
    emoji = %{name => tag["icon"]["url"]}

274
    Map.put(object, "emoji", emoji)
275
276
  end

277
  def fix_emoji(object), do: object
278

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

285
    Map.put(object, "tag", tag ++ tags)
286
287
  end

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

291
    Map.put(object, "tag", combined)
292
293
  end

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

296
  def fix_tag(object), do: object
297

298
299
300
301
302
  # 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)

303
    Map.put(object, "content", content)
304
305
306
307
  end

  def fix_content_map(object), do: object

308
309
  def fix_type(object, options \\ [])

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

320
  def fix_type(object, _), do: object
321

Maksim's avatar
Maksim committed
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
  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

348
349
350
351
352
353
354
355
356
357
358
  # 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

359
360
  def handle_incoming(data, options \\ [])

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

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

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

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

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

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

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

lain's avatar
lain committed
431
  def handle_incoming(
432
433
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
        _options
lain's avatar
lain committed
434
      ) do
435
436
437
438
    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})),
439
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
440
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
441
           {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
442
443
           {_, false} <- {:user_locked, User.locked?(followed)},
           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
444
445
           {_, {:ok, _}} <-
             {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
kaniini's avatar
kaniini committed
446
447
        ActivityPub.accept(%{
          to: [follower.ap_id],
448
          actor: followed,
kaniini's avatar
kaniini committed
449
450
451
          object: data,
          local: true
        })
452
453
      else
        {:user_blocked, true} ->
454
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
455
456
457
458
459
460
461
462
463

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

        {:follow, {:error, _}} ->
464
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
465
466
467
468
469
470
471
472
473
474

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

        {:user_locked, true} ->
          :noop
475
      end
lain's avatar
lain committed
476

477
478
      {:ok, activity}
    else
479
480
      _e ->
        :error
481
482
483
    end
  end

484
  def handle_incoming(
485
486
        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
        _options
487
      ) do
488
    with actor <- Containment.get_actor(data),
489
         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
490
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
491
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
492
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
493
494
495
496
497
498
499
500
         {: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
      })
501
502
    else
      _e -> :error
503
504
505
506
    end
  end

  def handle_incoming(
507
508
        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
        _options
509
      ) do
510
    with actor <- Containment.get_actor(data),
511
         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
512
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
513
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
514
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
lain's avatar
lain committed
515
         {:ok, activity} <-
516
           ActivityPub.reject(%{
lain's avatar
lain committed
517
             to: follow_activity.data["to"],
518
             type: "Reject",
519
             actor: followed,
lain's avatar
lain committed
520
521
522
             object: follow_activity.data["id"],
             local: false
           }) do
523
524
      User.unfollow(follower, followed)

525
      {:ok, activity}
526
527
    else
      _e -> :error
528
529
530
    end
  end

lain's avatar
lain committed
531
  def handle_incoming(
532
533
        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
534
      ) do
535
    with actor <- Containment.get_actor(data),
536
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
537
         {:ok, object} <- get_obj_helper(object_id),
feld's avatar
feld committed
538
         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
lain's avatar
lain committed
539
540
541
542
543
544
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
545
  def handle_incoming(
546
547
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
        _options
lain's avatar
lain committed
548
      ) do
549
    with actor <- Containment.get_actor(data),
550
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
551
         {:ok, object} <- get_obj_helper(object_id),
lain's avatar
lain committed
552
         public <- Visibility.is_public?(data),
553
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
lain's avatar
lain committed
554
555
556
557
558
559
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
560
  def handle_incoming(
561
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
562
563
          data,
        _options
564
565
      )
      when object_type in ["Person", "Application", "Service", "Organization"] do
minibikini's avatar
minibikini committed
566
    with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
lain's avatar
lain committed
567
568
      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)

rinpatch's avatar
rinpatch committed
569
570
      banner = new_user_data[:info][:banner]
      locked = new_user_data[:info][:locked] || false
571
572
573
574
575
576
      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
577
578
579
580

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

      actor
584
      |> User.upgrade_changeset(update_data, true)
lain's avatar
lain committed
585
      |> User.update_and_set_cache()
lain's avatar
lain committed
586

lain's avatar
lain committed
587
588
589
590
591
592
593
      ActivityPub.update(%{
        local: false,
        to: data["to"] || [],
        cc: data["cc"] || [],
        object: object,
        actor: actor_id
      })
lain's avatar
lain committed
594
595
596
597
598
599
600
    else
      e ->
        Logger.error(e)
        :error
    end
  end

601
602
603
604
605
  # 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
606
  def handle_incoming(
607
        %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
608
        _options
lain's avatar
lain committed
609
      ) do
lain's avatar
lain committed
610
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
611

612
    with actor <- Containment.get_actor(data),
613
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
614
         {:ok, object} <- get_obj_helper(object_id),
615
         :ok <- Containment.contain_origin(actor.ap_id, object.data),
lain's avatar
lain committed
616
617
618
         {:ok, activity} <- ActivityPub.delete(object, false) do
      {:ok, activity}
    else
619
620
621
      nil ->
        case User.get_cached_by_ap_id(object_id) do
          %User{ap_id: ^actor} = user ->
622
            User.delete(user)
623
624
625
626
627
628
629

          nil ->
            :error
        end

      _e ->
        :error
lain's avatar
lain committed
630
631
632
    end
  end

633
  def handle_incoming(
634
635
        %{
          "type" => "Undo",
636
          "object" => %{"type" => "Announce", "object" => object_id},
Maksim's avatar
Maksim committed
637
          "actor" => _actor,
638
          "id" => id
639
640
        } = data,
        _options
641
      ) do
642
    with actor <- Containment.get_actor(data),
643
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
644
         {:ok, object} <- get_obj_helper(object_id),
645
         {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
646
647
      {:ok, activity}
    else
Thog's avatar
Thog committed
648
      _e -> :error
649
650
651
    end
  end

normandy's avatar
normandy committed
652
653
654
655
656
657
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Follow", "object" => followed},
          "actor" => follower,
          "id" => id
658
659
        } = _data,
        _options
normandy's avatar
normandy committed
660
      ) do
normandy's avatar
normandy committed
661
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
662
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
normandy's avatar
normandy committed
663
         {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
normandy's avatar
normandy committed
664
665
666
      User.unfollow(follower, followed)
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
667
      _e -> :error
normandy's avatar
normandy committed
668
669
670
    end
  end

normandy's avatar
normandy committed
671
672
673
674
675
676
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Block", "object" => blocked},
          "actor" => blocker,
          "id" => id
677
678
        } = _data,
        _options
normandy's avatar
normandy committed
679
      ) do
680
    with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
681
         {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
682
         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
normandy's avatar
normandy committed
683
      User.unblock(blocker, blocked)
normandy's avatar
normandy committed
684
685
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
686
      _e -> :error
normandy's avatar
normandy committed
687
688
689
    end
  end

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

Thog's avatar
Thog committed
705
706
707
708
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Like", "object" => object_id},
Maksim's avatar
Maksim committed
709
          "actor" => _actor,
Thog's avatar
Thog committed
710
          "id" => id
711
712
        } = data,
        _options
Thog's avatar
Thog committed
713
      ) do
714
    with actor <- Containment.get_actor(data),
715
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
716
         {:ok, object} <- get_obj_helper(object_id),
Thog's avatar
Thog committed
717
718
719
         {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
      {:ok, activity}
    else
Thog's avatar
Thog committed
720
      _e -> :error
Thog's avatar
Thog committed
721
722
723
    end
  end

724
  def handle_incoming(_, _), do: :error
725

726
  @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
727
  def get_obj_helper(id, options \\ []) do
728
729
730
731
732
    if object = Object.normalize(id, true, options) do
      {:ok, object}
    else
      nil
    end
733
734
  end

735
736
737
738
  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)
739
740
741
742
    else
      _e -> object
    end
  end
lain's avatar
lain committed
743

744
745
746
  def set_reply_to_uri(obj), do: obj

  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
747
748
  def prepare_object(object) do
    object
lain's avatar
lain committed
749
    |> set_sensitive
lain's avatar
lain committed
750
    |> add_hashtags
lain's avatar
lain committed
751
    |> add_mention_tags
lain's avatar
lain committed
752
    |> add_emoji_tags
lain's avatar
lain committed
753
    |> add_attributed_to
lain's avatar
lain committed
754
    |> prepare_attachments
lain's avatar
lain committed
755
    |> set_conversation
756
    |> set_reply_to_uri
757
758
    |> strip_internal_fields
    |> strip_internal_tags
759
    |> set_type
lain's avatar
lain committed
760
761
  end

feld's avatar
feld committed
762
763
764
765
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
766

767
  def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
lain's avatar
lain committed
768
    object =
minibikini's avatar
minibikini committed
769
770
771
      object_id
      |> Object.normalize()
      |> Map.get(:data)
lain's avatar
lain committed
772
773
774
775
776
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
777
      |> Map.merge(Utils.make_json_ld_header())
minibikini's avatar
minibikini committed
778
      |> Map.delete("bcc")
lain's avatar
lain committed
779
780
781
782

    {:ok, data}
  end

kaniini's avatar
kaniini committed
783
784
785
  # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  # because of course it does.
  def prepare_outgoing(%{"type" => "Accept"} = data) do
786
    with follow_activity <- Activity.normalize(data["object"]) do
kaniini's avatar
kaniini committed
787
788
789
790
791
792
793
794
795
796
      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
797
        |> Map.merge(Utils.make_json_ld_header())
kaniini's avatar
kaniini committed
798
799
800
801
802

      {:ok, data}
    end
  end

803
  def prepare_outgoing(%{"type" => "Reject"} = data) do
804
    with follow_activity <- Activity.normalize(data["object"]) do
805
806
807
808
809
810
811
812
813
814
      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
815
        |> Map.merge(Utils.make_json_ld_header())
816
817
818
819
820

      {:ok, data}
    end
  end

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

    {:ok, data}
  end

831
832
833
834
835
836
  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)
837
    else
838
839
840
841
842
843
      {:fetch, e} ->
        Logger.error("Couldn't fetch #{object} #{inspect(e)}")
        data

      _ ->
        data
844
845
846
    end
  end

847
848
  def maybe_fix_object_url(data), do: data

lain's avatar
lain committed
849
  def add_hashtags(object) do
lain's avatar
lain committed
850
851
    tags =
      (object["tag"] || [])
852
853
854
855
856
857
858
859
860
861
862
863
      |> 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
864
      end)
lain's avatar
lain committed
865

866
    Map.put(object, "tag", tags)
lain's avatar
lain committed
867
868
  end

lain's avatar
lain committed
869
  def add_mention_tags(object) do
lain's avatar
lain committed
870
    mentions =
871
872
      object
      |> Utils.get_notified_from_object()
lain's avatar
lain committed
873
874
875
      |> Enum.map(fn user ->
        %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
      end)
lain's avatar
lain committed
876

lain's avatar
lain committed
877
    tags = object["tag"] || []
lain's avatar
lain committed
878

879
    Map.put(object, "tag", tags ++ mentions)
lain's avatar
lain committed
880
881
  end

Haelwenn's avatar
Haelwenn committed
882
883
884
  def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
    user_info = add_emoji_tags(user_info)

885
    Map.put(object, :info, user_info)
Haelwenn's avatar
Haelwenn committed
886
887
  end

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

    out =
893
      Enum.map(emoji, fn {name, url} ->
lain's avatar
lain committed
894
895
896
897
898
899
900
901
        %{
          "icon" => %{"url" => url, "type" => "Image"},
          "name" => ":" <> name <> ":",
          "type" => "Emoji",
          "updated" => "1970-01-01T00:00:00Z",
          "id" => url
        }
      end)
lain's avatar
lain committed
902

903
    Map.put(object, "tag", tags ++ out)
lain's avatar
lain committed
904
905
  end

906
  def add_emoji_tags(object), do: object
Haelwenn's avatar
Haelwenn committed
907

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

lain's avatar
lain committed
912
913
914
915
916
  def set_sensitive(object) do
    tags = object["tag"] || []
    Map.put(object, "sensitive", "nsfw" in tags)
  end