transmogrifier.ex 28.8 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
11
  alias Pleroma.Activity
  alias Pleroma.Object
  alias Pleroma.Repo
12
  alias Pleroma.User
Haelwenn's avatar
Haelwenn committed
13
14
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
15
  alias Pleroma.Web.ActivityPub.Visibility
16

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

19
20
  require Logger

21
22
23
24
25
  def get_actor(%{"actor" => actor}) when is_binary(actor) do
    actor
  end

  def get_actor(%{"actor" => actor}) when is_list(actor) do
26
27
28
    if is_binary(Enum.at(actor, 0)) do
      Enum.at(actor, 0)
    else
29
      Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
30
31
      |> Map.get("id")
    end
32
33
  end

34
35
  def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
    id
36
37
  end

38
39
40
41
  def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
    get_actor(%{"actor" => actor})
  end

42
43
44
  @doc """
  Checks that an imported AP object's actor matches the domain it came from.
  """
Maksim's avatar
Maksim committed
45
  def contain_origin(_id, %{"actor" => nil}), do: :error
46

Maksim's avatar
Maksim committed
47
  def contain_origin(id, %{"actor" => _actor} = params) do
48
    id_uri = URI.parse(id)
49
    actor_uri = URI.parse(get_actor(params))
50
51
52
53
54
55
56
57

    if id_uri.host == actor_uri.host do
      :ok
    else
      :error
    end
  end

Maksim's avatar
Maksim committed
58
  def contain_origin_from_id(_id, %{"id" => nil}), do: :error
59

Maksim's avatar
Maksim committed
60
  def contain_origin_from_id(id, %{"id" => other_id} = _params) do
61
62
63
64
65
66
67
68
69
70
    id_uri = URI.parse(id)
    other_uri = URI.parse(other_id)

    if id_uri.host == other_uri.host do
      :ok
    else
      :error
    end
  end

71
72
73
74
75
  @doc """
  Modifies an incoming AP object (mastodon format) to our internal format.
  """
  def fix_object(object) do
    object
76
    |> fix_actor
77
    |> fix_url
78
    |> fix_attachments
lain's avatar
lain committed
79
    |> fix_context
lain's avatar
lain committed
80
    |> fix_in_reply_to
lain's avatar
lain committed
81
    |> fix_emoji
82
    |> fix_tag
83
    |> fix_content_map
84
    |> fix_likes
85
86
87
88
    |> fix_addressing
  end

  def fix_addressing_list(map, field) do
89
90
91
92
93
94
95
96
97
    cond do
      is_binary(map[field]) ->
        Map.put(map, field, [map[field]])

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

      true ->
        map
98
99
100
    end
  end

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
  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

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

124
  def fix_explicit_addressing(object) do
125
126
127
128
129
130
131
132
    explicit_mentions =
      object
      |> Utils.determine_explicit_mentions()

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

    object
    |> fix_explicit_addressing(explicit_mentions)
lain's avatar
lain committed
133
134
  end

135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
  # 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
    else
      object
    end
  end

  def fix_implicit_addressing(object, _), do: object

160
  def fix_addressing(object) do
161
    %User{} = user = User.get_or_fetch_by_ap_id(object["actor"])
162
163
    followers_collection = User.ap_followers(user)

164
    object
165
166
167
168
    |> fix_addressing_list("to")
    |> fix_addressing_list("cc")
    |> fix_addressing_list("bto")
    |> fix_addressing_list("bcc")
169
    |> fix_explicit_addressing
170
    |> fix_implicit_addressing(followers_collection)
lain's avatar
lain committed
171
172
  end

173
174
  def fix_actor(%{"attributedTo" => actor} = object) do
    object
175
    |> Map.put("actor", get_actor(%{"actor" => actor}))
176
177
  end

178
179
180
181
182
  # 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
183
184
185
186
187
188
189
190
191
    object
    |> Map.put("likes", [])
    |> Map.put("like_count", 0)
  end

  def fix_likes(object) do
    object
  end

192
193
194
  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
      when not is_nil(in_reply_to) do
    in_reply_to_id =
195
      cond do
196
197
198
199
200
201
202
203
204
        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)

205
        # Maybe I should output an error too?
206
207
        true ->
          ""
208
209
      end

210
    case fetch_obj_helper(in_reply_to_id) do
lain's avatar
lain committed
211
      {:ok, replied_object} ->
212
        with %Activity{} = activity <-
213
               Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
214
215
216
217
218
219
220
221
          object
          |> Map.put("inReplyTo", replied_object.data["id"])
          |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
          |> Map.put("inReplyToStatusId", activity.id)
          |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
          |> Map.put("context", replied_object.data["context"] || object["conversation"])
        else
          e ->
