logo

pleroma

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

utils.ex (27638B)


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