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

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
    |> fix_summary
  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", "")
53
54
55
  end

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

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

      true ->
        map
65
66
67
    end
  end

68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
  def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
    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)
      |> Enum.uniq()

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

  def fix_explicit_addressing(object, _explicit_mentions), do: object

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
93
94
95
    explicit_mentions =
      object
      |> Utils.determine_explicit_mentions()

96
97
98
99
    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]
100
101
102

    object
    |> fix_explicit_addressing(explicit_mentions)
lain's avatar
lain committed
103
104
  end

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
  # 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
123
    else
124
      object
125
126
127
    end
  end

128
129
  def fix_implicit_addressing(object, _), do: object

130
  def fix_addressing(object) do
Alexander Strizhakov's avatar
Alexander Strizhakov committed
131
    {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
132
133
    followers_collection = User.ap_followers(user)

134
    object
135
136
137
138
    |> fix_addressing_list("to")
    |> fix_addressing_list("cc")
    |> fix_addressing_list("bto")
    |> fix_addressing_list("bcc")
139
    |> fix_explicit_addressing
140
    |> fix_implicit_addressing(followers_collection)
lain's avatar
lain committed
141
142
  end

143
144
  def fix_actor(%{"attributedTo" => actor} = object) do
    object
145
    |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
146
147
  end

148
149
150
151
152
  # 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
153
154
155
156
157
158
159
160
161
    object
    |> Map.put("likes", [])
    |> Map.put("like_count", 0)
  end

  def fix_likes(object) do
    object
  end

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

175
        # Maybe I should output an error too?
176
177
        true ->
          ""
178
179
      end

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

lain's avatar
lain committed
195
      e ->
196
        Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
lain's avatar
lain committed
197
198
199
        object
    end
  end
lain's avatar
lain committed
200

lain's avatar
lain committed
201
202
  def fix_in_reply_to(object), do: object

lain's avatar
lain committed
203
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
204
205
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

lain's avatar
lain committed
206
    object
Haelwenn's avatar
Haelwenn committed
207
208
    |> Map.put("context", context)
    |> Map.put("conversation", context)
lain's avatar
lain committed
209
210
  end

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

    object
    |> Map.put("attachment", attachments)
227
228
  end

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

234
  def fix_attachments(object), do: object
235

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

241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
  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
257
258
259
260
261
262
263
264
265
    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

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

  def fix_url(object), do: object

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

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

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

    # 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

290
291
292
293
294
295
296
297
  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

298
  def fix_emoji(object), do: object
299

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

306
    combined = tag ++ tags
307
308
309
310
311

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

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

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

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

321
  def fix_tag(object), do: object
322

323
324
325
326
327
328
329
330
331
332
333
  # 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

Maksim's avatar
Maksim committed
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
  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

360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
  # 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

394
395
396
397
398
399
  # 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

400
401
402
  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
kaniini's avatar
kaniini committed
403
  def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
404
      when objtype in ["Article", "Note", "Video", "Page"] do
405
    actor = Containment.get_actor(data)
406
407
408
409

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

411
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
412
         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
413
      object = fix_object(data["object"])
414

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

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

lain's avatar
lain committed
437
438
439
  def handle_incoming(
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
      ) do
440
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
441
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
442
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
443
444
445
446
447
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
           {:user_blocked, false} <-
             {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
           {:user_locked, false} <- {:user_locked, User.locked?(followed)},
           {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
kaniini's avatar
kaniini committed
448
449
        ActivityPub.accept(%{
          to: [follower.ap_id],
450
          actor: followed,
kaniini's avatar
kaniini committed
451
452
453
          object: data,
          local: true
        })
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
      else
        {:user_blocked, true} ->
          {:ok, _} = Utils.update_follow_state(activity, "reject")

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

        {:follow, {:error, _}} ->
          {:ok, _} = Utils.update_follow_state(activity, "reject")

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

        {:user_locked, true} ->
          :noop
477
      end
lain's avatar
lain committed
478

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

486
  def handle_incoming(
Maksim's avatar
Maksim committed
487
        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
488
      ) do
489
    with actor <- Containment.get_actor(data),
490
         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
491
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
492
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
493
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
lain's avatar
lain committed
494
495
496
497
         {:ok, activity} <-
           ActivityPub.accept(%{
             to: follow_activity.data["to"],
             type: "Accept",
498
             actor: followed,
lain's avatar
lain committed
499
500
501
             object: follow_activity.data["id"],
             local: false
           }) do
502
      if not User.following?(follower, followed) do
Maksim's avatar
Maksim committed
503
        {:ok, _follower} = User.follow(follower, followed)
504
      end
505

506
      {:ok, activity}
507
508
    else
      _e -> :error
509
510
511
512
    end
  end

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

530
      {:ok, activity}
531
532
    else
      _e -> :error
533
534
535
    end
  end

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

lain's avatar
lain committed
549
  def handle_incoming(
Maksim's avatar
Maksim committed
550
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
lain's avatar
lain committed
551
      ) do
552
    with actor <- Containment.get_actor(data),
553
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
554
         {:ok, object} <- get_obj_helper(object_id),
lain's avatar
lain committed
555
         public <- Visibility.is_public?(data),
556
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) 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(
564
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
lain's avatar
lain committed
565
          data
566
567
      )
      when object_type in ["Person", "Application", "Service", "Organization"] do
minibikini's avatar
minibikini committed
568
    with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
lain's avatar
lain committed
569
570
571
      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)

      banner = new_user_data[:info]["banner"]
572
      locked = new_user_data[:info]["locked"] || false
lain's avatar
lain committed
573
574
575
576

      update_data =
        new_user_data
        |> Map.take([:name, :bio, :avatar])
lain's avatar
lain committed
577
        |> Map.put(:info, %{"banner" => banner, "locked" => locked})
lain's avatar
lain committed
578
579
580

      actor
      |> User.upgrade_changeset(update_data)
lain's avatar
lain committed
581
      |> User.update_and_set_cache()
lain's avatar
lain committed
582

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

597
598
599
600
601
  # 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
602
  def handle_incoming(
603
        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
lain's avatar
lain committed
604
      ) do
lain's avatar
lain committed
605
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
606

607
    with actor <- Containment.get_actor(data),
608
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
609
         {:ok, object} <- get_obj_helper(object_id),
610
         :ok <- Containment.contain_origin(actor.ap_id, object.data),
lain's avatar
lain committed
611
612
613
         {:ok, activity} <- ActivityPub.delete(object, false) do
      {:ok, activity}
    else
feld's avatar
feld committed
614
      _e -> :error
lain's avatar
lain committed
615
616
617
    end
  end

618
  def handle_incoming(
619
620
        %{
          "type" => "Undo",
621
          "object" => %{"type" => "Announce", "object" => object_id},
Maksim's avatar
Maksim committed
622
          "actor" => _actor,
623
          "id" => id
624
        } = data
625
      ) do
626
    with actor <- Containment.get_actor(data),
627
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
628
         {:ok, object} <- get_obj_helper(object_id),
629
         {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
630
631
      {:ok, activity}
    else
Thog's avatar
Thog committed
632
      _e -> :error
633
634
635
    end
  end

normandy's avatar
normandy committed
636
637
638
639
640
641
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Follow", "object" => followed},
          "actor" => follower,
          "id" => id
normandy's avatar
normandy committed
642
        } = _data
normandy's avatar
normandy committed
643
      ) do
