utils.ex 32.7 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

lain's avatar
lain committed
5
defmodule Pleroma.Web.ActivityPub.Utils do
6 7 8 9 10
  alias Ecto.Changeset
  alias Ecto.UUID
  alias Pleroma.Activity
  alias Pleroma.Notification
  alias Pleroma.Object
Haelwenn's avatar
Haelwenn committed
11
  alias Pleroma.Repo
12
  alias Pleroma.User
Haelwenn's avatar
Haelwenn committed
13
  alias Pleroma.Web
14
  alias Pleroma.Web.ActivityPub.ActivityPub
15
  alias Pleroma.Web.ActivityPub.Visibility
16
  alias Pleroma.Web.AdminAPI.AccountView
lain's avatar
lain committed
17
  alias Pleroma.Web.Endpoint
18
  alias Pleroma.Web.Router.Helpers
Haelwenn's avatar
Haelwenn committed
19

lain's avatar
lain committed
20
  import Ecto.Query
Haelwenn's avatar
Haelwenn committed
21

kaniini's avatar
kaniini committed
22
  require Logger
23
  require Pleroma.Constants
lain's avatar
lain committed
24

25 26 27 28 29 30 31 32 33 34
  @supported_object_types [
    "Article",
    "Note",
    "Event",
    "Video",
    "Page",
    "Question",
    "Answer",
    "Audio"
  ]
35
  @strip_status_report_states ~w(closed resolved)
Sergey Suprunenko's avatar
Sergey Suprunenko committed
36 37
  @supported_report_states ~w(open closed resolved)
  @valid_visibilities ~w(public unlisted private direct)
38

39 40
  # Some implementations send the actor URI as the actor field, others send the entire actor object,
  # so figure out what the actor's URI is based on what we have.
Maksim's avatar
Maksim committed
41 42
  def get_ap_id(%{"id" => id} = _), do: id
  def get_ap_id(id), do: id
43 44

  def normalize_params(params) do
lain's avatar
lain committed
45
    Map.put(params, "actor", get_ap_id(params["actor"]))
46 47
  end

Maksim's avatar
Maksim committed
48 49 50 51 52 53
  @spec determine_explicit_mentions(map()) :: map()
  def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do
    Enum.flat_map(tag, fn
      %{"type" => "Mention", "href" => href} -> [href]
      _ -> []
    end)
54 55 56
  end

  def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
Maksim's avatar
Maksim committed
57 58
    object
    |> Map.put("tag", [tag])
59 60 61 62 63
    |> determine_explicit_mentions()
  end

  def determine_explicit_mentions(_), do: []

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
  @spec label_in_collection?(any(), any()) :: boolean()
  defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
  defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
  defp label_in_collection?(_, _), do: false

  @spec label_in_message?(String.t(), map()) :: boolean()
  def label_in_message?(label, params),
    do:
      [params["to"], params["cc"], params["bto"], params["bcc"]]
      |> Enum.any?(&label_in_collection?(label, &1))

  @spec unaddressed_message?(map()) :: boolean()
  def unaddressed_message?(params),
    do:
      [params["to"], params["cc"], params["bto"], params["bcc"]]
      |> Enum.all?(&is_nil(&1))
80

Maksim's avatar
Maksim committed
81
  @spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
82 83 84 85
  def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
    do:
      label_in_message?(ap_id, params) || unaddressed_message?(params) ||
        User.following?(recipient, actor)
86 87 88 89 90 91

  defp extract_list(target) when is_binary(target), do: [target]
  defp extract_list(lst) when is_list(lst), do: lst
  defp extract_list(_), do: []

  def maybe_splice_recipient(ap_id, params) do
92
    need_splice? =
93 94
      !label_in_collection?(ap_id, params["to"]) &&
        !label_in_collection?(ap_id, params["cc"])
95

96 97 98
    if need_splice? do
      cc_list = extract_list(params["cc"])
      Map.put(params, "cc", [ap_id | cc_list])
99 100 101 102 103
    else
      params
    end
  end

104 105 106 107
  def make_json_ld_header do
    %{
      "@context" => [
        "https://www.w3.org/ns/activitystreams",
Alexander Strizhakov's avatar
Alexander Strizhakov committed
108 109 110 111
        "#{Web.base_url()}/schemas/litepub-0.1.jsonld",
        %{
          "@language" => "und"
        }
112 113 114 115
      ]
    }
  end

lain's avatar
lain committed
116
  def make_date do
lain's avatar
lain committed
117
    DateTime.utc_now() |> DateTime.to_iso8601()
lain's avatar
lain committed
118 119 120 121 122 123 124 125 126 127 128
  end

  def generate_activity_id do
    generate_id("activities")
  end

  def generate_context_id do
    generate_id("contexts")
  end

  def generate_object_id do
lain's avatar
lain committed
129
    Helpers.o_status_url(Endpoint, :object, UUID.generate())
lain's avatar
lain committed
130 131 132
  end

  def generate_id(type) do
