logo

pleroma

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

builder.ex (11738B)


  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.Builder do
  5. @moduledoc """
  6. This module builds the objects. Meant to be used for creating local objects.
  7. This module encodes our addressing policies and general shape of our objects.
  8. """
  9. alias Pleroma.Activity
  10. alias Pleroma.Emoji
  11. alias Pleroma.Object
  12. alias Pleroma.User
  13. alias Pleroma.Web.ActivityPub.Relay
  14. alias Pleroma.Web.ActivityPub.Utils
  15. alias Pleroma.Web.ActivityPub.Visibility
  16. alias Pleroma.Web.CommonAPI.ActivityDraft
  17. alias Pleroma.Web.Endpoint
  18. require Pleroma.Constants
  19. def accept_or_reject(actor, activity, type) do
  20. data = %{
  21. "id" => Utils.generate_activity_id(),
  22. "actor" => actor.ap_id,
  23. "type" => type,
  24. "object" => activity.data["id"],
  25. "to" => [activity.actor]
  26. }
  27. {:ok, data, []}
  28. end
  29. @spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
  30. def reject(actor, rejected_activity) do
  31. accept_or_reject(actor, rejected_activity, "Reject")
  32. end
  33. @spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
  34. def accept(actor, accepted_activity) do
  35. accept_or_reject(actor, accepted_activity, "Accept")
  36. end
  37. @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
  38. def follow(follower, followed) do
  39. data = %{
  40. "id" => Utils.generate_activity_id(),
  41. "actor" => follower.ap_id,
  42. "type" => "Follow",
  43. "object" => followed.ap_id,
  44. "to" => [followed.ap_id]
  45. }
  46. {:ok, data, []}
  47. end
  48. defp unicode_emoji_react(_object, data, emoji) do
  49. data
  50. |> Map.put("content", emoji)
  51. |> Map.put("type", "EmojiReact")
  52. end
  53. defp add_emoji_content(data, emoji, url) do
  54. tag = [
  55. %{
  56. "id" => url,
  57. "type" => "Emoji",
  58. "name" => Emoji.maybe_quote(emoji),
  59. "icon" => %{
  60. "type" => "Image",
  61. "url" => url
  62. }
  63. }
  64. ]
  65. data
  66. |> Map.put("content", Emoji.maybe_quote(emoji))
  67. |> Map.put("type", "EmojiReact")
  68. |> Map.put("tag", tag)
  69. end
  70. defp remote_custom_emoji_react(
  71. %{data: %{"reactions" => existing_reactions}},
  72. data,
  73. emoji
  74. ) do
  75. [emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
  76. matching_reaction =
  77. Enum.find(
  78. existing_reactions,
  79. fn [name, _, url] ->
  80. if url != nil do
  81. url = URI.parse(url)
  82. url.host == instance && name == emoji_code
  83. end
  84. end
  85. )
  86. if matching_reaction do
  87. [name, _, url] = matching_reaction
  88. add_emoji_content(data, name, url)
  89. else
  90. {:error, "Could not react"}
  91. end
  92. end
  93. defp remote_custom_emoji_react(_object, _data, _emoji) do
  94. {:error, "Could not react"}
  95. end
  96. defp local_custom_emoji_react(data, emoji) do
  97. with %{file: path} = emojo <- Emoji.get(emoji) do
  98. url = "#{Endpoint.url()}#{path}"
  99. add_emoji_content(data, emojo.code, url)
  100. else
  101. _ -> {:error, "Emoji does not exist"}
  102. end
  103. end
  104. defp custom_emoji_react(object, data, emoji) do
  105. if String.contains?(emoji, "@") do
  106. remote_custom_emoji_react(object, data, emoji)
  107. else
  108. local_custom_emoji_react(data, emoji)
  109. end
  110. end
  111. @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
  112. def emoji_react(actor, object, emoji) do
  113. with {:ok, data, meta} <- object_action(actor, object) do
  114. data =
  115. if Emoji.unicode?(emoji) do
  116. unicode_emoji_react(object, data, emoji)
  117. else
  118. custom_emoji_react(object, data, emoji)
  119. end
  120. {:ok, data, meta}
  121. end
  122. end
  123. @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
  124. def undo(actor, object) do
  125. {:ok,
  126. %{
  127. "id" => Utils.generate_activity_id(),
  128. "actor" => actor.ap_id,
  129. "type" => "Undo",
  130. "object" => object.data["id"],
  131. "to" => object.data["to"] || [],
  132. "cc" => object.data["cc"] || []
  133. }, []}
  134. end
  135. @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
  136. def delete(actor, object_id) do
  137. object = Object.normalize(object_id, fetch: false)
  138. user = !object && User.get_cached_by_ap_id(object_id)
  139. to =
  140. case {object, user} do
  141. {%Object{}, _} ->
  142. # We are deleting an object, address everyone who was originally mentioned
  143. (object.data["to"] || []) ++ (object.data["cc"] || [])
  144. {_, %User{follower_address: follower_address}} ->
  145. # We are deleting a user, address the followers of that user
  146. [follower_address]
  147. end
  148. {:ok,
  149. %{
  150. "id" => Utils.generate_activity_id(),
  151. "actor" => actor.ap_id,
  152. "object" => object_id,
  153. "to" => to,
  154. "type" => "Delete"
  155. }, []}
  156. end
  157. def create(actor, object, recipients) do
  158. context =
  159. if is_map(object) do
  160. object["context"]
  161. else
  162. nil
  163. end
  164. {:ok,
  165. %{
  166. "id" => Utils.generate_activity_id(),
  167. "actor" => actor.ap_id,
  168. "to" => recipients,
  169. "object" => object,
  170. "type" => "Create",
  171. "published" => DateTime.utc_now() |> DateTime.to_iso8601()
  172. }
  173. |> Pleroma.Maps.put_if_present("context", context), []}
  174. end
  175. @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
  176. def note(%ActivityDraft{} = draft) do
  177. data =
  178. %{
  179. "type" => "Note",
  180. "to" => draft.to,
  181. "cc" => draft.cc,
  182. "content" => draft.content_html,
  183. "summary" => draft.summary,
  184. "sensitive" => draft.sensitive,
  185. "context" => draft.context,
  186. "attachment" => draft.attachments,
  187. "actor" => draft.user.ap_id,
  188. "tag" => Keyword.values(draft.tags) |> Enum.uniq()
  189. }
  190. |> add_in_reply_to(draft.in_reply_to)
  191. |> add_quote(draft.quote_post)
  192. |> Map.merge(draft.extra)
  193. {:ok, data, []}
  194. end
  195. defp add_in_reply_to(object, nil), do: object
  196. defp add_in_reply_to(object, in_reply_to) do
  197. with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
  198. Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
  199. else
  200. _ -> object
  201. end
  202. end
  203. defp add_quote(object, nil), do: object
  204. defp add_quote(object, quote_post) do
  205. with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do
  206. Map.put(object, "quoteUrl", quote_object.data["id"])
  207. else
  208. _ -> object
  209. end
  210. end
  211. def chat_message(actor, recipient, content, opts \\ []) do
  212. basic = %{
  213. "id" => Utils.generate_object_id(),
  214. "actor" => actor.ap_id,
  215. "type" => "ChatMessage",
  216. "to" => [recipient],
  217. "content" => content,
  218. "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
  219. "emoji" => Emoji.Formatter.get_emoji_map(content)
  220. }
  221. case opts[:attachment] do
  222. %Object{data: attachment_data} ->
  223. {
  224. :ok,
  225. Map.put(basic, "attachment", attachment_data),
  226. []
  227. }
  228. _ ->
  229. {:ok, basic, []}
  230. end
  231. end
  232. def answer(user, object, name) do
  233. {:ok,
  234. %{
  235. "type" => "Answer",
  236. "actor" => user.ap_id,
  237. "attributedTo" => user.ap_id,
  238. "cc" => [object.data["actor"]],
  239. "to" => [],
  240. "name" => name,
  241. "inReplyTo" => object.data["id"],
  242. "context" => object.data["context"],
  243. "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
  244. "id" => Utils.generate_object_id()
  245. }, []}
  246. end
  247. @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
  248. def tombstone(actor, id) do
  249. {:ok,
  250. %{
  251. "id" => id,
  252. "actor" => actor,
  253. "type" => "Tombstone"
  254. }, []}
  255. end
  256. @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
  257. def like(actor, object) do
  258. with {:ok, data, meta} <- object_action(actor, object) do
  259. data =
  260. data
  261. |> Map.put("type", "Like")
  262. {:ok, data, meta}
  263. end
  264. end
  265. @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
  266. def update(actor, object) do
  267. {to, cc} =
  268. if object["type"] in Pleroma.Constants.actor_types() do
  269. # User updates, always public
  270. {[Pleroma.Constants.as_public(), actor.follower_address], []}
  271. else
  272. # Status updates, follow the recipients in the object
  273. {object["to"] || [], object["cc"] || []}
  274. end
  275. {:ok,
  276. %{
  277. "id" => Utils.generate_activity_id(),
  278. "type" => "Update",
  279. "actor" => actor.ap_id,
  280. "object" => object,
  281. "to" => to,
  282. "cc" => cc
  283. }, []}
  284. end
  285. @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
  286. def block(blocker, blocked) do
  287. {:ok,
  288. %{
  289. "id" => Utils.generate_activity_id(),
  290. "type" => "Block",
  291. "actor" => blocker.ap_id,
  292. "object" => blocked.ap_id,
  293. "to" => [blocked.ap_id]
  294. }, []}
  295. end
  296. @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
  297. def announce(actor, object, options \\ []) do
  298. public? = Keyword.get(options, :public, false)
  299. to =
  300. cond do
  301. actor.ap_id == Relay.ap_id() ->
  302. [actor.follower_address]
  303. public? and Visibility.local_public?(object) ->
  304. [actor.follower_address, object.data["actor"], Utils.as_local_public()]
  305. public? ->
  306. [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
  307. true ->
  308. [actor.follower_address, object.data["actor"]]
  309. end
  310. {:ok,
  311. %{
  312. "id" => Utils.generate_activity_id(),
  313. "actor" => actor.ap_id,
  314. "object" => object.data["id"],
  315. "to" => to,
  316. "context" => object.data["context"],
  317. "type" => "Announce",
  318. "published" => Utils.make_date()
  319. }, []}
  320. end
  321. @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
  322. defp object_action(actor, object) do
  323. object_actor = User.get_cached_by_ap_id(object.data["actor"])
  324. # Address the actor of the object, and our actor's follower collection if the post is public.
  325. to =
  326. if Visibility.public?(object) do
  327. [actor.follower_address, object.data["actor"]]
  328. else
  329. [object.data["actor"]]
  330. end
  331. # CC everyone who's been addressed in the object, except ourself and the object actor's
  332. # follower collection
  333. cc =
  334. (object.data["to"] ++ (object.data["cc"] || []))
  335. |> List.delete(actor.ap_id)
  336. |> List.delete(object_actor.follower_address)
  337. {:ok,
  338. %{
  339. "id" => Utils.generate_activity_id(),
  340. "actor" => actor.ap_id,
  341. "object" => object.data["id"],
  342. "to" => to,
  343. "cc" => cc,
  344. "context" => object.data["context"]
  345. }, []}
  346. end
  347. @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
  348. def pin(%User{} = user, object) do
  349. {:ok,
  350. %{
  351. "id" => Utils.generate_activity_id(),
  352. "target" => pinned_url(user.nickname),
  353. "object" => object.data["id"],
  354. "actor" => user.ap_id,
  355. "type" => "Add",
  356. "to" => [Pleroma.Constants.as_public()],
  357. "cc" => [user.follower_address]
  358. }, []}
  359. end
  360. @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
  361. def unpin(%User{} = user, object) do
  362. {:ok,
  363. %{
  364. "id" => Utils.generate_activity_id(),
  365. "target" => pinned_url(user.nickname),
  366. "object" => object.data["id"],
  367. "actor" => user.ap_id,
  368. "type" => "Remove",
  369. "to" => [Pleroma.Constants.as_public()],
  370. "cc" => [user.follower_address]
  371. }, []}
  372. end
  373. defp pinned_url(nickname) when is_binary(nickname) do
  374. Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
  375. end
  376. end