transmogrifier.ex 26.2 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.
  """
9
10
  alias Pleroma.{Activity, User, Object, Repo}
  alias Pleroma.Web.ActivityPub.{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
    |> 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

92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
  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

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

115
  def fix_explicit_addressing(object) do
116
117
118
119
120
121
122
123
    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
124
125
  end

126
127
  def fix_addressing(object) do
    object
128
129
130
131
    |> fix_addressing_list("to")
    |> fix_addressing_list("cc")
    |> fix_addressing_list("bto")
    |> fix_addressing_list("bcc")
132
    |> fix_explicit_addressing
lain's avatar
lain committed
133
134
  end

135
136
  def fix_actor(%{"attributedTo" => actor} = object) do
    object
137
    |> Map.put("actor", get_actor(%{"actor" => actor}))
138
139
  end

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

  def fix_likes(object) do
    object
  end

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

167
        # Maybe I should output an error too?
168
169
        true ->
          ""
170
171
      end

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

lain's avatar
lain committed
188
      e ->
189
        Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
lain's avatar
lain committed
190
191
192
        object
    end
  end
lain's avatar
lain committed
193

lain's avatar
lain committed
194
195
  def fix_in_reply_to(object), do: object

lain's avatar
lain committed
196
  def fix_context(object) do
Haelwenn's avatar
Haelwenn committed
197
198
    context = object["context"] || object["conversation"] || Utils.generate_context_id()

lain's avatar
lain committed
199
    object
Haelwenn's avatar
Haelwenn committed
200
201
    |> Map.put("context", context)
    |> Map.put("conversation", context)
lain's avatar
lain committed
202
203
  end

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

    object
    |> Map.put("attachment", attachments)
220
221
  end

222
  def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
223
224
    Map.put(object, "attachment", [attachment])
    |> fix_attachments()
225
226
  end

227
  def fix_attachments(object), do: object
228

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

234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
  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
250
251
252
253
254
255
256
257
258
    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

259
260
    object
    |> Map.put("url", url_string)
261
262
263
264
  end

  def fix_url(object), do: object

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

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

lain's avatar
lain committed
273
274
        mapping |> Map.put(name, data["icon"]["url"])
      end)
lain's avatar
lain committed
275
276
277
278
279
280
281
282

    # 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

283
284
285
286
287
288
289
290
  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

291
  def fix_emoji(object), do: object
292

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

299
    combined = tag ++ tags
300
301
302
303
304

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

305
306
  def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
    combined = [tag, String.slice(hashtag, 1..-1)]
307
308
309
310
311

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

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

314
  def fix_tag(object), do: object
315

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

353
354
355
356
357
358
  # 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

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

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

370
    with nil <- Activity.get_create_by_object_ap_id(object["id"]),
lain's avatar
lain committed
371
         %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
372
      object = fix_object(data["object"])
373

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

      ActivityPub.create(params)
    else
lain's avatar
lain committed
391
      %Activity{} = activity -> {:ok, activity}
392
393
394
395
      _e -> :error
    end
  end

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

410
411
        User.follow(follower, followed)
      end
lain's avatar
lain committed
412

413
414
415
416
417
418
      {:ok, activity}
    else
      _e -> :error
    end
  end

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

439
      {:ok, activity}
440
441
    else
      _e -> :error
442
443
444
445
    end
  end

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

463
      {:ok, activity}
464
465
    else
      _e -> :error
466
467
468
    end
  end

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

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

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

      update_data =
        new_user_data
        |> Map.take([:name, :bio, :avatar])
lain's avatar
lain committed
510
        |> Map.put(:info, %{"banner" => banner, "locked" => locked})
lain's avatar
lain committed
511
512
513

      actor
      |> User.upgrade_changeset(update_data)
lain's avatar
lain committed
514
      |> User.update_and_set_cache()
lain's avatar
lain committed
515

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

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

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

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

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

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

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

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

639
640
  def handle_incoming(_), do: :error

641
642
643
  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"])

644
  def get_obj_helper(id) do
645
    if object = Object.normalize(id), do: {:ok, object}, else: nil
646
647
  end

648
649
650
651
652
653
654
655
  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
656

657
658
659
  def set_reply_to_uri(obj), do: obj

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

feld's avatar
feld committed
675
676
677
678
  #  @doc
  #  """
  #  internal -> Mastodon
  #  """