lain's avatar
lain committed
133
    "#{Web.base_url()}/#{type}/#{UUID.generate()}"
lain's avatar
lain committed
134 135
  end

136
  def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
137 138 139 140 141 142 143
    fake_create_activity = %{
      "to" => object["to"],
      "cc" => object["cc"],
      "type" => "Create",
      "object" => object
    }

144
    get_notified_from_object(fake_create_activity)
145 146
  end

147
  def get_notified_from_object(object) do
148
    Notification.get_notified_from_activity(%Activity{data: object}, false)
149 150
  end

151 152 153
  def create_context(context) do
    context = context || generate_id("contexts")

Sergey Suprunenko's avatar
Sergey Suprunenko committed
154 155 156
    # Ecto has problems accessing the constraint inside the jsonb,
    # so we explicitly check for the existed object before insert
    object = Object.get_cached_by_ap_id(context)
lain's avatar
lain committed
157

Sergey Suprunenko's avatar
Sergey Suprunenko committed
158 159 160 161 162 163 164
    with true <- is_nil(object),
         changeset <- Object.context_mapping(context),
         {:ok, inserted_object} <- Repo.insert(changeset) do
      inserted_object
    else
      _ ->
        object
165 166 167
    end
  end

lain's avatar
lain committed
168 169 170
  @doc """
  Enqueues an activity for federation if it's local
  """
171
  @spec maybe_federate(any()) :: :ok
lain's avatar
lain committed
172
  def maybe_federate(%Activity{local: true} = activity) do
lain's avatar
lain committed
173
    if Pleroma.Config.get!([:instance, :federating]) do
174
      Pleroma.Web.Federator.publish(activity)
lain's avatar
lain committed
175 176
    end

lain's avatar
lain committed
177 178
    :ok
  end
lain's avatar
lain committed
179

lain's avatar
lain committed
180 181 182 183 184 185
  def maybe_federate(_), do: :ok

  @doc """
  Adds an id and a published data if they aren't there,
  also adds it to an included object
  """
Maksim's avatar
Maksim committed
186
  @spec lazy_put_activity_defaults(map(), boolean) :: map()
187
  def lazy_put_activity_defaults(map, fake? \\ false)
lain's avatar
lain committed
188

Maksim's avatar
Maksim committed
189 190 191 192 193 194 195
  def lazy_put_activity_defaults(map, true) do
    map
    |> Map.put_new("id", "pleroma:fakeid")
    |> Map.put_new_lazy("published", &make_date/0)
    |> Map.put_new("context", "pleroma:fakecontext")
    |> Map.put_new("context_id", -1)
    |> lazy_put_object_defaults(true)
lain's avatar
lain committed
196 197
  end

198
  def lazy_put_activity_defaults(map, _fake?) do
Maksim's avatar
Maksim committed
199
    %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
200 201

    map
Maksim's avatar
Maksim committed
202
    |> Map.put_new_lazy("id", &generate_activity_id/0)
203
    |> Map.put_new_lazy("published", &make_date/0)
Maksim's avatar
Maksim committed
204 205 206
    |> Map.put_new("context", context)
    |> Map.put_new("context_id", context_id)
    |> lazy_put_object_defaults(false)
207 208
  end

Maksim's avatar
Maksim committed
209 210 211 212 213 214 215 216 217 218 219 220 221 222
  # Adds an id and published date if they aren't there.
  #
  @spec lazy_put_object_defaults(map(), boolean()) :: map()
  defp lazy_put_object_defaults(%{"object" => map} = activity, true)
       when is_map(map) do
    object =
      map
      |> Map.put_new("id", "pleroma:fake_object_id")
      |> Map.put_new_lazy("published", &make_date/0)
      |> Map.put_new("context", activity["context"])
      |> Map.put_new("context_id", activity["context_id"])
      |> Map.put_new("fake", true)

    %{activity | "object" => object}
lain's avatar
lain committed
223 224
  end

Maksim's avatar
Maksim committed
225 226 227 228 229 230 231 232 233 234
  defp lazy_put_object_defaults(%{"object" => map} = activity, _)
       when is_map(map) do
    object =
      map
      |> Map.put_new_lazy("id", &generate_object_id/0)
      |> Map.put_new_lazy("published", &make_date/0)
      |> Map.put_new("context", activity["context"])
      |> Map.put_new("context_id", activity["context_id"])

    %{activity | "object" => object}
lain's avatar
lain committed
235 236
  end

Maksim's avatar
Maksim committed
237 238
  defp lazy_put_object_defaults(activity, _), do: activity

lain's avatar
lain committed
239 240 241
  @doc """
  Inserts a full object if it is contained in an activity.
  """
242
  def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
243
      when is_map(object_data) and type in @supported_object_types do
244
    with {:ok, object} <- Object.create(object_data) do
245
      map = Map.put(map, "object", object.data["id"])
246

247
      {:ok, map, object}
lain's avatar
lain committed
248 249
    end
  end
lain's avatar
lain committed
250

