transmogrifier.ex 24.5 KB
Newer Older
1
2
3
4
5
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
  @moduledoc """
  A module to handle coding from internal to wire ActivityPub and back.
  """
  alias Pleroma.User
lain's avatar
lain committed
6
  alias Pleroma.Object
7
  alias Pleroma.Activity
lain's avatar
lain committed
8
  alias Pleroma.Repo
9
  alias Pleroma.Web.ActivityPub.ActivityPub
10
  alias Pleroma.Web.ActivityPub.Utils
11

lain's avatar
lain committed
12
13
  import Ecto.Query

14
15
  require Logger

16
17
18
19
20
  def get_actor(%{"actor" => actor}) when is_binary(actor) do
    actor
  end

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

29
30
  def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
    id
31
32
  end

33
34
35
36
  def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
    get_actor(%{"actor" => actor})
  end

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

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

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

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

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

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

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

  def fix_addressing(map) do
    map
    |> fix_addressing_list("to")
    |> fix_addressing_list("cc")
    |> fix_addressing_list("bto")
    |> fix_addressing_list("bcc")
lain's avatar
lain committed
98
99
  end

100
101
  def fix_actor(%{"attributedTo" => actor} = object) do
    object
102
    |> Map.put("actor", get_actor(%{"actor" => actor}))
103
104
  end

105
106
107
108
109
110
111
112
113
114
115
116
117
118
  def fix_likes(%{"likes" => likes} = object)
      when is_bitstring(likes) do
    # Check for standardisation
    # This is what Peertube does
    # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
    object
    |> Map.put("likes", [])
    |> Map.put("like_count", 0)
  end

  def fix_likes(object) do
    object
  end

119
120
121
  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
      when not is_nil(in_reply_to) do
    in_reply_to_id =
122
      cond do
123
124
125
126
127
128
129
130
131
        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)

132
        # Maybe I should output an error too?
133
134
        true ->
          ""
135
136
      end

137
    case fetch_obj_helper(in_reply_to_id) do
lain's avatar
lain committed
138
      {:ok, replied_object} ->
139
140
141
142
143
144
145
146
147
148
        with %Activity{} = activity <-
               Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do
          object
          |> Map.put("inReplyTo", replied_object.data["id"])
          |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
          |> Map.put("inReplyToStatusId", activity.id)
          |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
          |> Map.put("context", replied_object.data["context"] || object["conversation"])
        else
          e ->
149
            Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
150
151
            object
        end
lain's avatar
lain committed
152

lain's avatar
lain committed
153
      e ->
154
        Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
lain's avatar
lain committed
155
156
157
        object
    end
  end
lain's avatar
lain committed
158

lain's avatar
lain committed
159
160
  def fix_in_reply_to(object), do: object

lain's avatar
lain committed
161
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
162
163
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

lain's avatar
lain committed
164
    object
Haelwenn's avatar
Haelwenn committed
165
166
    |> Map.put("context", context)
    |> Map.put("conversation", context)
lain's avatar
lain committed
167
168
  end

169
  def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
lain's avatar
lain committed
170
    attachments =
171
      attachment
lain's avatar
lain committed
172
173
174
175
      |> Enum.map(fn data ->
        url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
        Map.put(data, "url", url)
      end)
lain's avatar
lain committed
176
177
178

    object
    |> Map.put("attachment", attachments)
179
180
  end

181
  def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
182
183
    Map.put(object, "attachment", [attachment])
    |> fix_attachments()
184
185
  end

186
  def fix_attachments(object), do: object
187

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
  def fix_url(%{"url" => url} = object) when is_map(url) do
    object
    |> Map.put("url", url["href"])
  end

  def fix_url(%{"url" => url} = object) when is_list(url) do
    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

203
204
205
206
207
208
209
210
    if Map.get(object, "type") == "Video" do
      object
      |> Map.delete("url")
      |> Map.put("attachment", url_string)
    else
      object
      |> Map.put("url", url_string)
    end
