transmogrifier.ex 26.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
11
12
13
14
  alias Pleroma.Activity
  alias Pleroma.User
  alias Pleroma.Object
  alias Pleroma.Repo
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
15

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

18
19
  require Logger

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

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

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

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

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

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

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

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

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

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

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

  def fix_addressing_list(map, field) do
    if is_binary(map[field]) do
      map
      |> Map.put(field, [map[field]])
    else
      map
    end
  end

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
  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

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

119
  def fix_explicit_addressing(object) do
120
121
122
123
124
125
126
127
    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
128
129
  end

130
131
  def fix_addressing(object) do
    object
132
133
134
135
    |> fix_addressing_list("to")
    |> fix_addressing_list("cc")
    |> fix_addressing_list("bto")
    |> fix_addressing_list("bcc")
136
    |> fix_explicit_addressing
lain's avatar
lain committed
137
138
  end

139
140
  def fix_actor(%{"attributedTo" => actor} = object) do
    object
141
    |> Map.put("actor", get_actor(%{"actor" => actor}))
142
143
  end

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

  def fix_likes(object) do
    object
  end

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

171
        # Maybe I should output an error too?
172
173
        true ->
          ""
174
175
      end

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

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

lain's avatar
lain committed
198
199
  def fix_in_reply_to(object), do: object

lain's avatar
lain committed
200
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
201
202
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

lain's avatar
lain committed
203
    object
Haelwenn's avatar
Haelwenn committed
204
205
    |> Map.put("context", context)
    |> Map.put("conversation", context)
lain's avatar
lain committed
206
207
  end

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

    object
    |> Map.put("attachment", attachments)
224
225
  end

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

231
  def fix_attachments(object), do: object
232

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

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

263
264
    object
    |> Map.put("url", url_string)
265
266
267
268
  end

  def fix_url(object), do: object

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

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

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

    # 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

287
288
289
290
291
292
293
294
  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

295
  def fix_emoji(object), do: object
296

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

303
    combined = tag ++ tags
304
305
306
307
308

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

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

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

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

318
  def fix_tag(object), do: object
319

320
321
322
323
324
325
326
327
328
329
330
  # 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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
  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

357
358
359
360
361
362
  # 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

363
364
365
  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
kaniini's avatar
kaniini committed
366
  def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
367
      when objtype in ["Article", "Note", "Video", "Page"] do
368
    actor = get_actor(data)
369
370
371
372

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

374
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
lain's avatar
lain committed
375
         %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
376
      object = fix_object(data["object"])
377

378
379
380
381
      params = %{
        to: data["to"],
        object: object,
        actor: user,
lain's avatar
lain committed
382
        context: object["conversation"],
383
384
        local: false,
        published: data["published"],
lain's avatar
lain committed
385
386
387
        additional:
          Map.take(data, [
            "cc",
388
            "directMessage",
lain's avatar
lain committed
389
390
            "id"
          ])
391
392
393
394
      }

      ActivityPub.create(params)
    else
lain's avatar
lain committed
395
      %Activity{} = activity -> {:ok, activity}
396
397
398
399
      _e -> :error
    end
  end

lain's avatar
lain committed
400
401
402
  def handle_incoming(
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
      ) do
403
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
404
405
         %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
406
      if not User.locked?(followed) do
kaniini's avatar
kaniini committed
407
408
        ActivityPub.accept(%{
          to: [follower.ap_id],
409
          actor: followed,
kaniini's avatar
kaniini committed
410
411
412
413
          object: data,
          local: true
        })

414
415
        User.follow(follower, followed)
      end
lain's avatar
lain committed
416

417
418
419
420
421
422
      {:ok, activity}
    else
      _e -> :error
    end
  end

423
  def handle_incoming(
Maksim's avatar
Maksim committed
424
        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
425
      ) do
426
427
    with actor <- get_actor(data),
         %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
428
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
429
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
430
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
lain's avatar
lain committed
431
432
433
434
         {:ok, activity} <-
           ActivityPub.accept(%{
             to: follow_activity.data["to"],
             type: "Accept",
435
             actor: followed,
lain's avatar
lain committed
436
437
438
             object: follow_activity.data["id"],
             local: false
           }) do
439
      if not User.following?(follower, followed) do