251
  def insert_full_object(map), do: {:ok, map, nil}
lain's avatar
lain committed
252 253 254 255 256 257

  #### Like-related helpers

  @doc """
  Returns an existing like if a user already liked an object
  """
258
  @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
Thog's avatar
Thog committed
259
  def get_existing_like(actor, %{data: %{"id" => id}}) do
260 261 262 263
    actor
    |> Activity.Queries.by_actor()
    |> Activity.Queries.by_object_id(id)
    |> Activity.Queries.by_type("Like")
264
    |> limit(1)
265
    |> Repo.one()
lain's avatar
lain committed
266 267
  end

268 269 270 271
  @doc """
  Returns like activities targeting an object
  """
  def get_object_likes(%{data: %{"id" => id}}) do
272 273 274 275
    id
    |> Activity.Queries.by_object_id()
    |> Activity.Queries.by_type("Like")
    |> Repo.all()
276 277
  end

278
  @spec make_like_data(User.t(), map(), String.t()) :: map()
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
  def make_like_data(
        %User{ap_id: ap_id} = actor,
        %{data: %{"actor" => object_actor_id, "id" => id}} = object,
        activity_id
      ) do
    object_actor = User.get_cached_by_ap_id(object_actor_id)

    to =
      if Visibility.is_public?(object) do
        [actor.follower_address, object.data["actor"]]
      else
        [object.data["actor"]]
      end

    cc =
      (object.data["to"] ++ (object.data["cc"] || []))
      |> List.delete(actor.ap_id)
      |> List.delete(object_actor.follower_address)

298
    %{
lain's avatar
lain committed
299 300 301
      "type" => "Like",
      "actor" => ap_id,
      "object" => id,
302 303
      "to" => to,
      "cc" => cc,
lain's avatar
lain committed
304 305
      "context" => object.data["context"]
    }
306
    |> maybe_put("id", activity_id)
lain's avatar
lain committed
307 308
  end

309 310
  def make_emoji_reaction_data(user, object, emoji, activity_id) do
    make_like_data(user, object, activity_id)
lain's avatar
lain committed
311
    |> Map.put("type", "EmojiReact")
312 313 314
    |> Map.put("content", emoji)
  end

315
  @spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
316
          {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
317
  def update_element_in_object(property, element, object, count \\ nil) do
318
    length =
319 320
      count ||
        length(element)
321

322 323 324
    data =
      Map.merge(
        object.data,
325
        %{"#{property}_count" => length, "#{property}s" => element}
326
      )
lain's avatar
lain committed
327

328 329 330
    object
    |> Changeset.change(data: data)
    |> Object.update_and_set_cache()
lain's avatar
lain committed
331 332
  end

333 334 335 336 337 338 339
  @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
          {:ok, Object.t()} | {:error, Ecto.Changeset.t()}

  def add_emoji_reaction_to_object(
        %Activity{data: %{"content" => emoji, "actor" => actor}},
        object
      ) do
340
    reactions = get_cached_emoji_reactions(object)
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361

    new_reactions =
      case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
        nil ->
          reactions ++ [[emoji, [actor]]]

        index ->
          List.update_at(
            reactions,
            index,
            fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end
          )
      end

    count = emoji_count(new_reactions)

    update_element_in_object("reaction", new_reactions, object, count)
  end

  def emoji_count(reactions_list) do
    Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end)
362 363
  end

364 365 366 367
  def remove_emoji_reaction_from_object(
        %Activity{data: %{"content" => emoji, "actor" => actor}},
        object
      ) do
368
    reactions = get_cached_emoji_reactions(object)
369 370

    new_reactions =
371 372 373 374 375 376 377 378 379 380 381
      case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
        nil ->
          reactions

        index ->
          List.update_at(
            reactions,
            index,
            fn [emoji, users] -> [emoji, List.delete(users, actor)] end
          )
          |> Enum.reject(fn [_, users] -> Enum.empty?(users) end)
382 383
      end

384 385
    count = emoji_count(new_reactions)
    update_element_in_object("reaction", new_reactions, object, count)
386 387
  end

388 389 390 391 392 393 394 395
  def get_cached_emoji_reactions(object) do
    if is_list(object.data["reactions"]) do
      object.data["reactions"]
    else
      []
    end
  end