211
212
213
214
  end

  def fix_url(object), do: object

215
  def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
lain's avatar
lain committed
216
217
218
219
220
    emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)

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

lain's avatar
lain committed
223
224
        mapping |> Map.put(name, data["icon"]["url"])
      end)
lain's avatar
lain committed
225
226
227
228
229
230
231
232

    # 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

233
234
235
236
237
238
239
240
  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

241
  def fix_emoji(object), do: object
242

243
  def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
lain's avatar
lain committed
244
    tags =
245
      tag
lain's avatar
lain committed
246
247
      |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
      |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
248

249
    combined = tag ++ tags
250
251
252
253
254

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

255
256
  def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
    combined = [tag, String.slice(hashtag, 1..-1)]
257
258
259
260
261

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

262
  def fix_tag(object), do: object
263

264
265
266
267
268
269
270
271
272
273
274
  # 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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
  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

301
302
303
304
305
306
  # 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

307
308
309
  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
kaniini's avatar
kaniini committed
310
  def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
311
      when objtype in ["Article", "Note", "Video", "Page"] do
312
    actor = get_actor(data)
313
314
315
316

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

lain's avatar
lain committed
318
319
    with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
         %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
320
      object = fix_object(data["object"])
321

322
323
324
325
      params = %{
        to: data["to"],
        object: object,
        actor: user,
lain's avatar
lain committed
326
        context: object["conversation"],
327
328
        local: false,
        published: data["published"],
lain's avatar
lain committed
329
330
331
332
333
        additional:
          Map.take(data, [
            "cc",
            "id"
          ])
334
335
336
337
      }

      ActivityPub.create(params)
    else
lain's avatar
lain committed
338
      %Activity{} = activity -> {:ok, activity}
339
340
341
342
      _e -> :error
    end
  end

lain's avatar
lain committed
343
344
345
  def handle_incoming(
        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
      ) do
346
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
347
348
         %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
349
      if not User.locked?(followed) do
kaniini's avatar
kaniini committed
350
351
352
353
354
355
356
        ActivityPub.accept(%{
          to: [follower.ap_id],
          actor: followed.ap_id,
          object: data,
          local: true
        })

357
358
        User.follow(follower, followed)
      end
lain's avatar
lain committed
359

360
361
362
363
364
365
      {:ok, activity}
    else
      _e -> :error
    end
  end

366
  def handle_incoming(
Maksim's avatar
Maksim committed
367
        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
368
      ) do
369
370
    with actor <- get_actor(data),
         %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
371
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
372
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
373
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
lain's avatar
lain committed
374
375
376
377
378
379
380
381
         {:ok, activity} <-
           ActivityPub.accept(%{
             to: follow_activity.data["to"],
             type: "Accept",
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             local: false
           }) do
382
      if not User.following?(follower, followed) do
Maksim's avatar
Maksim committed
383
        {:ok, _follower} = User.follow(follower, followed)
384
      end
385

386
      {:ok, activity}
387
388
    else
      _e -> :error
389
390
391
392
    end
  end

  def handle_incoming(
Maksim's avatar
Maksim committed
393
        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
394
      ) do
395
396
    with actor <- get_actor(data),
         %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