normandy's avatar
normandy committed
644
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
645
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
normandy's avatar
normandy committed
646
         {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
normandy's avatar
normandy committed
647
648
649
      User.unfollow(follower, followed)
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
650
      _e -> :error
normandy's avatar
normandy committed
651
652
653
    end
  end

normandy's avatar
normandy committed
654
655
656
657
658
659
660
661
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Block", "object" => blocked},
          "actor" => blocker,
          "id" => id
        } = _data
      ) do
href's avatar
href committed
662
    with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
663
         %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
664
         {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
665
         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
normandy's avatar
normandy committed
666
      User.unblock(blocker, blocked)
normandy's avatar
normandy committed
667
668
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
669
      _e -> :error
normandy's avatar
normandy committed
670
671
672
    end
  end

673
  def handle_incoming(
Maksim's avatar
Maksim committed
674
        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
675
      ) 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),
0x1C3B00DA's avatar
0x1C3B00DA committed
678
         {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
679
         {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
680
      User.unfollow(blocker, blocked)
681
      User.block(blocker, blocked)
normandy's avatar
normandy committed
682
683
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
684
      _e -> :error
normandy's avatar
normandy committed
685
686
    end
  end
687

Thog's avatar
Thog committed
688
689
690
691
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Like", "object" => object_id},
Maksim's avatar
Maksim committed
692
          "actor" => _actor,
Thog's avatar
Thog committed
693
          "id" => id
694
        } = data
Thog's avatar
Thog committed
695
      ) do
696
    with actor <- Containment.get_actor(data),
697
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
698
         {:ok, object} <- get_obj_helper(object_id),
Thog's avatar
Thog committed
699
700
701
         {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
      {:ok, activity}
    else
Thog's avatar
Thog committed
702
      _e -> :error
Thog's avatar
Thog committed
703
704
705
    end
  end

706
707
  def handle_incoming(_), do: :error

708
  def get_obj_helper(id) do
709
    if object = Object.normalize(id), do: {:ok, object}, else: nil
710
711
  end

712
713
714
715
  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)
716
717
718
719
    else
      _e -> object
    end
  end
lain's avatar
lain committed
720

721
722
723
  def set_reply_to_uri(obj), do: obj

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

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

744
  def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
lain's avatar
lain committed
745
    object =
746
      Object.normalize(object_id).data
lain's avatar
lain committed
747
748
749
750
751
      |> prepare_object

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

    {:ok, data}
  end

