logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

utils.ex (27439B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.ActivityPub.Utils do
  5. alias Ecto.Changeset
  6. alias Ecto.UUID
  7. alias Pleroma.Activity
  8. alias Pleroma.Config
  9. alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID
  10. alias Pleroma.Maps
  11. alias Pleroma.Notification
  12. alias Pleroma.Object
  13. alias Pleroma.Repo
  14. alias Pleroma.User
  15. alias Pleroma.Web.ActivityPub.ActivityPub
  16. alias Pleroma.Web.ActivityPub.Visibility
  17. alias Pleroma.Web.AdminAPI.AccountView
  18. alias Pleroma.Web.Endpoint
  19. alias Pleroma.Web.Router.Helpers
  20. import Ecto.Query
  21. require Logger
  22. require Pleroma.Constants
  23. @supported_object_types [
  24. "Article",
  25. "Note",
  26. "Event",
  27. "Video",
  28. "Page",
  29. "Question",
  30. "Answer",
  31. "Audio",
  32. "Image"
  33. ]
  34. @strip_status_report_states ~w(closed resolved)
  35. @supported_report_states ~w(open closed resolved)
  36. @valid_visibilities ~w(public unlisted private direct)
  37. def as_local_public, do: Endpoint.url() <> "/#Public"
  38. # Some implementations send the actor URI as the actor field, others send the entire actor object,
  39. # so figure out what the actor's URI is based on what we have.
  40. def get_ap_id(%{"id" => id} = _), do: id
  41. def get_ap_id(id), do: id
  42. def normalize_params(params) do
  43. Map.put(params, "actor", get_ap_id(params["actor"]))
  44. end
  45. @spec determine_explicit_mentions(map()) :: [any]
  46. def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
  47. Enum.flat_map(tag, fn
  48. %{"type" => "Mention", "href" => href} -> [href]
  49. _ -> []
  50. end)
  51. end
  52. def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
  53. object
  54. |> Map.put("tag", [tag])
  55. |> determine_explicit_mentions()
  56. end
  57. def determine_explicit_mentions(_), do: []
  58. @spec label_in_collection?(any(), any()) :: boolean()
  59. defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
  60. defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
  61. defp label_in_collection?(_, _), do: false
  62. @spec label_in_message?(String.t(), map()) :: boolean()
  63. def label_in_message?(label, params),
  64. do:
  65. [params["to"], params["cc"], params["bto"], params["bcc"]]
  66. |> Enum.any?(&label_in_collection?(label, &1))
  67. @spec unaddressed_message?(map()) :: boolean()
  68. def unaddressed_message?(params),
  69. do:
  70. [params["to"], params["cc"], params["bto"], params["bcc"]]
  71. |> Enum.all?(&is_nil(&1))
  72. @spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
  73. def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
  74. do:
  75. label_in_message?(ap_id, params) || unaddressed_message?(params) ||
  76. User.following?(recipient, actor)
  77. defp extract_list(target) when is_binary(target), do: [target]
  78. defp extract_list(lst) when is_list(lst), do: lst
  79. defp extract_list(_), do: []
  80. def maybe_splice_recipient(ap_id, params) do
  81. need_splice? =
  82. !label_in_collection?(ap_id, params["to"]) &&
  83. !label_in_collection?(ap_id, params["cc"])
  84. if need_splice? do
  85. cc = [ap_id | extract_list(params["cc"])]
  86. params
  87. |> Map.put("cc", cc)
  88. |> Maps.safe_put_in(["object", "cc"], cc)
  89. else
  90. params
  91. end
  92. end
  93. def make_json_ld_header do
  94. %{
  95. "@context" => [
  96. "https://www.w3.org/ns/activitystreams",
  97. "#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
  98. %{
  99. "@language" => "und"
  100. }
  101. ]
  102. }
  103. end
  104. def make_date do
  105. DateTime.utc_now() |> DateTime.to_iso8601()
  106. end
  107. def generate_activity_id do
  108. generate_id("activities")
  109. end
  110. def generate_context_id do
  111. generate_id("contexts")
  112. end
  113. def generate_object_id do
  114. Helpers.o_status_url(Endpoint, :object, UUID.generate())
  115. end
  116. def generate_id(type) do
  117. "#{Endpoint.url()}/#{type}/#{UUID.generate()}"
  118. end
  119. def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
  120. fake_create_activity = %{
  121. "to" => object["to"],
  122. "cc" => object["cc"],
  123. "type" => "Create",
  124. "object" => object
  125. }
  126. get_notified_from_object(fake_create_activity)
  127. end
  128. def get_notified_from_object(object) do
  129. Notification.get_notified_from_activity(%Activity{data: object}, false)
  130. end
  131. def maybe_create_context(context), do: context || generate_id("contexts")
  132. @doc """
  133. Enqueues an activity for federation if it's local
  134. """
  135. @spec maybe_federate(any()) :: :ok
  136. def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
  137. outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
  138. with true <- Config.get!([:instance, :federating]),
  139. true <- type != "Block" || outgoing_blocks,
  140. false <- Visibility.local_public?(activity) do
  141. Pleroma.Web.Federator.publish(activity)
  142. end
  143. :ok
  144. end
  145. def maybe_federate(_), do: :ok
  146. @doc """
  147. Adds an id and a published data if they aren't there,
  148. also adds it to an included object
  149. """
  150. @spec lazy_put_activity_defaults(map(), boolean) :: map()
  151. def lazy_put_activity_defaults(map, fake? \\ false)
  152. def lazy_put_activity_defaults(map, true) do
  153. map
  154. |> Map.put_new("id", "pleroma:fakeid")
  155. |> Map.put_new_lazy("published", &make_date/0)
  156. |> Map.put_new("context", "pleroma:fakecontext")
  157. |> lazy_put_object_defaults(true)
  158. end
  159. def lazy_put_activity_defaults(map, _fake?) do
  160. context = maybe_create_context(map["context"])
  161. map
  162. |> Map.put_new_lazy("id", &generate_activity_id/0)
  163. |> Map.put_new_lazy("published", &make_date/0)
  164. |> Map.put_new("context", context)
  165. |> lazy_put_object_defaults(false)
  166. end
  167. # Adds an id and published date if they aren't there.
  168. #
  169. @spec lazy_put_object_defaults(map(), boolean()) :: map()
  170. defp lazy_put_object_defaults(%{"object" => map} = activity, true)
  171. when is_map(map) do
  172. object =
  173. map
  174. |> Map.put_new("id", "pleroma:fake_object_id")
  175. |> Map.put_new_lazy("published", &make_date/0)
  176. |> Map.put_new("context", activity["context"])
  177. |> Map.put_new("fake", true)
  178. %{activity | "object" => object}
  179. end
  180. defp lazy_put_object_defaults(%{"object" => map} = activity, _)
  181. when is_map(map) do
  182. object =
  183. map
  184. |> Map.put_new_lazy("id", &generate_object_id/0)
  185. |> Map.put_new_lazy("published", &make_date/0)
  186. |> Map.put_new("context", activity["context"])
  187. %{activity | "object" => object}
  188. end
  189. defp lazy_put_object_defaults(activity, _), do: activity
  190. @doc """
  191. Inserts a full object if it is contained in an activity.
  192. """
  193. def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
  194. when type in @supported_object_types do
  195. with {:ok, object} <- Object.create(object_data) do
  196. map = Map.put(map, "object", object.data["id"])
  197. {:ok, map, object}
  198. end
  199. end
  200. def insert_full_object(map), do: {:ok, map, nil}
  201. #### Like-related helpers
  202. @doc """
  203. Returns an existing like if a user already liked an object
  204. """
  205. @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
  206. def get_existing_like(actor, %{data: %{"id" => id}}) do
  207. actor
  208. |> Activity.Queries.by_actor()
  209. |> Activity.Queries.by_object_id(id)
  210. |> Activity.Queries.by_type("Like")
  211. |> limit(1)
  212. |> Repo.one()
  213. end
  214. @doc """
  215. Returns like activities targeting an object
  216. """
  217. def get_object_likes(%{data: %{"id" => id}}) do
  218. id
  219. |> Activity.Queries.by_object_id()
  220. |> Activity.Queries.by_type("Like")
  221. |> Repo.all()
  222. end
  223. @spec make_like_data(User.t(), map(), String.t()) :: map()
  224. def make_like_data(
  225. %User{ap_id: ap_id} = actor,
  226. %{data: %{"actor" => object_actor_id, "id" => id}} = object,
  227. activity_id
  228. ) do
  229. object_actor = User.get_cached_by_ap_id(object_actor_id)
  230. to =
  231. if Visibility.public?(object) do
  232. [actor.follower_address, object.data["actor"]]
  233. else
  234. [object.data["actor"]]
  235. end
  236. cc =
  237. (object.data["to"] ++ (object.data["cc"] || []))
  238. |> List.delete(actor.ap_id)
  239. |> List.delete(object_actor.follower_address)
  240. %{
  241. "type" => "Like",
  242. "actor" => ap_id,
  243. "object" => id,
  244. "to" => to,
  245. "cc" => cc,
  246. "context" => object.data["context"]
  247. }
  248. |> Maps.put_if_present("id", activity_id)
  249. end
  250. def make_emoji_reaction_data(user, object, emoji, activity_id) do
  251. make_like_data(user, object, activity_id)
  252. |> Map.put("type", "EmojiReact")
  253. |> Map.put("content", emoji)
  254. end
  255. @spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
  256. {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
  257. def update_element_in_object(property, element, object, count \\ nil) do
  258. length =
  259. count ||
  260. length(element)
  261. data =
  262. Map.merge(
  263. object.data,
  264. %{"#{property}_count" => length, "#{property}s" => element}
  265. )
  266. object
  267. |> Changeset.change(data: data)
  268. |> Object.update_and_set_cache()
  269. end
  270. @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
  271. {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
  272. def add_emoji_reaction_to_object(
  273. %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
  274. object
  275. ) do
  276. reactions = get_cached_emoji_reactions(object)
  277. emoji = Pleroma.Emoji.maybe_strip_name(emoji)
  278. url = maybe_emoji_url(emoji, activity)
  279. new_reactions =
  280. case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
  281. if is_nil(candidate_url) do
  282. emoji == candidate
  283. else
  284. url == candidate_url
  285. end
  286. end) do
  287. nil ->
  288. reactions ++ [[emoji, [actor], url]]
  289. index ->
  290. List.update_at(
  291. reactions,
  292. index,
  293. fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
  294. )
  295. end
  296. count = emoji_count(new_reactions)
  297. update_element_in_object("reaction", new_reactions, object, count)
  298. end
  299. defp maybe_emoji_url(
  300. name,
  301. %Activity{
  302. data: %{
  303. "tag" => [
  304. %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
  305. ]
  306. }
  307. }
  308. ),
  309. do: url
  310. defp maybe_emoji_url(_, _), do: nil
  311. def emoji_count(reactions_list) do
  312. Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
  313. end
  314. def remove_emoji_reaction_from_object(
  315. %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
  316. object
  317. ) do
  318. emoji = Pleroma.Emoji.maybe_strip_name(emoji)
  319. reactions = get_cached_emoji_reactions(object)
  320. url = maybe_emoji_url(emoji, activity)
  321. new_reactions =
  322. case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
  323. if is_nil(candidate_url) do
  324. emoji == candidate
  325. else
  326. url == candidate_url
  327. end
  328. end) do
  329. nil ->
  330. reactions
  331. index ->
  332. List.update_at(
  333. reactions,
  334. index,
  335. fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
  336. )
  337. |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
  338. end
  339. count = emoji_count(new_reactions)
  340. update_element_in_object("reaction", new_reactions, object, count)
  341. end
  342. def get_cached_emoji_reactions(object) do
  343. Object.get_emoji_reactions(object)
  344. end
  345. @spec add_like_to_object(Activity.t(), Object.t()) ::
  346. {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
  347. def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
  348. [actor | fetch_likes(object)]
  349. |> Enum.uniq()
  350. |> update_likes_in_object(object)
  351. end
  352. @spec remove_like_from_object(Activity.t(), Object.t()) ::
  353. {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
  354. def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
  355. object
  356. |> fetch_likes()
  357. |> List.delete(actor)
  358. |> update_likes_in_object(object)
  359. end
  360. defp update_likes_in_object(likes, object) do
  361. update_element_in_object("like", likes, object)
  362. end
  363. defp fetch_likes(object) do
  364. if is_list(object.data["likes"]) do
  365. object.data["likes"]
  366. else
  367. []
  368. end
  369. end
  370. #### Follow-related helpers
  371. @doc """
  372. Updates a follow activity's state (for locked accounts).
  373. """
  374. @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
  375. def update_follow_state_for_all(
  376. %Activity{data: %{"actor" => actor, "object" => object}} = activity,
  377. state
  378. ) do
  379. "Follow"
  380. |> Activity.Queries.by_type()
  381. |> Activity.Queries.by_actor(actor)
  382. |> Activity.Queries.by_object_id(object)
  383. |> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
  384. |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
  385. |> Repo.update_all([])
  386. activity = Activity.get_by_id(activity.id)
  387. {:ok, activity}
  388. end
  389. def update_follow_state(
  390. %Activity{} = activity,
  391. state
  392. ) do
  393. new_data = Map.put(activity.data, "state", state)
  394. changeset = Changeset.change(activity, data: new_data)
  395. with {:ok, activity} <- Repo.update(changeset) do
  396. {:ok, activity}
  397. end
  398. end
  399. @doc """
  400. Makes a follow activity data for the given follower and followed
  401. """
  402. def make_follow_data(
  403. %User{ap_id: follower_id},
  404. %User{ap_id: followed_id} = _followed,
  405. activity_id
  406. ) do
  407. %{
  408. "type" => "Follow",
  409. "actor" => follower_id,
  410. "to" => [followed_id],
  411. "cc" => [Pleroma.Constants.as_public()],
  412. "object" => followed_id,
  413. "state" => "pending"
  414. }
  415. |> Maps.put_if_present("id", activity_id)
  416. end
  417. def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
  418. "Follow"
  419. |> Activity.Queries.by_type()
  420. |> where(actor: ^follower_id)
  421. # this is to use the index
  422. |> Activity.Queries.by_object_id(followed_id)
  423. |> order_by([activity], fragment("? desc nulls last", activity.id))
  424. |> limit(1)
  425. |> Repo.one()
  426. end
  427. def fetch_latest_undo(%User{ap_id: ap_id}) do
  428. "Undo"
  429. |> Activity.Queries.by_type()
  430. |> where(actor: ^ap_id)
  431. |> order_by([activity], fragment("? desc nulls last", activity.id))
  432. |> limit(1)
  433. |> Repo.one()
  434. end
  435. def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
  436. %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
  437. emoji = Pleroma.Emoji.maybe_quote(emoji)
  438. "EmojiReact"
  439. |> Activity.Queries.by_type()
  440. |> where(actor: ^ap_id)
  441. |> custom_emoji_discriminator(emoji)
  442. |> Activity.Queries.by_object_id(object_ap_id)
  443. |> order_by([activity], fragment("? desc nulls last", activity.id))
  444. |> limit(1)
  445. |> Repo.one()
  446. end
  447. defp custom_emoji_discriminator(query, emoji) do
  448. if String.contains?(emoji, "@") do
  449. stripped = Pleroma.Emoji.maybe_strip_name(emoji)
  450. [name, domain] = String.split(stripped, "@")
  451. domain_pattern = "%/" <> domain <> "/%"
  452. emoji_pattern = Pleroma.Emoji.maybe_quote(name)
  453. query
  454. |> where([activity], fragment("?->>'content' = ?
  455. AND EXISTS (
  456. SELECT FROM jsonb_array_elements(?->'tag') elem
  457. WHERE elem->>'id' ILIKE ?
  458. )", activity.data, ^emoji_pattern, activity.data, ^domain_pattern))
  459. else
  460. query
  461. |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
  462. end
  463. end
  464. #### Announce-related helpers
  465. @doc """
  466. Returns an existing announce activity if the notice has already been announced
  467. """
  468. @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
  469. def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
  470. "Announce"
  471. |> Activity.Queries.by_type()
  472. |> where(actor: ^actor)
  473. # this is to use the index
  474. |> Activity.Queries.by_object_id(ap_id)
  475. |> Repo.one()
  476. end
  477. @doc """
  478. Make announce activity data for the given actor and object
  479. """
  480. # for relayed messages, we only want to send to subscribers
  481. def make_announce_data(
  482. %User{ap_id: ap_id} = user,
  483. %Object{data: %{"id" => id}} = object,
  484. activity_id,
  485. false
  486. ) do
  487. %{
  488. "type" => "Announce",
  489. "actor" => ap_id,
  490. "object" => id,
  491. "to" => [user.follower_address],
  492. "cc" => [],
  493. "context" => object.data["context"]
  494. }
  495. |> Maps.put_if_present("id", activity_id)
  496. end
  497. def make_announce_data(
  498. %User{ap_id: ap_id} = user,
  499. %Object{data: %{"id" => id}} = object,
  500. activity_id,
  501. true
  502. ) do
  503. %{
  504. "type" => "Announce",
  505. "actor" => ap_id,
  506. "object" => id,
  507. "to" => [user.follower_address, object.data["actor"]],
  508. "cc" => [Pleroma.Constants.as_public()],
  509. "context" => object.data["context"]
  510. }
  511. |> Maps.put_if_present("id", activity_id)
  512. end
  513. def make_undo_data(
  514. %User{ap_id: actor, follower_address: follower_address},
  515. %Activity{
  516. data: %{"id" => undone_activity_id, "context" => context},
  517. actor: undone_activity_actor
  518. },
  519. activity_id \\ nil
  520. ) do
  521. %{
  522. "type" => "Undo",
  523. "actor" => actor,
  524. "object" => undone_activity_id,
  525. "to" => [follower_address, undone_activity_actor],
  526. "cc" => [Pleroma.Constants.as_public()],
  527. "context" => context
  528. }
  529. |> Maps.put_if_present("id", activity_id)
  530. end
  531. @spec add_announce_to_object(Activity.t(), Object.t()) ::
  532. {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
  533. def add_announce_to_object(
  534. %Activity{data: %{"actor" => actor}},
  535. object
  536. ) do
  537. unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
  538. announcements = take_announcements(object)
  539. with announcements <- Enum.uniq([actor | announcements]) do
  540. update_element_in_object("announcement", announcements, object)
  541. end
  542. else
  543. {:ok, object}
  544. end
  545. end
  546. def add_announce_to_object(_, object), do: {:ok, object}
  547. @spec remove_announce_from_object(Activity.t(), Object.t()) ::
  548. {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
  549. def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
  550. with announcements <- List.delete(take_announcements(object), actor) do
  551. update_element_in_object("announcement", announcements, object)
  552. end
  553. end
  554. defp take_announcements(%{data: %{"announcements" => announcements}} = _)
  555. when is_list(announcements),
  556. do: announcements
  557. defp take_announcements(_), do: []
  558. #### Unfollow-related helpers
  559. def make_unfollow_data(follower, followed, follow_activity, activity_id) do
  560. %{
  561. "type" => "Undo",
  562. "actor" => follower.ap_id,
  563. "to" => [followed.ap_id],
  564. "object" => follow_activity.data
  565. }
  566. |> Maps.put_if_present("id", activity_id)
  567. end
  568. #### Block-related helpers
  569. @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
  570. def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
  571. "Block"
  572. |> Activity.Queries.by_type()
  573. |> where(actor: ^blocker_id)
  574. # this is to use the index
  575. |> Activity.Queries.by_object_id(blocked_id)
  576. |> order_by([activity], fragment("? desc nulls last", activity.id))
  577. |> limit(1)
  578. |> Repo.one()
  579. end
  580. def make_block_data(blocker, blocked, activity_id) do
  581. %{
  582. "type" => "Block",
  583. "actor" => blocker.ap_id,
  584. "to" => [blocked.ap_id],
  585. "object" => blocked.ap_id
  586. }
  587. |> Maps.put_if_present("id", activity_id)
  588. end
  589. #### Create-related helpers
  590. def make_create_data(params, additional) do
  591. published = params.published || make_date()
  592. %{
  593. "type" => "Create",
  594. "to" => params.to |> Enum.uniq(),
  595. "actor" => params.actor.ap_id,
  596. "object" => params.object,
  597. "published" => published,
  598. "context" => params.context
  599. }
  600. |> Map.merge(additional)
  601. end
  602. #### Listen-related helpers
  603. def make_listen_data(params, additional) do
  604. published = params.published || make_date()
  605. %{
  606. "type" => "Listen",
  607. "to" => params.to |> Enum.uniq(),
  608. "actor" => params.actor.ap_id,
  609. "object" => params.object,
  610. "published" => published,
  611. "context" => params.context
  612. }
  613. |> Map.merge(additional)
  614. end
  615. #### Flag-related helpers
  616. @spec make_flag_data(map(), map()) :: map()
  617. def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
  618. %{
  619. "type" => "Flag",
  620. "actor" => actor.ap_id,
  621. "content" => content,
  622. "object" => build_flag_object(params),
  623. "context" => context,
  624. "state" => "open"
  625. }
  626. |> Map.merge(additional)
  627. end
  628. def make_flag_data(_, _), do: %{}
  629. defp build_flag_object(%{account: account, statuses: statuses}) do
  630. [account.ap_id | build_flag_object(%{statuses: statuses})]
  631. end
  632. defp build_flag_object(%{statuses: statuses}) do
  633. Enum.map(statuses || [], &build_flag_object/1)
  634. end
  635. defp build_flag_object(%Activity{} = activity) do
  636. object = Object.normalize(activity, fetch: false)
  637. # Do not allow people to report Creates. Instead, report the Object that is Created.
  638. if activity.data["type"] != "Create" do
  639. build_flag_object_with_actor_and_id(
  640. object,
  641. User.get_by_ap_id(activity.data["actor"]),
  642. activity.data["id"]
  643. )
  644. else
  645. build_flag_object(object)
  646. end
  647. end
  648. defp build_flag_object(%Object{} = object) do
  649. actor = User.get_by_ap_id(object.data["actor"])
  650. build_flag_object_with_actor_and_id(object, actor, object.data["id"])
  651. end
  652. defp build_flag_object(act) when is_map(act) or is_binary(act) do
  653. id =
  654. case act do
  655. %Activity{} = act -> act.data["id"]
  656. act when is_map(act) -> act["id"]
  657. act when is_binary(act) -> act
  658. end
  659. case Activity.get_by_ap_id_with_object(id) do
  660. %Activity{object: object} = _ ->
  661. build_flag_object(object)
  662. nil ->
  663. case Object.get_by_ap_id(id) do
  664. %Object{} = object -> build_flag_object(object)
  665. _ -> %{"id" => id, "deleted" => true}
  666. end
  667. end
  668. end
  669. defp build_flag_object(_), do: []
  670. defp build_flag_object_with_actor_and_id(%Object{data: data}, actor, id) do
  671. %{
  672. "type" => "Note",
  673. "id" => id,
  674. "content" => data["content"],
  675. "published" => data["published"],
  676. "actor" =>
  677. AccountView.render(
  678. "show.json",
  679. %{user: actor, skip_visibility_check: true}
  680. )
  681. }
  682. end
  683. #### Report-related helpers
  684. def get_reports(params, page, page_size) do
  685. params =
  686. params
  687. |> Map.put(:type, "Flag")
  688. |> Map.put(:skip_preload, true)
  689. |> Map.put(:preload_report_notes, true)
  690. |> Map.put(:total, true)
  691. |> Map.put(:limit, page_size)
  692. |> Map.put(:offset, (page - 1) * page_size)
  693. ActivityPub.fetch_activities([], params, :offset)
  694. end
  695. defp maybe_strip_report_status(data, state) do
  696. with true <- Config.get([:instance, :report_strip_status]),
  697. true <- state in @strip_status_report_states,
  698. {:ok, stripped_activity} = strip_report_status_data(%Activity{data: data}) do
  699. data |> Map.put("object", stripped_activity.data["object"])
  700. else
  701. _ -> data
  702. end
  703. end
  704. def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
  705. new_data =
  706. activity.data
  707. |> Map.put("state", state)
  708. |> maybe_strip_report_status(state)
  709. activity
  710. |> Changeset.change(data: new_data)
  711. |> Repo.update()
  712. end
  713. def update_report_state(activity_ids, state) when state in @supported_report_states do
  714. activities_num = length(activity_ids)
  715. from(a in Activity, where: a.id in ^activity_ids)
  716. |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
  717. |> Repo.update_all([])
  718. |> case do
  719. {^activities_num, _} -> :ok
  720. _ -> {:error, activity_ids}
  721. end
  722. end
  723. def update_report_state(_, _), do: {:error, "Unsupported state"}
  724. def strip_report_status_data(activity) do
  725. [actor | reported_activities] = activity.data["object"]
  726. stripped_activities =
  727. Enum.reduce(reported_activities, [], fn act, acc ->
  728. case ObjectID.cast(act) do
  729. {:ok, act} -> [act | acc]
  730. _ -> acc
  731. end
  732. end)
  733. new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
  734. {:ok, %{activity | data: new_data}}
  735. end
  736. def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
  737. [to, cc, recipients] =
  738. activity
  739. |> get_updated_targets(visibility)
  740. |> Enum.map(&Enum.uniq/1)
  741. object_data =
  742. activity.object.data
  743. |> Map.put("to", to)
  744. |> Map.put("cc", cc)
  745. {:ok, object} =
  746. activity.object
  747. |> Object.change(%{data: object_data})
  748. |> Object.update_and_set_cache()
  749. activity_data =
  750. activity.data
  751. |> Map.put("to", to)
  752. |> Map.put("cc", cc)
  753. activity
  754. |> Map.put(:object, object)
  755. |> Activity.change(%{data: activity_data, recipients: recipients})
  756. |> Repo.update()
  757. end
  758. def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
  759. defp get_updated_targets(
  760. %Activity{data: %{"to" => to} = data, recipients: recipients},
  761. visibility
  762. ) do
  763. cc = Map.get(data, "cc", [])
  764. follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
  765. public = Pleroma.Constants.as_public()
  766. case visibility do
  767. "public" ->
  768. to = [public | List.delete(to, follower_address)]
  769. cc = [follower_address | List.delete(cc, public)]
  770. recipients = [public | recipients]
  771. [to, cc, recipients]
  772. "private" ->
  773. to = [follower_address | List.delete(to, public)]
  774. cc = List.delete(cc, public)
  775. recipients = List.delete(recipients, public)
  776. [to, cc, recipients]
  777. "unlisted" ->
  778. to = [follower_address | List.delete(to, public)]
  779. cc = [public | List.delete(cc, follower_address)]
  780. recipients = recipients ++ [follower_address, public]
  781. [to, cc, recipients]
  782. _ ->
  783. [to, cc, recipients]
  784. end
  785. end
  786. def get_existing_votes(actor, %{data: %{"id" => id}}) do
  787. actor
  788. |> Activity.Queries.by_actor()
  789. |> Activity.Queries.by_type("Create")
  790. |> Activity.with_preloaded_object()
  791. |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
  792. |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
  793. |> Repo.all()
  794. end
  795. def maybe_handle_group_posts(activity) do
  796. poster = User.get_cached_by_ap_id(activity.actor)
  797. mentions =
  798. activity.data["to"]
  799. |> Enum.filter(&(&1 != activity.actor))
  800. mentioned_local_groups =
  801. User.get_all_by_ap_id(mentions)
  802. |> Enum.filter(fn user ->
  803. user.actor_type == "Group" and
  804. user.local and
  805. not User.blocks?(user, poster)
  806. end)
  807. mentioned_local_groups
  808. |> Enum.each(fn group ->
  809. Pleroma.Web.CommonAPI.repeat(activity.id, group)
  810. end)
  811. :ok
  812. end
  813. end