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

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

20
21
  require Logger

22
23
24
25
26
  @doc """
  Modifies an incoming AP object (mastodon format) to our internal format.
  """
  def fix_object(object) do
    object
27
    |> fix_actor
28
    |> fix_url
29
    |> fix_attachments
lain's avatar
lain committed
30
    |> fix_context
lain's avatar
lain committed
31
    |> fix_in_reply_to
lain's avatar
lain committed
32
    |> fix_emoji
33
    |> fix_tag
34
    |> fix_content_map
35
    |> fix_likes
36
    |> fix_addressing
37
    |> fix_summary
38
    |> fix_type
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  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", "")
54
55
56
  end

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

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

      true ->
        map
66
67
68
    end
  end

69
70
71
72
73
  def fix_explicit_addressing(
        %{"to" => to, "cc" => cc} = object,
        explicit_mentions,
        follower_collection
      ) do
74
75
76
77
78
79
80
81
82
83
    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)
84
      |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
85
86
87
88
89
90
91
      |> Enum.uniq()

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

92
  def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
93

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

97
  def fix_explicit_addressing(object) do
98
99
100
101
    explicit_mentions =
      object
      |> Utils.determine_explicit_mentions()

102
103
104
105
    follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address

    explicit_mentions =
      explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection]
106

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

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
  # 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
        "https://www.w3.org/ns/activitystreams#Public" in cc ->
          to = to ++ [followers_collection]
          Map.put(object, "to", to)

        "https://www.w3.org/ns/activitystreams#Public" in to ->
          cc = cc ++ [followers_collection]
          Map.put(object, "cc", cc)

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

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

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

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

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

153
154
155
156
157
  # Check for standardisation
  # This is what Peertube does
  # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
  # Prismo returns only an integer (count) as "likes"
  def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
158
159
160
161
162
163
164
165
166
    object
    |> Map.put("likes", [])
    |> Map.put("like_count", 0)
  end

  def fix_likes(object) do
    object
  end

167
168
169
  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
      when not is_nil(in_reply_to) do
    in_reply_to_id =
170
      cond do
171
172
173
174
175
176
177
178
179
        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)

180
        # Maybe I should output an error too?
181
182
        true ->
          ""
183
184
      end

185
    case get_obj_helper(in_reply_to_id) do
lain's avatar
lain committed
186
      {:ok, replied_object} ->
rinpatch's avatar
rinpatch committed
187
        with %Activity{} = _activity <-
188
               Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
189
190
191
192
193
194
195
          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 ->
196
            Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
197
198
            object
        end
lain's avatar
lain committed
199

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

lain's avatar
lain committed
206
207
  def fix_in_reply_to(object), do: object

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

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

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

    object
    |> Map.put("attachment", attachments)
232
233
  end

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

239
  def fix_attachments(object), do: object
240

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

246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
  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
262
263
264
265
266
267
268
269
270
    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

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

  def fix_url(object), do: object

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

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

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

    # 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

295
296
297
298
299
300
301
302
  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

303
  def fix_emoji(object), do: object
304

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

311
    combined = tag ++ tags
312
313
314
315
316

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

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

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

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

326
  def fix_tag(object), do: object
327

328
329
330
331
332
333
334
335
336
337
338
  # 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

339
340
341
  def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do
    reply = Object.normalize(reply_id)

342
    if reply && (reply.data["type"] == "Question" and object["name"]) do
343
344
345
346
347
348
349
350
      Map.put(object, "type", "Answer")
    else
      object
    end
  end

  def fix_type(object), do: object

Maksim's avatar
Maksim committed
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
  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

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

411
412
413
414
415
416
  # disallow objects with bogus IDs
  def handle_incoming(%{"id" => nil}), do: :error
  def handle_incoming(%{"id" => ""}), do: :error
  # length of https:// = 8, should validate better, but good enough for now.
  def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error

417
418
419
  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
kaniini's avatar
kaniini committed
420
  def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
421
      when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
422
    actor = Containment.get_actor(data)
423
424
425
426

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

428
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
429
         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
430
      object = fix_object(data["object"])
431

432
433
434
435
      params = %{
        to: data["to"],
        object: object,
        actor: user,
lain's avatar
lain committed
436
        context: object["conversation"],
437
438
        local: false,
        published: data["published"],
lain's avatar
lain committed
439
440
441
        additional:
          Map.take(data, [
            "cc",
442
            "directMessage",
lain's avatar
lain committed
443
444
            "id"
          ])
445
446
447
448
      }

      ActivityPub.create(params)
    else
lain's avatar
lain committed
449
      %Activity{} = activity -> {:ok, activity}
450
451
452
453
      _e -> :error
    end
  end

lain's avatar
lain committed
454
455
456
  def handle_incoming(
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
      ) do
457
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
458
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
459
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
460
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
461
           {_, false} <-