396 397
  @spec add_like_to_object(Activity.t(), Object.t()) ::
          {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
lain's avatar
lain committed
398
  def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
399 400 401
    [actor | fetch_likes(object)]
    |> Enum.uniq()
    |> update_likes_in_object(object)
lain's avatar
lain committed
402 403
  end

404 405
  @spec remove_like_from_object(Activity.t(), Object.t()) ::
          {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
lain's avatar
lain committed
406
  def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
407 408 409 410 411
    object
    |> fetch_likes()
    |> List.delete(actor)
    |> update_likes_in_object(object)
  end
Haelwenn's avatar
Haelwenn committed
412

413 414 415 416 417 418 419 420 421
  defp update_likes_in_object(likes, object) do
    update_element_in_object("like", likes, object)
  end

  defp fetch_likes(object) do
    if is_list(object.data["likes"]) do
      object.data["likes"]
    else
      []
lain's avatar
lain committed
422 423 424 425 426
    end
  end

  #### Follow-related helpers

kaniini's avatar
kaniini committed
427 428 429
  @doc """
  Updates a follow activity's state (for locked accounts).
  """
Maksim's avatar
Maksim committed
430
  @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
431 432
  def update_follow_state_for_all(
        %Activity{data: %{"actor" => actor, "object" => object}} = activity,
433 434
        state
      ) do
Maksim's avatar
Maksim committed
435 436 437 438 439 440 441
    "Follow"
    |> Activity.Queries.by_type()
    |> Activity.Queries.by_actor(actor)
    |> Activity.Queries.by_object_id(object)
    |> where(fragment("data->>'state' = 'pending'"))
    |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
    |> Repo.update_all([])
442

Maksim's avatar
Maksim committed
443 444 445 446 447
    User.set_follow_state_cache(actor, object, state)

    activity = Activity.get_by_id(activity.id)

    {:ok, activity}
448 449
  end

rinpatch's avatar
rinpatch committed
450 451 452 453
  def update_follow_state(
        %Activity{data: %{"actor" => actor, "object" => object}} = activity,
        state
      ) do
454 455 456 457 458
    new_data = Map.put(activity.data, "state", state)
    changeset = Changeset.change(activity, data: new_data)

    with {:ok, activity} <- Repo.update(changeset) do
      User.set_follow_state_cache(actor, object, state)
kaniini's avatar
kaniini committed
459 460 461 462
      {:ok, activity}
    end
  end

lain's avatar
lain committed
463 464 465
  @doc """
  Makes a follow activity data for the given follower and followed
  """
kaniini's avatar
kaniini committed
466 467
  def make_follow_data(
        %User{ap_id: follower_id},
Maksim's avatar
Maksim committed
468
        %User{ap_id: followed_id} = _followed,
kaniini's avatar
kaniini committed
469 470
        activity_id
      ) do
471
    %{
lain's avatar
lain committed
472 473 474
      "type" => "Follow",
      "actor" => follower_id,
      "to" => [followed_id],
475
      "cc" => [Pleroma.Constants.as_public()],
476 477
      "object" => followed_id,
      "state" => "pending"
lain's avatar
lain committed
478
    }
479
    |> maybe_put("id", activity_id)
lain's avatar
lain committed
480 481
  end

lain's avatar
lain committed
482
  def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
483 484 485 486 487 488 489 490
    "Follow"
    |> Activity.Queries.by_type()
    |> where(actor: ^follower_id)
    # this is to use the index
    |> Activity.Queries.by_object_id(followed_id)
    |> order_by([activity], fragment("? desc nulls last", activity.id))
    |> limit(1)
    |> Repo.one()
lain's avatar
lain committed
491 492
  end

493 494 495 496 497 498 499 500 501
  def fetch_latest_undo(%User{ap_id: ap_id}) do
    "Undo"
    |> Activity.Queries.by_type()
    |> where(actor: ^ap_id)
    |> order_by([activity], fragment("? desc nulls last", activity.id))
    |> limit(1)
    |> Repo.one()
  end

lain's avatar
lain committed
502 503 504
  def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
    %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)

lain's avatar
lain committed
505
    "EmojiReact"
lain's avatar
lain committed
506 507 508 509 510 511 512 513 514
    |> Activity.Queries.by_type()
    |> where(actor: ^ap_id)
    |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
    |> Activity.Queries.by_object_id(object_ap_id)
    |> order_by([activity], fragment("? desc nulls last", activity.id))
    |> limit(1)
    |> Repo.one()
  end

lain's avatar
lain committed
515 516 517
  #### Announce-related helpers

  @doc """
518
  Retruns an existing announce activity if the notice has already been announced
lain's avatar
lain committed
519
  """
Maksim's avatar
Maksim committed
520
  @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
521 522 523 524 525 526 527
  def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
    "Announce"
    |> Activity.Queries.by_type()
    |> where(actor: ^actor)
    # this is to use the index
    |> Activity.Queries.by_object_id(ap_id)
    |> Repo.one()
normandy's avatar
normandy committed
528 529
  end

normandy's avatar
normandy committed
530 531 532
  @doc """
  Make announce activity data for the given actor and object
  """
533 534
  # for relayed messages, we only want to send to subscribers
  def make_announce_data(
535
        %User{ap_id: ap_id} = user,
536
        %Object{data: %{"id" => id}} = object,
537 538
        activity_id,
        false
539
      ) do
540
    %{
541 542 543 544 545 546 547
      "type" => "Announce",
      "actor" => ap_id,
      "object" => id,
      "to" => [user.follower_address],
      "cc" => [],
      "context" => object.data["context"]
    }
548
    |> maybe_put("id", activity_id)
549 550
  end

lain's avatar
lain committed
551 552 553
  def make_announce_data(
        %User{ap_id: ap_id} = user,
        %Object{data: %{"id" => id}} = object,
554 555
        activity_id,
        true
lain's avatar
lain committed
556
      ) do
557
    %{
lain's avatar
lain committed
558 559 560
      "type" => "Announce",
      "actor" => ap_id,
      "object" => id,
lain's avatar
lain committed
561
      "to" => [user.follower_address, object.data["actor"]],
562
      "cc" => [Pleroma.Constants.as_public()],
lain's avatar
lain committed
563 564
      "context" => object.data["context"]
    }
565
    |> maybe_put("id", activity_id)
lain's avatar
lain committed
566 567
  end

568 569 570 571 572
  @doc """
  Make unannounce activity data for the given actor and object
  """
  def make_unannounce_data(
        %User{ap_id: ap_id} = user,
lain's avatar
lain committed
573
        %Activity{data: %{"context" => context, "object" => object}} = activity,
574
        activity_id
575
      ) do
lain's avatar
lain committed
576 577
    object = Object.normalize(object)

578
    %{
579 580
      "type" => "Undo",
      "actor" => ap_id,
581
      "object" => activity.data,
lain's avatar
lain committed
582
      "to" => [user.follower_address, object.data["actor"]],
583
      "cc" => [Pleroma.Constants.as_public()],
normandy's avatar
normandy committed
584
      "context" => context
585
    }
586
    |> maybe_put("id", activity_id)
587 588
  end

Thog's avatar
Thog committed
589 590
  def make_unlike_data(
        %User{ap_id: ap_id} = user,
lain's avatar
lain committed
591
        %Activity{data: %{"context" => context, "object" => object}} = activity,
Thog's avatar
Thog committed
592 593
        activity_id
      ) do
lain's avatar
lain committed
594 595
    object = Object.normalize(object)

596
    %{
Thog's avatar
Thog committed
597 598 599
      "type" => "Undo",
      "actor" => ap_id,
      "object" => activity.data,
lain's avatar
lain committed
600
      "to" => [user.follower_address, object.data["actor"]],
601
      "cc" => [Pleroma.Constants.as_public()],
Thog's avatar
Thog committed
602 603
      "context" => context
    }
604
    |> maybe_put("id", activity_id)
Thog's avatar
Thog committed
605 606
  end

607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625
  def make_undo_data(
        %User{ap_id: actor, follower_address: follower_address},
        %Activity{
          data: %{"id" => undone_activity_id, "context" => context},
          actor: undone_activity_actor
        },
        activity_id \\ nil
      ) do
    %{
      "type" => "Undo",
      "actor" => actor,
      "object" => undone_activity_id,
      "to" => [follower_address, undone_activity_actor],
      "cc" => [Pleroma.Constants.as_public()],
      "context" => context
    }
    |> maybe_put("id", activity_id)
  end

Maksim's avatar
Maksim committed
626 627
  @spec add_announce_to_object(Activity.t(), Object.t()) ::
          {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
628
  def add_announce_to_object(
629
        %Activity{data: %{"actor" => actor}},
630 631
        object
      ) do
632 633
    unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
      announcements = take_announcements(object)
Haelwenn's avatar
Haelwenn committed
634

635 636 637 638 639
      with announcements <- Enum.uniq([actor | announcements]) do
        update_element_in_object("announcement", announcements, object)
      end
    else
      {:ok, object}
lain's avatar
lain committed
640 641 642
    end
  end

643 644
  def add_announce_to_object(_, object), do: {:ok, object}

Maksim's avatar
Maksim committed
645 646
  @spec remove_announce_from_object(Activity.t(), Object.t()) ::
          {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
normandy's avatar
normandy committed
647
  def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
648
    with announcements <- List.delete(take_announcements(object), actor) do
normandy's avatar
normandy committed
649 650 651 652
      update_element_in_object("announcement", announcements, object)
    end
  end

653
  defp take_announcements(%{data: %{"announcements" => announcements}} = _)
Maksim's avatar
Maksim committed
654 655 656
       when is_list(announcements),
       do: announcements

657
  defp take_announcements(_), do: []
Maksim's avatar
Maksim committed
658

lain's avatar
lain committed
659 660
  #### Unfollow-related helpers

normandy's avatar
normandy committed
661
  def make_unfollow_data(follower, followed, follow_activity, activity_id) do
662
    %{
lain's avatar
lain committed
663 664 665
      "type" => "Undo",
      "actor" => follower.ap_id,
      "to" => [followed.ap_id],
normandy's avatar
normandy committed
666
      "object" => follow_activity.data
lain's avatar
lain committed
667
    }
668
    |> maybe_put("id", activity_id)
lain's avatar
lain committed
669 670
  end

normandy's avatar
normandy committed
671
  #### Block-related helpers
Maksim's avatar
Maksim committed
672
  @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
normandy's avatar
normandy committed
673
  def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
674 675 676 677 678 679 680 681
    "Block"
    |> Activity.Queries.by_type()
    |> where(actor: ^blocker_id)
    # this is to use the index
    |> Activity.Queries.by_object_id(blocked_id)
    |> order_by([activity], fragment("? desc nulls last", activity.id))
    |> limit(1)
    |> Repo.one()
normandy's avatar
normandy committed
682 683
  end

normandy's avatar
normandy committed
684
  def make_block_data(blocker, blocked, activity_id) do
685
    %{
normandy's avatar
normandy committed
686 687 688 689 690
      "type" => "Block",
      "actor" => blocker.ap_id,
      "to" => [blocked.ap_id],
      "object" => blocked.ap_id
    }
691
    |> maybe_put("id", activity_id)
normandy's avatar
normandy committed
692 693
  end

normandy's avatar
normandy committed
694
  def make_unblock_data(blocker, blocked, block_activity, activity_id) do
695
    %{
normandy's avatar
normandy committed
696 697 698 699
      "type" => "Undo",
      "actor" => blocker.ap_id,
      "to" => [blocked.ap_id],
      "object" => block_activity.data
lain's avatar
lain committed
700
    }
701
    |> maybe_put("id", activity_id)
lain's avatar
lain committed
702 703 704 705 706 707
  end

  #### Create-related helpers

  def make_create_data(params, additional) do
    published = params.published || make_date()
lain's avatar
lain committed
708

Thog's avatar
Thog committed
709
    %{
lain's avatar
lain committed
710
      "type" => "Create",
lain's avatar
lain committed
711
      "to" => params.to |> Enum.uniq(),
lain's avatar
lain committed
712 713 714 715 716 717 718
      "actor" => params.actor.ap_id,
      "object" => params.object,
      "published" => published,
      "context" => params.context
    }
    |> Map.merge(additional)
  end
minibikini's avatar
Reports  
minibikini committed
719

720 721 722
  #### Listen-related helpers
  def make_listen_data(params, additional) do
    published = params.published || make_date()
723

724 725 726 727 728 729 730 731 732 733
    %{
      "type" => "Listen",
      "to" => params.to |> Enum.uniq(),
      "actor" => params.actor.ap_id,
      "object" => params.object,
      "published" => published,
      "context" => params.context
    }
    |> Map.merge(additional)
  end
minibikini's avatar
Reports  
minibikini committed
734 735

  #### Flag-related helpers
Maksim's avatar
Maksim committed
736 737
  @spec make_flag_data(map(), map()) :: map()
  def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
minibikini's avatar
Reports  
minibikini committed
738 739
    %{
      "type" => "Flag",
Maksim's avatar
Maksim committed
740 741 742 743
      "actor" => actor.ap_id,
      "content" => content,
      "object" => build_flag_object(params),
      "context" => context,
Sergey Suprunenko's avatar
Sergey Suprunenko committed
744
      "state" => "open"
minibikini's avatar
Reports  
minibikini committed
745 746 747
    }
    |> Map.merge(additional)
  end
748

Maksim's avatar
Maksim committed
749 750 751
  def make_flag_data(_, _), do: %{}

  defp build_flag_object(%{account: account, statuses: statuses} = _) do
Maxim Filippov's avatar
Maxim Filippov committed
752 753
    [account.ap_id] ++ build_flag_object(%{statuses: statuses})
  end
754

Maxim Filippov's avatar
Maxim Filippov committed
755 756 757
  defp build_flag_object(%{statuses: statuses}) do
    Enum.map(statuses || [], &build_flag_object/1)
  end
758

Maxim Filippov's avatar
Maxim Filippov committed
759 760 761 762 763 764 765 766
  defp build_flag_object(act) when is_map(act) or is_binary(act) do
    id =
      case act do
        %Activity{} = act -> act.data["id"]
        act when is_map(act) -> act["id"]
        act when is_binary(act) -> act
      end

767 768 769 770 771 772 773 774 775 776 777 778
    case Activity.get_by_ap_id_with_object(id) do
      %Activity{} = activity ->
        %{
          "type" => "Note",
          "id" => activity.data["id"],
          "content" => activity.object.data["content"],
          "published" => activity.object.data["published"],
          "actor" =>
            AccountView.render("show.json", %{
              user: User.get_by_ap_id(activity.object.data["actor"])
            })
        }
Maxim Filippov's avatar
Maxim Filippov committed
779

780 781 782
      _ ->
        %{"id" => id, "deleted" => true}
    end
Maksim's avatar
Maksim committed
783 784 785 786
  end

  defp build_flag_object(_), do: []

787 788 789 790 791 792 793
  @doc """
  Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
  the first one to `pages_left` pages.
  If the amount of pages is higher than the collection has, it returns whatever was there.
  """
  def fetch_ordered_collection(from, pages_left, acc \\ []) do
    with {:ok, response} <- Tesla.get(from),
feld's avatar
feld committed
794
         {:ok, collection} <- Jason.decode(response.body) do
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824
      case collection["type"] do
        "OrderedCollection" ->
          # If we've encountered the OrderedCollection and not the page,
          # just call the same function on the page address
          fetch_ordered_collection(collection["first"], pages_left)

        "OrderedCollectionPage" ->
          if pages_left > 0 do
            # There are still more pages
            if Map.has_key?(collection, "next") do
              # There are still more pages, go deeper saving what we have into the accumulator
              fetch_ordered_collection(
                collection["next"],
                pages_left - 1,
                acc ++ collection["orderedItems"]
              )
            else
              # No more pages left, just return whatever we already have
              acc ++ collection["orderedItems"]
            end
          else
            # Got the amount of pages needed, add them all to the accumulator
            acc ++ collection["orderedItems"]
          end

        _ ->
          {:error, "Not an OrderedCollection or OrderedCollectionPage"}
      end
    end
  end
Sergey Suprunenko's avatar
Sergey Suprunenko committed
825 826

  #### Report-related helpers
827 828 829 830 831
  def get_reports(params, page, page_size) do
    params =
      params
      |> Map.put("type", "Flag")
      |> Map.put("skip_preload", true)
Maxim Filippov's avatar
Maxim Filippov committed
832
      |> Map.put("preload_report_notes", true)
833 834 835 836 837 838 839
      |> Map.put("total", true)
      |> Map.put("limit", page_size)
      |> Map.put("offset", (page - 1) * page_size)

    ActivityPub.fetch_activities([], params, :offset)
  end

Maxim Filippov's avatar
Maxim Filippov committed
840 841 842 843
  def parse_report_group(activity) do
    reports = get_reports_by_status_id(activity["id"])
    max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
    actors = Enum.map(reports, & &1.user_actor)
844 845 846 847 848 849 850
    [%{data: %{"object" => [account_id | _]}} | _] = reports

    account =
      AccountView.render("show.json", %{
        user: User.get_by_ap_id(account_id)
      })

851
    status = get_status_data(activity)
Maxim Filippov's avatar
Maxim Filippov committed
852 853 854

    %{
      date: max_date.data["published"],
855
      account: account,
856
      status: status,
Maxim Filippov's avatar
Maxim Filippov committed
857 858
      actors: Enum.uniq(actors),
      reports: reports
859 860 861
    }
  end

862 863 864 865 866 867 868
  defp get_status_data(status) do
    case status["deleted"] do
      true ->
        %{
          "id" => status["id"],
          "deleted" => true
        }
869 870

      _ ->
871
        Activity.get_by_ap_id(status["id"])
872 873 874
    end
  end

875
  def get_reports_by_status_id(ap_id) do
876 877
    from(a in Activity,
      where: fragment("(?)->>'type' = 'Flag'", a.data),
878 879
      where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]),
      or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])