Maksim's avatar
Maksim committed
440
        {:ok, _follower} = User.follow(follower, followed)
441
      end
442

443
      {:ok, activity}
444
445
    else
      _e -> :error
446
447
448
449
    end
  end

  def handle_incoming(
Maksim's avatar
Maksim committed
450
        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
451
      ) do
452
453
    with actor <- get_actor(data),
         %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
454
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
455
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
456
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
lain's avatar
lain committed
457
         {:ok, activity} <-
458
           ActivityPub.reject(%{
lain's avatar
lain committed
459
             to: follow_activity.data["to"],
460
             type: "Reject",
461
             actor: followed,
lain's avatar
lain committed
462
463
464
             object: follow_activity.data["id"],
             local: false
           }) do
465
466
      User.unfollow(follower, followed)

467
      {:ok, activity}
468
469
    else
      _e -> :error
470
471
472
    end
  end

lain's avatar
lain committed
473
  def handle_incoming(
Maksim's avatar
Maksim committed
474
        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
lain's avatar
lain committed
475
      ) do
476
477
478
    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
479
         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
lain's avatar
lain committed
480
481
482
483
484
485
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
486
  def handle_incoming(
Maksim's avatar
Maksim committed
487
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
lain's avatar
lain committed
488
      ) do
489
490
491
    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),
492
493
         public <- ActivityPub.is_public?(data),
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
lain's avatar
lain committed
494
495
496
497
498
499
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
500
  def handle_incoming(
501
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
lain's avatar
lain committed
502
          data
503
504
      )
      when object_type in ["Person", "Application", "Service", "Organization"] do
lain's avatar
lain committed
505
506
507
508
    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"]
509
      locked = new_user_data[:info]["locked"] || false
lain's avatar
lain committed
510
511
512
513

      update_data =
        new_user_data
        |> Map.take([:name, :bio, :avatar])
lain's avatar
lain committed
514
        |> Map.put(:info, %{"banner" => banner, "locked" => locked})
lain's avatar
lain committed
515
516
517

      actor
      |> User.upgrade_changeset(update_data)
lain's avatar
lain committed
518
      |> User.update_and_set_cache()
lain's avatar
lain committed
519

lain's avatar
lain committed
520
521
522
523
524
525
526
      ActivityPub.update(%{
        local: false,
        to: data["to"] || [],
        cc: data["cc"] || [],
        object: object,
        actor: actor_id
      })
lain's avatar
lain committed
527
528
529
530
531
532
533
    else
      e ->
        Logger.error(e)
        :error
    end
  end

534
535
536
537
538
  # 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
539
  def handle_incoming(
540
        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
lain's avatar
lain committed
541
      ) do
lain's avatar
lain committed
542
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
543

544
    with actor <- get_actor(data),
545
         %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
546
         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
547
         :ok <- contain_origin(actor.ap_id, object.data),
lain's avatar
lain committed
548
549
550
         {:ok, activity} <- ActivityPub.delete(object, false) do
      {:ok, activity}
    else
feld's avatar
feld committed
551
      _e -> :error
lain's avatar
lain committed
552
553
554
    end
  end

555
  def handle_incoming(
556
557
        %{
          "type" => "Undo",
558
          "object" => %{"type" => "Announce", "object" => object_id},
Maksim's avatar
Maksim committed
559
          "actor" => _actor,
560
          "id" => id
561
        } = data
562
      ) do
563
564
565
    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),
566
         {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
567
568
      {:ok, activity}
    else
Thog's avatar
Thog committed
569
      _e -> :error
570
571
572
    end
  end

normandy's avatar
normandy committed
573
574
575
576
577
578
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Follow", "object" => followed},
          "actor" => follower,
          "id" => id
normandy's avatar
normandy committed
579
        } = _data
normandy's avatar
normandy committed
580
      ) do
normandy's avatar
normandy committed
581
582
    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
583
         {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
normandy's avatar
normandy committed
584
585
586
      User.unfollow(follower, followed)
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
587
      _e -> :error
normandy's avatar
normandy committed
588
589
590
    end
  end

normandy's avatar
normandy committed
591
592
593
594
595
596
597
598
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Block", "object" => blocked},
          "actor" => blocker,
          "id" => id
        } = _data
      ) do