462
             {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
463
464
           {_, false} <- {:user_locked, User.locked?(followed)},
           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
465
466
           {_, {:ok, _}} <-
             {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
kaniini's avatar
kaniini committed
467
468
        ActivityPub.accept(%{
          to: [follower.ap_id],
469
          actor: followed,
kaniini's avatar
kaniini committed
470
471
472
          object: data,
          local: true
        })
473
474
      else
        {:user_blocked, true} ->
475
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
476
477
478
479
480
481
482
483
484

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

        {:follow, {:error, _}} ->
485
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
486
487
488
489
490
491
492
493
494
495

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

        {:user_locked, true} ->
          :noop
496
      end
lain's avatar
lain committed
497

498
499
      {:ok, activity}
    else
500
501
      _e ->
        :error
502
503
504
    end
  end

505
  def handle_incoming(
Maksim's avatar
Maksim committed
506
        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
507
      ) do
508
    with actor <- Containment.get_actor(data),
509
         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
510
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
511
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
512
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
513
514
515
516
517
518
519
520
         {: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
      })
521
522
    else
      _e -> :error
523
524
525
526
    end
  end

  def handle_incoming(
Maksim's avatar
Maksim committed
527
        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
528
      ) do
529
    with actor <- Containment.get_actor(data),
530
         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
531
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
532
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
533
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
lain's avatar
lain committed
534
         {:ok, activity} <-
535
           ActivityPub.reject(%{
lain's avatar
lain committed
536
             to: follow_activity.data["to"],
537
             type: "Reject",
538
             actor: followed,
lain's avatar
lain committed
539
540
541
             object: follow_activity.data["id"],
             local: false
           }) do
542
543
      User.unfollow(follower, followed)

544
      {:ok, activity}
545
546
    else
      _e -> :error
547
548
549
    end
  end

lain's avatar
lain committed
550
  def handle_incoming(
Maksim's avatar
Maksim committed
551
        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
lain's avatar
lain committed
552
      ) do
553
    with actor <- Containment.get_actor(data),
554
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
555
         {:ok, object} <- get_obj_helper(object_id),
feld's avatar
feld committed
556
         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
lain's avatar
lain committed
557
558
559
560
561
562
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
563
  def handle_incoming(
Maksim's avatar
Maksim committed
564
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
lain's avatar
lain committed
565
      ) do
566
    with actor <- Containment.get_actor(data),
567
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
568
         {:ok, object} <- get_obj_helper(object_id),
lain's avatar
lain committed
569
         public <- Visibility.is_public?(data),
570
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
lain's avatar
lain committed
571
572
573
574
575
576
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
577
  def handle_incoming(
578
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
lain's avatar
lain committed
579
          data
580
581
      )
      when object_type in ["Person", "Application", "Service", "Organization"] do
minibikini's avatar
minibikini committed
582
    with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
lain's avatar
lain committed
583
584
585
      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)

      banner = new_user_data[:info]["banner"]
586
      locked = new_user_data[:info]["locked"] || false
lain's avatar
lain committed
587
588
589
590

      update_data =
        new_user_data
        |> Map.take([:name, :bio, :avatar])
lain's avatar
lain committed
591
        |> Map.put(:info, %{"banner" => banner, "locked" => locked})
lain's avatar
lain committed
592
593
594

      actor
      |> User.upgrade_changeset(update_data)
lain's avatar
lain committed
595
      |> User.update_and_set_cache()
lain's avatar
lain committed
596

lain's avatar
lain committed
597
598
599
600
601
602
603
      ActivityPub.update(%{
        local: false,
        to: data["to"] || [],
        cc: data["cc"] || [],
        object: object,
        actor: actor_id
      })
lain's avatar
lain committed
604
605
606
607
608
609
610
    else
      e ->
        Logger.error(e)
        :error
    end
  end

611
612
613
614
615
  # 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
616
  def handle_incoming(
617
        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
lain's avatar
lain committed
618
      ) do
lain's avatar
lain committed
619
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
620

621
    with actor <- Containment.get_actor(data),
622
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
623
         {:ok, object} <- get_obj_helper(object_id),
624
         :ok <- Containment.contain_origin(actor.ap_id, object.data),
lain's avatar
lain committed
625
626
627
         {:ok, activity} <- ActivityPub.delete(object, false) do
      {:ok, activity}
    else
feld's avatar
feld committed
628
      _e -> :error
lain's avatar
lain committed
629
630
631
    end
  end

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

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

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

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

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

720
721
  def handle_incoming(_), do: :error

722
  def get_obj_helper(id) do
723
    if object = Object.normalize(id), do: {:ok, object}, else: nil
724
725
  end

726
727
728
729
  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)
730
731
732
733
    else
      _e -> object
    end
  end
lain's avatar
lain committed
734

735
736
737
  def set_reply_to_uri(obj), do: obj

  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
738
739
  def prepare_object(object) do
    object
lain's avatar
lain committed
740
    |> set_sensitive
lain's avatar
lain committed
741
    |> add_hashtags
lain's avatar
lain committed
742
    |> add_mention_tags
lain's avatar
lain committed
743
    |> add_emoji_tags
lain's avatar
lain committed
744
    |> add_attributed_to
745
    |> add_likes
lain's avatar
lain committed
746
    |> prepare_attachments