880 881 882 883 884
    )
    |> Activity.with_preloaded_user_actor()
    |> Repo.all()
  end

885
  @spec get_reports_grouped_by_status([String.t()]) :: %{
886 887 888 889 890 891 892 893
          required(:groups) => [
            %{
              required(:date) => String.t(),
              required(:account) => %{},
              required(:status) => %{},
              required(:actors) => [%User{}],
              required(:reports) => [%Activity{}]
            }
894
          ]
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909
        }
  def get_reports_grouped_by_status(activity_ids) do
    parsed_groups =
      activity_ids
      |> Enum.map(fn id ->
        id
        |> build_flag_object()
        |> parse_report_group()
      end)

    %{
      groups: parsed_groups
    }
  end

910
  @spec get_grouped_reports() :: [
911
          %{
912 913 914 915
            status: Activity.t(),
            account: User.t(),
            actors: [User.t()],
            reports: [Activity.t()]
916 917
          }
        ]
918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
  def get_grouped_reports do
    reports = Activity.flags_activities_query() |> Repo.all()

    flags =
      Enum.map(reports, fn %{
                             id: id,
                             actor: actor,
                             data: %{"object" => [account | statuses], "published" => date}
                           } ->
        flag = %{
          report_id: id,
          actor: actor,
          account: account,
          date: date
        }

        Enum.map(statuses, fn
          status when is_map(status) ->
            Map.put(flag, :id, status["id"])

          activity_id when is_binary(activity_id) ->
            Map.put(flag, :id, activity_id)
        end)
      end)

    ids = %{accounts: [], actors: [], reports: []}

    {ids, groups} =
      flags
      |> List.flatten()
      |> Enum.reduce({ids, %{}}, fn status, {ids, acc} ->
        acc =
          Map.update(
            acc,
            status.id,
            %{
              account: status.account,
              actors: [status.actor],
              reports: [status.report_id],
              date: status.date
            },
            &update_reported_group(&1, status)
          )

        ids =
          ids
          |> Map.put(:accounts, [status.account | ids.accounts])
          |> Map.put(:actors, [status.actor | ids.actors])
          |> Map.put(:reports, [status.report_id | ids.reports])

        {ids, acc}
      end)

    loaded_activities =
      groups
      |> Map.keys()
      |> Activity.all_by_ap_ids_with_object()
      |> Enum.reduce(%{}, fn activity, acc ->
        Map.put(acc, activity.data["id"], activity)
      end)

    loaded_users =
      (ids.accounts ++ ids.actors)
      |> Enum.uniq()
      |> User.get_all_by_ap_ids()
      |> Enum.reduce(%{}, fn user, acc -> Map.put(acc, user.ap_id, user) end)

    loaded_reports =
      reports
      |> Enum.reduce(%{}, fn report, acc -> Map.put(acc, report.id, report) end)

    Enum.map(groups, fn {activity_id, group} ->
      updated_actors =
        group.actors
        |> Enum.uniq()
        |> Enum.map(&Map.get(loaded_users, &1))

      updated_reports =
        group.reports
        |> Enum.uniq()
        |> Enum.map(&Map.get(loaded_reports, &1))

      group
      |> Map.put(
        :status,
        Map.get(loaded_activities, activity_id, %{"id" => activity_id, "deleted" => true})
      )
      |> Map.put(
        :account,
        Map.get(loaded_users, group.account, %{"id" => group.account, "deleted" => true})
      )
      |> Map.put(:actors, updated_actors)
      |> Map.put(:reports, updated_reports)
    end)
  end

  defp update_reported_group(group, status) do
    if NaiveDateTime.compare(
         NaiveDateTime.from_iso8601!(status.date),
         NaiveDateTime.from_iso8601!(group.date)
       ) == :gt do
      Map.put(group, :date, status.date)
    else
      group
    end
    |> Map.put(:actors, [status.actor | group.actors])
    |> Map.put(:reports, [status.report_id | group.reports])
  end

  @spec get_reported_activities() :: [String.t()]