lain's avatar
lain committed
679

680
  def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do
lain's avatar
lain committed
681
682
683
684
685
686
687
    object =
      object
      |> prepare_object

    data =
      data
      |> Map.put("object", object)
lain's avatar
lain committed
688
      |> Map.merge(Utils.make_json_ld_header())
lain's avatar
lain committed
689
690
691
692

    {:ok, data}
  end

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

      {:ok, data}
    end
  end

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

      {:ok, data}
    end
  end

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

    {:ok, data}
  end

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

752
753
754
755
756
757
758
759
760
        e ->
          Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
          data
      end
    else
      data
    end
  end

lain's avatar
lain committed
761
  def add_hashtags(object) do
lain's avatar
lain committed
762
763
764
765
766
767
768
769
770
    tags =
      (object["tag"] || [])
      |> Enum.map(fn tag ->
        %{
          "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
          "name" => "##{tag}",
          "type" => "Hashtag"
        }
      end)
lain's avatar
lain committed
771
772
773
774
775

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

lain's avatar
lain committed
776
  def add_mention_tags(object) do
lain's avatar
lain committed
777
    mentions =
778
779
      object
      |> Utils.get_notified_from_object()
lain's avatar
lain committed
780
781
782
      |> Enum.map(fn user ->
        %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
      end)
lain's avatar
lain committed
783

lain's avatar
lain committed
784
    tags = object["tag"] || []
lain's avatar
lain committed
785
786

    object
lain's avatar
lain committed
787
    |> Map.put("tag", tags ++ mentions)
lain's avatar
lain committed
788
789
  end

lain's avatar
lain committed
790
791
792
793
  # 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
794
795
796
797
798
799
800
801
802
803
804
805

    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
806
807
808
809
810

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

lain's avatar
lain committed
811
812
813
814
  def set_conversation(object) do
    Map.put(object, "conversation", object["context"])
  end

lain's avatar
lain committed
815
816
817
818
819
  def set_sensitive(object) do
    tags = object["tag"] || []
    Map.put(object, "sensitive", "nsfw" in tags)
  end

lain's avatar
lain committed
820
821
822
823
824
  def add_attributed_to(object) do
    attributedTo = object["attributedTo"] || object["actor"]

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

827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
  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
843
  def prepare_attachments(object) do
lain's avatar
lain committed
844
845
846
847
848
849
    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
850
851
852
853

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

855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
  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
877
878
  defp user_upgrade_task(user) do
    old_follower_address = User.ap_followers(user)
lain's avatar
lain committed
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896

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

899
900
    maybe_retire_websub(user.ap_id)

lain's avatar
lain committed
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
    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
918
919
920
921
    Repo.update_all(q, [])
  end

  def upgrade_user_from_ap_id(ap_id, async \\ true) do
922
    with %User{local: false} = user <- User.get_by_ap_id(ap_id),
lain's avatar
lain committed
923
         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
lain's avatar
lain committed
924
      already_ap = User.ap_enabled?(user)
lain's avatar
lain committed
925
926
927
928

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

lain's avatar
lain committed
930
931
932
933
934
935
936
      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
937
          user_upgrade_task(user)
lain's avatar
lain committed
938
        end
lain's avatar
lain committed
939
      end
lain's avatar
lain committed
940
941
942
943
944
945

      {:ok, user}
    else
      e -> e
    end
  end
946
947
948

  def maybe_retire_websub(ap_id) do
    # some sanity checks
lain's avatar
lain committed
949
950
951
952
953
954
955
    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}%")
        )

956
957
958
      Repo.delete_all(q)
    end
  end
959
960
961

  def maybe_fix_user_url(data) do
    if is_map(data["url"]) do
Thog's avatar
Thog committed
962
963
964
      Map.put(data, "url", data["url"]["href"])
    else
      data
965
966
967
968
969
970
971
    end
  end

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