222
            Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
223
224
            object
        end
lain's avatar
lain committed
225

lain's avatar
lain committed
226
      e ->
227
        Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
lain's avatar
lain committed
228
229
230
        object
    end
  end
lain's avatar
lain committed
231

lain's avatar
lain committed
232
233
  def fix_in_reply_to(object), do: object

lain's avatar
lain committed
234
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
235
236
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

lain's avatar
lain committed
237
    object
Haelwenn's avatar
Haelwenn committed
238
239
    |> Map.put("context", context)
    |> Map.put("conversation", context)
lain's avatar
lain committed
240
241
  end

242
  def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
lain's avatar
lain committed
243
    attachments =
244
      attachment
lain's avatar
lain committed
245
      |> Enum.map(fn data ->
246
247
248
249
250
251
252
253
        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
254
      end)
lain's avatar
lain committed
255
256
257

    object
    |> Map.put("attachment", attachments)
258
259
  end

260
  def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
261
262
    Map.put(object, "attachment", [attachment])
    |> fix_attachments()
263
264
  end

265
  def fix_attachments(object), do: object
266

267
268
269
270
271
  def fix_url(%{"url" => url} = object) when is_map(url) do
    object
    |> Map.put("url", url["href"])
  end

272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
  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
288
289
290
291
292
293
294
295
296
    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

297
298
    object
    |> Map.put("url", url_string)
299
300
301
302
  end

  def fix_url(object), do: object

303
  def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
lain's avatar
lain committed
304
305
306
307
308
    emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)

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

lain's avatar
lain committed
311
312
        mapping |> Map.put(name, data["icon"]["url"])
      end)
lain's avatar
lain committed
313
314
315
316
317
318
319
320

    # 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

321
322
323
324
325
326
327
328
  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

329
  def fix_emoji(object), do: object
330

331
  def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
lain's avatar
lain committed
332
    tags =
333
      tag
lain's avatar
lain committed
334
335
      |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
      |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
336

337
    combined = tag ++ tags
338
339
340
341
342

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

343
344
  def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
    combined = [tag, String.slice(hashtag, 1..-1)]
345
346
347
348
349

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

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

352
  def fix_tag(object), do: object
353

354
355
356
357
358
359
360
361
362
363
364
  # 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
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
  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

391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
  # 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

425
426
427
428
429
430
  # 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

431
432
433
  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
kaniini's avatar
kaniini committed
434
  def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
435
      when objtype in ["Article", "Note", "Video", "Page"] do
436
    actor = get_actor(data)
437
438
439
440

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

442
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
lain's avatar
lain committed
443
         %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
444
      object = fix_object(data["object"])
445

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

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

lain's avatar
lain committed
468
469
470
  def handle_incoming(
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
      ) do
471
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
472
473
         %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
474
      if not User.locked?(followed) do
kaniini's avatar
kaniini committed
475
476
        ActivityPub.accept(%{
          to: [follower.ap_id],
477
          actor: followed,
kaniini's avatar
kaniini committed
478
479
480
481
          object: data,
          local: true
        })

482
483
        User.follow(follower, followed)
      end
lain's avatar
lain committed
484

485
486
487
488
489
490
      {:ok, activity}
    else
      _e -> :error
    end
  end

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

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

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

535
      {:ok, activity}
536
537
    else
      _e -> :error
538
539
540
    end
  end

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

lain's avatar
lain committed
554
  def handle_incoming(
Maksim's avatar
Maksim committed
555
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
lain's avatar
lain committed
556
      ) do
557
558
559
    with actor <- get_actor(data),
         %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
lain's avatar
lain committed
560
         public <- Visibility.is_public?(data),
561
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
lain's avatar
lain committed
562
563
564
565
566
567
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
568
  def handle_incoming(
569
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
lain's avatar
lain committed
570
          data
571
572
      )
      when object_type in ["Person", "Application", "Service", "Organization"] do
lain's avatar
lain committed
573
574
575
576
    with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)

      banner = new_user_data[:info]["banner"]
577
      locked = new_user_data[:info]["locked"] || false
lain's avatar
lain committed
578
579
580
581

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

      actor
      |> User.upgrade_changeset(update_data)
lain's avatar
lain committed
586
      |> User.update_and_set_cache()
lain's avatar
lain committed
587

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

602
603
604
605
606
  # 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