href's avatar
href committed
599
    with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
600
         %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
normandy's avatar
normandy committed
601
602
         %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
normandy's avatar
normandy committed
603
      User.unblock(blocker, blocked)
normandy's avatar
normandy committed
604
605
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
606
      _e -> :error
normandy's avatar
normandy committed
607
608
609
    end
  end

610
  def handle_incoming(
Maksim's avatar
Maksim committed
611
        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
612
      ) do
href's avatar
href committed
613
    with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
614
         %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
normandy's avatar
normandy committed
615
         %User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
616
         {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
617
      User.unfollow(blocker, blocked)
618
      User.block(blocker, blocked)
normandy's avatar
normandy committed
619
620
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
621
      _e -> :error
normandy's avatar
normandy committed
622
623
    end
  end
624

Thog's avatar
Thog committed
625
626
627
628
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Like", "object" => object_id},
Maksim's avatar
Maksim committed
629
          "actor" => _actor,
Thog's avatar
Thog committed
630
          "id" => id
631
        } = data
Thog's avatar
Thog committed
632
      ) do
633
634
635
    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
636
637
638
         {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
      {:ok, activity}
    else
Thog's avatar
Thog committed
639
      _e -> :error
Thog's avatar
Thog committed
640
641
642
    end
  end

643
644
  def handle_incoming(_), do: :error

645
646
647
  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"])

648
  def get_obj_helper(id) do
649
    if object = Object.normalize(id), do: {:ok, object}, else: nil
650
651
  end

652
  def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) when is_binary(inReplyTo) do
653
654
655
656
657
658
659
    with false <- String.starts_with?(inReplyTo, "http"),
         {:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
      Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
    else
      _e -> object
    end
  end
lain's avatar
lain committed
660

661
662
663
  def set_reply_to_uri(obj), do: obj

  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
664
665
  def prepare_object(object) do
    object
lain's avatar
lain committed
666
    |> set_sensitive
lain's avatar
lain committed
667
    |> add_hashtags
lain's avatar
lain committed
668
    |> add_mention_tags
lain's avatar
lain committed
669
    |> add_emoji_tags
lain's avatar
lain committed
670
    |> add_attributed_to
671
    |> add_likes
lain's avatar
lain committed
672
    |> prepare_attachments
lain's avatar
lain committed
673
    |> set_conversation
674
    |> set_reply_to_uri
675
676
    |> strip_internal_fields
    |> strip_internal_tags
lain's avatar
lain committed
677
678
  end

feld's avatar
feld committed
679
680
681
682
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
683

684
  def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do
lain's avatar
lain committed
685
686
687
688
689
690
691
    object =
      object
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
692
      |> Map.merge(Utils.make_json_ld_header())
lain's avatar
lain committed
693
694
695
696

    {:ok, data}
  end

kaniini's avatar
kaniini committed
697
698
699
  # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  # because of course it does.
  def prepare_outgoing(%{"type" => "Accept"} = data) do
700
    with follow_activity <- Activity.normalize(data["object"]) do
kaniini's avatar
kaniini committed
701
702
703
704
705
706
707
708
709
710
      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
711
        |> Map.merge(Utils.make_json_ld_header())
kaniini's avatar
kaniini committed
712
713
714
715
716

      {:ok, data}
    end
  end

717
  def prepare_outgoing(%{"type" => "Reject"} = data) do
718
    with follow_activity <- Activity.normalize(data["object"]) do
719
720
721
722
723
724
725
726
727
728
      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
729
        |> Map.merge(Utils.make_json_ld_header())
730
731
732
733
734

      {:ok, data}
    end
  end

feld's avatar
feld committed
735
  def prepare_outgoing(%{"type" => _type} = data) do
lain's avatar
lain committed
736
737
738
    data =
      data
      |> maybe_fix_object_url
lain's avatar
lain committed
739
      |> Map.merge(Utils.make_json_ld_header())
740
741
742
743

    {:ok, data}
  end

744
745
  def maybe_fix_object_url(data) do
    if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
746
      case fetch_obj_helper(data["object"]) do
747
748
        {:ok, relative_object} ->
          if relative_object.data["external_url"] do
feld's avatar
feld committed
749
            _data =
lain's avatar
lain committed
750
751
              data
              |> Map.put("object", relative_object.data["external_url"])
752
753
754
          else
            data
          end
lain's avatar
lain committed
755

756
757
758
759
760
761
762
763
764
        e ->
          Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
          data
      end
    else
      data
    end
  end

lain's avatar
lain committed
765
  def add_hashtags(object) do
lain's avatar
lain committed
766
767
    tags =
      (object["tag"] || [])
768
769
770
771
772
773
774
775
776
777
778
779
      |> 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
780
      end)