397
         {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
398
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
399
         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
lain's avatar
lain committed
400
401
402
403
404
405
406
407
         {:ok, activity} <-
           ActivityPub.accept(%{
             to: follow_activity.data["to"],
             type: "Accept",
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             local: false
           }) do
408
409
      User.unfollow(follower, followed)

410
      {:ok, activity}
411
412
    else
      _e -> :error
413
414
415
    end
  end

lain's avatar
lain committed
416
  def handle_incoming(
Maksim's avatar
Maksim committed
417
        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
lain's avatar
lain committed
418
      ) do
419
420
421
    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
422
         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
lain's avatar
lain committed
423
424
425
426
427
428
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
429
  def handle_incoming(
Maksim's avatar
Maksim committed
430
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
lain's avatar
lain committed
431
      ) do
432
433
434
    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
435
         {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
lain's avatar
lain committed
436
437
438
439
440
441
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
442
  def handle_incoming(
443
        %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
lain's avatar
lain committed
444
          data
445
446
      )
      when object_type in ["Person", "Application", "Service", "Organization"] do
lain's avatar
lain committed
447
448
449
450
    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"]
451
      locked = new_user_data[:info]["locked"] || false
lain's avatar
lain committed
452
453
454
455

      update_data =
        new_user_data
        |> Map.take([:name, :bio, :avatar])
lain's avatar
lain committed
456
        |> Map.put(:info, %{"banner" => banner, "locked" => locked})
lain's avatar
lain committed
457
458
459

      actor
      |> User.upgrade_changeset(update_data)
lain's avatar
lain committed
460
      |> User.update_and_set_cache()
lain's avatar
lain committed
461

lain's avatar
lain committed
462
463
464
465
466
467
468
      ActivityPub.update(%{
        local: false,
        to: data["to"] || [],
        cc: data["cc"] || [],
        object: object,
        actor: actor_id
      })
lain's avatar
lain committed
469
470
471
472
473
474
475
    else
      e ->
        Logger.error(e)
        :error
    end
  end

476
477
478
479
480
  # 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
481
  def handle_incoming(
482
        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
lain's avatar
lain committed
483
      ) do
lain's avatar
lain committed
484
    object_id = Utils.get_ap_id(object_id)
lain's avatar
lain committed
485

486
    with actor <- get_actor(data),
487
         %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
488
         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
489
         :ok <- contain_origin(actor.ap_id, object.data),
lain's avatar
lain committed
490
491
492
         {:ok, activity} <- ActivityPub.delete(object, false) do
      {:ok, activity}
    else
feld's avatar
feld committed
493
      _e -> :error
lain's avatar
lain committed
494
495
496
    end
  end

497
  def handle_incoming(
498
499
        %{
          "type" => "Undo",
500
          "object" => %{"type" => "Announce", "object" => object_id},
Maksim's avatar
Maksim committed
501
          "actor" => _actor,
502
          "id" => id
503
        } = data
504
      ) do
505
506
507
    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),
508
         {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
509
510
      {:ok, activity}
    else
Thog's avatar
Thog committed
511
      _e -> :error
512
513
514
    end
  end

normandy's avatar
normandy committed
515
516
517
518
519
520
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Follow", "object" => followed},
          "actor" => follower,
          "id" => id
normandy's avatar
normandy committed
521
        } = _data
normandy's avatar
normandy committed
522
      ) do
normandy's avatar
normandy committed
523
524
    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
525
         {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
normandy's avatar
normandy committed
526
527
528
      User.unfollow(follower, followed)
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
529
      _e -> :error
normandy's avatar
normandy committed
530
531
532
    end
  end

normandy's avatar
normandy committed
533
534
535
536
537
538
539
540
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Block", "object" => blocked},
          "actor" => blocker,
          "id" => id
        } = _data
      ) do
href's avatar
href committed
541
    with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
542
         %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