lain's avatar
lain committed
747
    |> set_conversation
748
    |> set_reply_to_uri
749
750
    |> strip_internal_fields
    |> strip_internal_tags
751
    |> set_type
lain's avatar
lain committed
752
753
  end

feld's avatar
feld committed
754
755
756
757
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
758

759
  def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
lain's avatar
lain committed
760
    object =
761
      Object.normalize(object_id).data
lain's avatar
lain committed
762
763
764
765
766
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
767
      |> Map.merge(Utils.make_json_ld_header())
lain's avatar
lain committed
768
769
770
771

    {:ok, data}
  end

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

      {:ok, data}
    end
  end

792
  def prepare_outgoing(%{"type" => "Reject"} = data) do
793
    with follow_activity <- Activity.normalize(data["object"]) do
794
795
796
797
798
799
800
801
802
803
      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
804
        |> Map.merge(Utils.make_json_ld_header())
805
806
807
808
809

      {:ok, data}
    end
  end

feld's avatar
feld committed
810
  def prepare_outgoing(%{"type" => _type} = data) do
lain's avatar
lain committed
811
812
    data =
      data
813
      |> strip_internal_fields
lain's avatar
lain committed
814
      |> maybe_fix_object_url
lain's avatar
lain committed
815
      |> Map.merge(Utils.make_json_ld_header())
816
817
818
819

    {:ok, data}
  end

820
821
  def maybe_fix_object_url(data) do
    if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
822
      case get_obj_helper(data["object"]) do
823
824
        {:ok, relative_object} ->
          if relative_object.data["external_url"] do
feld's avatar
feld committed
825
            _data =
lain's avatar
lain committed
826
827
              data
              |> Map.put("object", relative_object.data["external_url"])
828
829
830
          else
            data
          end
lain's avatar
lain committed
831

832
833
834
835
836
837
838
839
840
        e ->
          Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
          data
      end
    else
      data
    end
  end

lain's avatar
lain committed
841
  def add_hashtags(object) do
lain's avatar
lain committed
842
843
    tags =
      (object["tag"] || [])
844
845
846
847
848
849
850
851
852
853
854
855
      |> 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
856
      end)
lain's avatar
lain committed
857
858
859
860
861

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

lain's avatar
lain committed
862
  def add_mention_tags(object) do
lain's avatar
lain committed
863
    mentions =
864
865
      object
      |> Utils.get_notified_from_object()
lain's avatar
lain committed
866
867
868
      |> Enum.map(fn user ->
        %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
      end)
lain's avatar
lain committed
869

lain's avatar
lain committed
870
    tags = object["tag"] || []
lain's avatar
lain committed
871
872

    object
lain's avatar
lain committed
873
    |> Map.put("tag", tags ++ mentions)
lain's avatar
lain committed
874
875
  end

Haelwenn's avatar
Haelwenn committed
876
877
878
879
880
881
882
  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
883
  # TODO: we should probably send mtime instead of unix epoch time for updated
Haelwenn's avatar
Haelwenn committed
884
  def add_emoji_tags(%{"emoji" => emoji} = object) do
lain's avatar
lain committed
885
    tags = object["tag"] || []
lain's avatar
lain committed
886
887
888
889
890
891
892
893
894
895
896
897

    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
898
899
900
901
902

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

Haelwenn's avatar
Haelwenn committed
903
904
905
906
  def add_emoji_tags(object) do
    object
  end

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

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

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

  def set_type(object), do: object

lain's avatar
lain committed
922
  def add_attributed_to(object) do
923
    attributed_to = object["attributedTo"] || object["actor"]
lain's avatar
lain committed
924
925

    object
926
    |> Map.put("attributedTo", attributed_to)
927
  end
lain's avatar
lain committed
928

929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
  def add_likes(%{"id" => id, "like_count" => likes} = object) do
    likes = %{
      "id" => "#{id}/likes",
      "first" => "#{id}/likes?page=1",
      "type" => "OrderedCollection",
      "totalItems" => likes
    }

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

  def add_likes(object) do
    object
  end

lain's avatar
lain committed
945
  def prepare_attachments(object) do
lain's avatar
lain committed
946
947
948
949
950
951
    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
952
953
954
955

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

957
958
959
960
961
962
963
  defp strip_internal_fields(object) do
    object
    |> Map.drop([
      "like_count",
      "announcements",
      "announcement_count",
      "emoji",
964
965
      "context_id",
      "deleted_activity_id"
966
967
968
969
970
971
972
973
974
975
976
977
978
979
    ])
  end

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

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

  defp strip_internal_tags(object), do: object

980
  def perform(:user_upgrade, user) do
981
982
    # we pass a fake user so that the followers collection is stripped away
    old_follower_address = User.ap_followers(%User{nickname: user.nickname})
lain's avatar
lain committed
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000

    q =
      from(
        u in User,
        where: ^old_follower_address in u.following,
        update: [
          set: [
            following:
              fragment(
                "array_replace(?,?,?)",
                u.following,
                ^old_follower_address,
                ^user.follower_address
              )
          ]
        ]
      )

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