Maxim Filippov's avatar
Maxim Filippov committed
1028
  def get_reported_activities do
1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
    reported_activities_query =
      from(a in Activity,
        where: fragment("(?)->>'type' = 'Flag'", a.data),
        select: %{
          activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data)
        },
        group_by: fragment("activity")
      )

    from(a in subquery(reported_activities_query),
      distinct: true,
1040
      select: %{
1041 1042
        id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity)
      }
1043 1044
    )
    |> Repo.all()
1045
    |> Enum.map(& &1.id)
1046
  end
Sergey Suprunenko's avatar
Sergey Suprunenko committed
1047

1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061
  def update_report_state(%Activity{} = activity, state)
      when state in @strip_status_report_states do
    {:ok, stripped_activity} = strip_report_status_data(activity)

    new_data =
      activity.data
      |> Map.put("state", state)
      |> Map.put("object", stripped_activity.data["object"])

    activity
    |> Changeset.change(data: new_data)
    |> Repo.update()
  end

Sergey Suprunenko's avatar
Sergey Suprunenko committed
1062
  def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
1063 1064 1065 1066 1067
    new_data = Map.put(activity.data, "state", state)

    activity
    |> Changeset.change(data: new_data)
    |> Repo.update()
Sergey Suprunenko's avatar
Sergey Suprunenko committed
1068 1069
  end