normandy's avatar
normandy committed
543
544
         %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
         {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
normandy's avatar
normandy committed
545
      User.unblock(blocker, blocked)
normandy's avatar
normandy committed
546
547
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
548
      _e -> :error
normandy's avatar
normandy committed
549
550
551
    end
  end

552
  def handle_incoming(
Maksim's avatar
Maksim committed
553
        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
554
      ) do
href's avatar
href committed
555
    with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
556
         %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
normandy's avatar
normandy committed
557
         %User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
normandy's avatar
normandy committed
558
         {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
559
      User.unfollow(blocker, blocked)
560
      User.block(blocker, blocked)
normandy's avatar
normandy committed
561
562
      {:ok, activity}
    else
Maksim's avatar
Maksim committed
563
      _e -> :error
normandy's avatar
normandy committed
564
565
    end
  end
566

Thog's avatar
Thog committed
567
568
569
570
  def handle_incoming(
        %{
          "type" => "Undo",
          "object" => %{"type" => "Like", "object" => object_id},
Maksim's avatar
Maksim committed
571
          "actor" => _actor,
Thog's avatar
Thog committed
572
          "id" => id
573
        } = data
Thog's avatar
Thog committed
574
      ) do
575
576
577
    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
578
579
580
         {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
      {:ok, activity}
    else
Thog's avatar
Thog committed
581
      _e -> :error
Thog's avatar
Thog committed
582
583
584
    end
  end

585
586
  def handle_incoming(_), do: :error

587
588
589
  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"])

590
  def get_obj_helper(id) do
591
    if object = Object.normalize(id), do: {:ok, object}, else: nil
592
593
  end

594
595
596
597
598
599
600
601
  def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
    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
602

603
604
605
  def set_reply_to_uri(obj), do: obj

  # Prepares the object of an outgoing create activity.
lain's avatar
lain committed
606
607
  def prepare_object(object) do
    object
lain's avatar
lain committed
608
    |> set_sensitive
lain's avatar
lain committed
609
    |> add_hashtags
lain's avatar
lain committed
610
    |> add_mention_tags
lain's avatar
lain committed
611
    |> add_emoji_tags
lain's avatar
lain committed
612
    |> add_attributed_to
lain's avatar
lain committed
613
    |> prepare_attachments
lain's avatar
lain committed
614
    |> set_conversation
615
    |> set_reply_to_uri
616
617
    |> strip_internal_fields
    |> strip_internal_tags
lain's avatar
lain committed
618
619
  end

feld's avatar
feld committed
620
621
622
623
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
624

lain's avatar
lain committed
625
  def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
lain's avatar
lain committed
626
627
628
629
630
631
632
    object =
      object
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
633
      |> Map.merge(Utils.make_json_ld_header())
lain's avatar
lain committed
634
635
636
637

    {:ok, data}
  end

kaniini's avatar
kaniini committed
638
639
640
  # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  # because of course it does.
  def prepare_outgoing(%{"type" => "Accept"} = data) do
641
    with follow_activity <- Activity.normalize(data["object"]) do
kaniini's avatar
kaniini committed
642
643
644
645
646
647
648
649
650
651
      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
652
        |> Map.merge(Utils.make_json_ld_header())
kaniini's avatar
kaniini committed
653
654
655
656
657

      {:ok, data}
    end
  end

658
  def prepare_outgoing(%{"type" => "Reject"} = data) do
659
    with follow_activity <- Activity.normalize(data["object"]) do
660
661
662
663
664
665
666
667
668
669
      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
670
        |> Map.merge(Utils.make_json_ld_header())
671
672
673
674
675

      {:ok, data}
    end
  end

feld's avatar
feld committed
676
  def prepare_outgoing(%{"type" => _type} = data) do
lain's avatar
lain committed
677
678
679
    data =
      data
      |> maybe_fix_object_url
lain's avatar
lain committed
680
      |> Map.merge(Utils.make_json_ld_header())
681
682
683
684

    {:ok, data}
  end

685
686
  def maybe_fix_object_url(data) do
    if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
687
      case fetch_obj_helper(data["object"]) do
688
689
        {:ok, relative_object} ->
          if relative_object.data["external_url"] do
feld's avatar
feld committed
690
            _data =
lain's avatar
lain committed
691
692
              data
              |> Map.put("object", relative_object.data["external_url"])
693
694
695
          else
            data
          end
lain's avatar
lain committed
696

697
698
699
700
701
702
703
704
705
        e ->
          Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
          data
      end
    else
      data
    end
  end

lain's avatar
lain committed
706
  def add_hashtags(object) do
lain's avatar
lain committed
707
708
709
710
711
712
713
714
715
    tags =
      (object["tag"] || [])
      |> Enum.map(fn tag ->
        %{
          "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
          "name" => "##{tag}",
          "type" => "Hashtag"
        }
      end)
lain's avatar
lain committed
716
717
718
719
720

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

lain's avatar
lain committed
721
  def add_mention_tags(object) do
lain's avatar
lain committed
722
    mentions =
723
724
      object
      |> Utils.get_notified_from_object()
lain's avatar
lain committed
725
726
727
      |> Enum.map(fn user ->
        %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
      end)
lain's avatar
lain committed
728

lain's avatar
lain committed
729
    tags = object["tag"] || []
lain's avatar
lain committed
730
731

    object
lain's avatar
lain committed
732
    |> Map.put("tag", tags ++ mentions)
lain's avatar
lain committed
733
734
  end

lain's avatar
lain committed
735
736
737
738
  # 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
739
740
741
742
743
744
745
746
747
748
749
750

    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
751
752
753
754
755

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

lain's avatar
lain committed
756
757
758
759
  def set_conversation(object) do
    Map.put(object, "conversation", object["context"])
  end

lain's avatar
lain committed
760
761
762
763
764
  def set_sensitive(object) do
    tags = object["tag"] || []
    Map.put(object, "sensitive", "nsfw" in tags)
  end

lain's avatar
lain committed
765
766
767
768
769
  def add_attributed_to(object) do
    attributedTo = object["attributedTo"] || object["actor"]

    object
    |> Map.put("attributedTo", attributedTo)
770
  end
lain's avatar
lain committed
771
772

  def prepare_attachments(object) do
lain's avatar
lain committed
773
774
775
776
777
778
    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
779
780
781
782

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

784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
  defp strip_internal_fields(object) do
    object
    |> Map.drop([
      "likes",
      "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
807
808
  defp user_upgrade_task(user) do
    old_follower_address = User.ap_followers(user)
lain's avatar
lain committed
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826

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

829
830
    maybe_retire_websub(user.ap_id)

lain's avatar
lain committed
831
    # Only do this for recent activties, don't go through the whole db.
lain's avatar
lain committed
832
833
    # Only look at the last 1000 activities.
    since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
lain's avatar
lain committed
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852

    q =
      from(
        a in Activity,
        where: ^old_follower_address in a.recipients,
        where: a.id > ^since,
        update: [
          set: [
            recipients:
              fragment(
                "array_replace(?,?,?)",
                a.recipients,
                ^old_follower_address,
                ^user.follower_address
              )
          ]
        ]
      )

lain's avatar
lain committed
853
854
855
856
    Repo.update_all(q, [])
  end

  def upgrade_user_from_ap_id(ap_id, async \\ true) do
857
    with %User{local: false} = user <- User.get_by_ap_id(ap_id),
lain's avatar
lain committed
858
         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
lain's avatar
lain committed
859
      already_ap = User.ap_enabled?(user)
lain's avatar
lain committed
860
861
862
863

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

lain's avatar
lain committed
865
866
867
868
869
870
871
      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
872
          user_upgrade_task(user)
lain's avatar
lain committed
873
        end
lain's avatar
lain committed
874
      end
lain's avatar
lain committed
875
876
877
878
879
880

      {:ok, user}
    else
      e -> e
    end
  end
881
882
883

  def maybe_retire_websub(ap_id) do
    # some sanity checks
lain's avatar
lain committed
884
885
886
887
888
889
890
    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}%")
        )

891
892
893
      Repo.delete_all(q)
    end
  end
894
895
896

  def maybe_fix_user_url(data) do
    if is_map(data["url"]) do
Thog's avatar
Thog committed
897
898
899
      Map.put(data, "url", data["url"]["href"])
    else
      data
900
901
902
903
904
905
906
    end
  end

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