lain's avatar
lain committed
781
782
783
784
785

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

lain's avatar
lain committed
786
  def add_mention_tags(object) do
lain's avatar
lain committed
787
    mentions =
788
789
      object
      |> Utils.get_notified_from_object()
lain's avatar
lain committed
790
791
792
      |> Enum.map(fn user ->
        %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
      end)
lain's avatar
lain committed
793

lain's avatar
lain committed
794
    tags = object["tag"] || []
lain's avatar
lain committed
795
796

    object
lain's avatar
lain committed
797
    |> Map.put("tag", tags ++ mentions)
lain's avatar
lain committed
798
799
  end

lain's avatar
lain committed
800
801
802
803
  # 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
804
805
806
807
808
809
810
811
812
813
814
815

    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
816
817
818
819
820

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

lain's avatar
lain committed
821
822
823
824
  def set_conversation(object) do
    Map.put(object, "conversation", object["context"])
  end

lain's avatar
lain committed
825
826
827
828
829
  def set_sensitive(object) do
    tags = object["tag"] || []
    Map.put(object, "sensitive", "nsfw" in tags)
  end

lain's avatar
lain committed
830
831
832
833
834
  def add_attributed_to(object) do
    attributedTo = object["attributedTo"] || object["actor"]

    object
    |> Map.put("attributedTo", attributedTo)
835
  end
lain's avatar
lain committed
836

837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
  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
853
  def prepare_attachments(object) do
lain's avatar
lain committed
854
855
856
857
858
859
    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
860
861
862
863

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

865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
  defp strip_internal_fields(object) do
    object
    |> Map.drop([
      "like_count",
      "announcements",
      "announcement_count",
      "emoji",
      "context_id"
    ])
  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
887
888
  defp user_upgrade_task(user) do
    old_follower_address = User.ap_followers(user)
lain's avatar
lain committed
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906

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

909
910
    maybe_retire_websub(user.ap_id)

lain's avatar
lain committed
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
    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
928
929
930
931
    Repo.update_all(q, [])
  end

  def upgrade_user_from_ap_id(ap_id, async \\ true) do
932
    with %User{local: false} = user <- User.get_by_ap_id(ap_id),
lain's avatar
lain committed
933
         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
lain's avatar
lain committed
934
      already_ap = User.ap_enabled?(user)
lain's avatar
lain committed
935
936
937
938

      {:ok, user} =
        User.upgrade_changeset(user, data)
        |> Repo.update()
lain's avatar
lain committed
939

lain's avatar
lain committed
940
941
942
943
944
945
946
      if !already_ap do
        # This could potentially take a long time, do it in the background
        if async do
          Task.start(fn ->
            user_upgrade_task(user)
          end)
        else
lain's avatar
lain committed
947
          user_upgrade_task(user)
lain's avatar
lain committed
948
        end
lain's avatar
lain committed
949
      end
lain's avatar
lain committed
950
951
952
953
954
955

      {:ok, user}
    else
      e -> e
    end
  end
956
957
958

  def maybe_retire_websub(ap_id) do
    # some sanity checks
lain's avatar
lain committed
959
960
961
962
963
964
965
    if is_binary(ap_id) && String.length(ap_id) > 8 do
      q =
        from(
          ws in Pleroma.Web.Websub.WebsubClientSubscription,
          where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
        )

966
967
968
      Repo.delete_all(q)
    end
  end
969
970
971

  def maybe_fix_user_url(data) do
    if is_map(data["url"]) do
Thog's avatar
Thog committed
972
973
974
      Map.put(data, "url", data["url"]["href"])
    else
      data
975
976
977
978
979
980
981
    end
  end

  def maybe_fix_user_object(data) do
    data
    |> maybe_fix_user_url
  end
982
end