1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
  def update_report_state(activity_ids, state) when state in @supported_report_states do
    activities_num = length(activity_ids)

    from(a in Activity, where: a.id in ^activity_ids)
    |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
    |> Repo.update_all([])
    |> case do
      {^activities_num, _} -> :ok
      _ -> {:error, activity_ids}
    end
  end

Sergey Suprunenko's avatar
Sergey Suprunenko committed
1082 1083
  def update_report_state(_, _), do: {:error, "Unsupported state"}

1084 1085
  def strip_report_status_data(activity) do
    [actor | reported_activities] = activity.data["object"]
1086 1087 1088 1089 1090 1091 1092

    stripped_activities =
      Enum.map(reported_activities, fn
        act when is_map(act) -> act["id"]
        act when is_binary(act) -> act
      end)

1093 1094 1095 1096 1097
    new_data = put_in(activity.data, ["object"], [actor | stripped_activities])

    {:ok, %{activity | data: new_data}}
  end

Sergey Suprunenko's avatar
Sergey Suprunenko committed
1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
  def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
    [to, cc, recipients] =
      activity
      |> get_updated_targets(visibility)
      |> Enum.map(&Enum.uniq/1)

    object_data =
      activity.object.data
      |> Map.put("to", to)
      |> Map.put("cc", cc)

    {:ok, object} =
      activity.object
      |> Object.change(%{data: object_data})
      |> Object.update_and_set_cache()

    activity_data =
      activity.data
      |> Map.put("to", to)
      |> Map.put("cc", cc)

    activity
    |> Map.put(:object, object)
    |> Activity.change(%{data: activity_data, recipients: recipients})
    |> Repo.update()
  end

  def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}

  defp get_updated_targets(
         %Activity{data: %{"to" => to} = data, recipients: recipients},
         visibility
       ) do
    cc = Map.get(data, "cc", [])
    follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
1133
    public = Pleroma.Constants.as_public()
Sergey Suprunenko's avatar
Sergey Suprunenko committed
1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152