kaniini's avatar
kaniini committed
757
758
759
  # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  # because of course it does.
  def prepare_outgoing(%{"type" => "Accept"} = data) do
760
    with follow_activity <- Activity.normalize(data["object"]) do
kaniini's avatar
kaniini committed
761
762
763
764
765
766
767
768
769
770
      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
771
        |> Map.merge(Utils.make_json_ld_header())
kaniini's avatar
kaniini committed
772
773
774
775
776

      {:ok, data}
    end
  end

777
  def prepare_outgoing(%{"type" => "Reject"} = data) do
778
    with follow_activity <- Activity.normalize(data["object"]) do
779
780
781
782
783
784
785
786
787
788
      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
789
        |> Map.merge(Utils.make_json_ld_header())
790
791
792
793
794

      {:ok, data}
    end
  end

feld's avatar
feld committed
795
  def prepare_outgoing(%{"type" => _type} = data) do
lain's avatar
lain committed
796
797
    data =
      data
798
      |> strip_internal_fields
lain's avatar
lain committed
799
      |> maybe_fix_object_url
lain's avatar
lain committed
800
      |> Map.merge(Utils.make_json_ld_header())
801
802
803
804

    {:ok, data}
  end

805
806
  def maybe_fix_object_url(data) do
    if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
807
      case get_obj_helper(data["object"]) do
808
809
        {:ok, relative_object} ->
          if relative_object.data["external_url"] do
feld's avatar
feld committed
810
            _data =
lain's avatar
lain committed
811
812
              data
              |> Map.put("object", relative_object.data["external_url"])
813
814
815
          else
            data
          end
lain's avatar
lain committed
816

817
818
819
820
821
822
823
824
825
        e ->
          Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
          data
      end
    else
      data
    end
  end

lain's avatar
lain committed
826
  def add_hashtags(object) do
lain's avatar
lain committed
827
828
    tags =
      (object["tag"] || [])
829
830
831
832
833
834
835
836
837
838
839
840
      |> 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
841
      end)
lain's avatar
lain committed
842
843
844
845
846

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

lain's avatar
lain committed
847
  def add_mention_tags(object) do
lain's avatar
lain committed
848
    mentions =
849
850
      object
      |> Utils.get_notified_from_object()
lain's avatar
lain committed
851
852
853
      |> Enum.map(fn user ->
        %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
      end)
lain's avatar
lain committed
854

lain's avatar
lain committed
855
    tags = object["tag"] || []
lain's avatar
lain committed
856
857

    object
lain's avatar
lain committed
858
    |> Map.put("tag", tags ++ mentions)
lain's avatar
lain committed
859
860
  end

Haelwenn's avatar
Haelwenn committed
861
862
863
864
865
866
867
  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
868
  # TODO: we should probably send mtime instead of unix epoch time for updated
Haelwenn's avatar
Haelwenn committed
869
  def add_emoji_tags(%{"emoji" => emoji} = object) do
lain's avatar
lain committed
870
    tags = object["tag"] || []
lain's avatar
lain committed
871
872
873
874
875
876
877
878
879
880
881
882

    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
883
884
885
886
887

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

Haelwenn's avatar
Haelwenn committed
888
889
890
891
  def add_emoji_tags(object) do
    object
  end

lain's avatar
lain committed
892
893
894
895
  def set_conversation(object) do
    Map.put(object, "conversation", object["context"])
  end

lain's avatar
lain committed
896
897
898
899
900
  def set_sensitive(object) do
    tags = object["tag"] || []
    Map.put(object, "sensitive", "nsfw" in tags)
  end

lain's avatar
lain committed
901
  def add_attributed_to(object) do
902
    attributed_to = object["attributedTo"] || object["actor"]
lain's avatar
lain committed
903
904

    object
905
    |> Map.put("attributedTo", attributed_to)
906
  end
lain's avatar
lain committed
907

908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
  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
924
  def prepare_attachments(object) do
lain's avatar
lain committed
925
926
927
928
929
930
    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
931
932
933
934

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

936
937
938
939
940
941
942
  defp strip_internal_fields(object) do
    object
    |> Map.drop([
      "like_count",
      "announcements",
      "announcement_count",
      "emoji",
943
944
      "context_id",
      "deleted_activity_id"
945
946
947
948
949
950
951
952
953
954
955
956
957
958
    ])
  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

959
  def perform(:user_upgrade, user) do
960
961
    # 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
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979

    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
              )
          ]
        ]
      )

lain's avatar
lain committed
980
981
    Repo.update_all(q, [])

982
983
    maybe_retire_websub(user.ap_id)

lain's avatar
lain committed
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
    q =
      from(
        a in Activity,
        where: ^old_follower_address in a.recipients,
        update: [
          set: [
            recipients:
              fragment(
                "array_replace(?,?,?)",
                a.recipients,
                ^old_follower_address,
                ^user.follower_address
              )
          ]
        ]
      )

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