607
  def handle_incoming(
608
        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
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 <- get_actor(data),
613
         %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
614
         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
615
         :ok <- 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
feld's avatar
feld committed
619
      _e -> :error
lain's avatar
lain committed
620
621
622
    end
  end

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

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

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

678
  def handle_incoming(
Maksim's avatar
Maksim committed
679
        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
680
      ) do
href's avatar
href committed
681
    with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
682
         %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
normandy's avatar
normandy committed
683
         %User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
684
         {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
685
      User.unfollow(blocker, blocked)
686
      User.block(blocker, blocked)
normandy's avatar
normandy committed
687
688
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
689
      _e -> :error
normandy's avatar
normandy committed
690
691
    end
  end
692

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

711
712
  def handle_incoming(_), do: :error

713
714
715
  def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id)
  def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"])

716
  def get_obj_helper(id) do
717
    if object = Object.normalize(id), do: {:ok, object}, else: nil
718
719
  end

720
721
722
723
  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)
724
725
726
727
    else
      _e -> object
    end
  end
lain's avatar
lain committed
728

729
730
731
  def set_reply_to_uri(obj), do: obj

  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
732
733
  def prepare_object(object) do
    object
lain's avatar
lain committed
734
    |> set_sensitive
lain's avatar
lain committed
735
    |> add_hashtags
lain's avatar
lain committed
736
    |> add_mention_tags
lain's avatar
lain committed
737
    |> add_emoji_tags
lain's avatar
lain committed
738
    |> add_attributed_to
739
    |> add_likes
lain's avatar
lain committed
740
    |> prepare_attachments
lain's avatar
lain committed
741
    |> set_conversation
742
    |> set_reply_to_uri
743
744
    |> strip_internal_fields
    |> strip_internal_tags
lain's avatar
lain committed
745
746
  end

feld's avatar
feld committed
747
748
749
750
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
751

752
  def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do
lain's avatar
lain committed
753
754
755
756
757
758
759
    object =
      object
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
760
      |> Map.merge(Utils.make_json_ld_header())
lain's avatar
lain committed
761
762
763
764

    {:ok, data}
  end

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

      {:ok, data}
    end
  end

785
  def prepare_outgoing(%{"type" => "Reject"} = data) do
786
    with follow_activity <- Activity.normalize(data["object"]) do
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())
798
799
800
801
802

      {:ok, data}
    end
  end

feld's avatar
feld committed
803
  def prepare_outgoing(%{"type" => _type} = data) do
lain's avatar
lain committed
804
805
    data =
      data
806
      |> strip_internal_fields
lain's avatar
lain committed
807
      |> maybe_fix_object_url
lain's avatar
lain committed
808
      |> Map.merge(Utils.make_json_ld_header())
809
810
811
812

    {:ok, data}
  end

813
814
  def maybe_fix_object_url(data) do
    if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
815
      case fetch_obj_helper(data["object"]) do
816
817
        {:ok, relative_object} ->
          if relative_object.data["external_url"] do
feld's avatar
feld committed
818
            _data =
lain's avatar
lain committed
819
820
              data
              |> Map.put("object", relative_object.data["external_url"])
821
822
823
          else
            data
          end
lain's avatar
lain committed
824

825
826
827
828
829
830
831
832
833
        e ->
          Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
          data
      end
    else
      data
    end
  end

lain's avatar
lain committed
834
  def add_hashtags(object) do
lain's avatar
lain committed
835
836
    tags =
      (object["tag"] || [])
837
838
839
840
841
842
843
844
845
846
847
848
      |> 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
849
      end)
lain's avatar
lain committed
850
851
852
853
854

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

lain's avatar
lain committed
855
  def add_mention_tags(object) do
lain's avatar
lain committed
856
    mentions =
857
858
      object
      |> Utils.get_notified_from_object()
lain's avatar
lain committed
859
860
861
      |> Enum.map(fn user ->
        %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
      end)
lain's avatar
lain committed
862

lain's avatar
lain committed
863
    tags = object["tag"] || []
lain's avatar
lain committed
864
865

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

lain's avatar
lain committed
869
870
871
872
  # TODO: we should probably send mtime instead of unix epoch time for updated
  def add_emoji_tags(object) do
    tags = object["tag"] || []
    emoji = object["emoji"] || []
lain's avatar
lain committed
873
874
875
876
877
878
879
880
881
882
883
884

    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
885
886
887
888
889

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

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

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

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

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

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

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

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

lain's avatar
lain committed
957
  defp user_upgrade_task(user) do
958
959
    # 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
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977

    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
978
979
    Repo.update_all(q, [])

980
981
    maybe_retire_websub(user.ap_id)

lain's avatar
lain committed
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
    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
              )
          ]
        ]
      )

lain's avatar
lain committed
999
1000
    Repo.update_all(q, [])
  end
For faster browsing, not all history is shown. View entire blame