logo

pleroma

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

status_view.ex (26300B)


  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.MastodonAPI.StatusView do
  5. use Pleroma.Web, :view
  6. require Pleroma.Constants
  7. alias Pleroma.Activity
  8. alias Pleroma.HTML
  9. alias Pleroma.Maps
  10. alias Pleroma.Object
  11. alias Pleroma.Repo
  12. alias Pleroma.User
  13. alias Pleroma.UserRelationship
  14. alias Pleroma.Web.CommonAPI
  15. alias Pleroma.Web.CommonAPI.Utils
  16. alias Pleroma.Web.MastodonAPI.AccountView
  17. alias Pleroma.Web.MastodonAPI.PollView
  18. alias Pleroma.Web.MastodonAPI.StatusView
  19. alias Pleroma.Web.MediaProxy
  20. alias Pleroma.Web.PleromaAPI.EmojiReactionController
  21. alias Pleroma.Web.RichMedia.Card
  22. import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
  23. # This is a naive way to do this, just spawning a process per activity
  24. # to fetch the preview. However it should be fine considering
  25. # pagination is restricted to 40 activities at a time
  26. defp fetch_rich_media_for_activities(activities) do
  27. Enum.each(activities, fn activity ->
  28. Card.get_by_activity(activity)
  29. end)
  30. end
  31. # TODO: Add cached version.
  32. defp get_replied_to_activities([]), do: %{}
  33. defp get_replied_to_activities(activities) do
  34. activities
  35. |> Enum.map(fn
  36. %{data: %{"type" => "Create"}} = activity ->
  37. object = Object.normalize(activity, fetch: false)
  38. object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
  39. _ ->
  40. nil
  41. end)
  42. |> Enum.filter(& &1)
  43. |> Activity.create_by_object_ap_id_with_object()
  44. |> Repo.all()
  45. |> Enum.reduce(%{}, fn activity, acc ->
  46. object = Object.normalize(activity, fetch: false)
  47. if object, do: Map.put(acc, object.data["id"], activity), else: acc
  48. end)
  49. end
  50. defp get_quoted_activities([]), do: %{}
  51. defp get_quoted_activities(activities) do
  52. activities
  53. |> Enum.map(fn
  54. %{data: %{"type" => "Create"}} = activity ->
  55. object = Object.normalize(activity, fetch: false)
  56. object && object.data["quoteUrl"] != "" && object.data["quoteUrl"]
  57. _ ->
  58. nil
  59. end)
  60. |> Enum.filter(& &1)
  61. |> Activity.create_by_object_ap_id_with_object()
  62. |> Repo.all()
  63. |> Enum.reduce(%{}, fn activity, acc ->
  64. object = Object.normalize(activity, fetch: false)
  65. if object, do: Map.put(acc, object.data["id"], activity), else: acc
  66. end)
  67. end
  68. # DEPRECATED This field seems to be a left-over from the StatusNet era.
  69. # If your application uses `pleroma.conversation_id`: this field is deprecated.
  70. # It is currently stubbed instead by doing a CRC32 of the context, and
  71. # clearing the MSB to avoid overflow exceptions with signed integers on the
  72. # different clients using this field (Java/Kotlin code, mostly; see Husky.)
  73. # This should be removed in a future version of Pleroma. Pleroma-FE currently
  74. # depends on this field, as well.
  75. defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do
  76. import Bitwise
  77. :erlang.crc32(context)
  78. |> band(bnot(0x8000_0000))
  79. end
  80. defp get_context_id(_), do: nil
  81. # Check if the user reblogged this status
  82. defp reblogged?(activity, %User{ap_id: ap_id}) do
  83. with %Object{data: %{"announcements" => announcements}} when is_list(announcements) <-
  84. Object.normalize(activity, fetch: false) do
  85. ap_id in announcements
  86. else
  87. _ -> false
  88. end
  89. end
  90. # False if the user is logged out
  91. defp reblogged?(_activity, _user), do: false
  92. def render("index.json", opts) do
  93. reading_user = opts[:for]
  94. # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
  95. activities = Enum.filter(opts.activities, & &1)
  96. # Start prefetching rich media before doing anything else
  97. fetch_rich_media_for_activities(activities)
  98. replied_to_activities = get_replied_to_activities(activities)
  99. quoted_activities = get_quoted_activities(activities)
  100. parent_activities =
  101. activities
  102. |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
  103. |> Enum.map(&Object.normalize(&1, fetch: false).data["id"])
  104. |> Activity.create_by_object_ap_id()
  105. |> Activity.with_preloaded_object(:left)
  106. |> Activity.with_preloaded_bookmark(reading_user)
  107. |> Activity.with_set_thread_muted_field(reading_user)
  108. |> Repo.all()
  109. relationships_opt =
  110. cond do
  111. Map.has_key?(opts, :relationships) ->
  112. opts[:relationships]
  113. is_nil(reading_user) ->
  114. UserRelationship.view_relationships_option(nil, [])
  115. true ->
  116. # Note: unresolved users are filtered out
  117. actors =
  118. (activities ++ parent_activities)
  119. |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
  120. |> Enum.filter(& &1)
  121. UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
  122. end
  123. opts =
  124. opts
  125. |> Map.put(:replied_to_activities, replied_to_activities)
  126. |> Map.put(:quoted_activities, quoted_activities)
  127. |> Map.put(:parent_activities, parent_activities)
  128. |> Map.put(:relationships, relationships_opt)
  129. safe_render_many(activities, StatusView, "show.json", opts)
  130. end
  131. def render(
  132. "show.json",
  133. %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
  134. ) do
  135. user = CommonAPI.get_user(activity.data["actor"])
  136. created_at = Utils.to_masto_date(activity.data["published"])
  137. object = Object.normalize(activity, fetch: false)
  138. reblogged_parent_activity =
  139. if opts[:parent_activities] do
  140. Activity.Queries.find_by_object_ap_id(
  141. opts[:parent_activities],
  142. object.data["id"]
  143. )
  144. else
  145. Activity.create_by_object_ap_id(object.data["id"])
  146. |> Activity.with_preloaded_bookmark(opts[:for])
  147. |> Activity.with_set_thread_muted_field(opts[:for])
  148. |> Repo.one()
  149. end
  150. reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
  151. reblogged = render("show.json", reblog_rendering_opts)
  152. favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
  153. bookmark = Activity.get_bookmark(reblogged_parent_activity, opts[:for])
  154. bookmark_folder =
  155. if bookmark != nil do
  156. bookmark.folder_id
  157. else
  158. nil
  159. end
  160. mentions =
  161. activity.recipients
  162. |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
  163. |> Enum.filter(& &1)
  164. |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
  165. {pinned?, pinned_at} = pin_data(object, user)
  166. %{
  167. id: to_string(activity.id),
  168. uri: object.data["id"],
  169. url: object.data["id"],
  170. account:
  171. AccountView.render("show.json", %{
  172. user: user,
  173. for: opts[:for]
  174. }),
  175. in_reply_to_id: nil,
  176. in_reply_to_account_id: nil,
  177. reblog: reblogged,
  178. content: reblogged[:content] || "",
  179. created_at: created_at,
  180. reblogs_count: 0,
  181. replies_count: 0,
  182. favourites_count: 0,
  183. reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
  184. favourited: present?(favorited),
  185. bookmarked: present?(bookmark),
  186. muted: false,
  187. pinned: pinned?,
  188. sensitive: false,
  189. spoiler_text: "",
  190. visibility: get_visibility(activity),
  191. media_attachments: reblogged[:media_attachments] || [],
  192. mentions: mentions,
  193. tags: reblogged[:tags] || [],
  194. application: build_application(object.data["generator"]),
  195. language: get_language(object),
  196. emojis: [],
  197. pleroma: %{
  198. local: activity.local,
  199. pinned_at: pinned_at,
  200. bookmark_folder: bookmark_folder
  201. }
  202. }
  203. end
  204. def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
  205. object = Object.normalize(activity, fetch: false)
  206. user = CommonAPI.get_user(activity.data["actor"])
  207. user_follower_address = user.follower_address
  208. like_count = object.data["like_count"] || 0
  209. announcement_count = object.data["announcement_count"] || 0
  210. hashtags = Object.hashtags(object)
  211. sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
  212. tags = Object.tags(object)
  213. tag_mentions =
  214. tags
  215. |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
  216. |> Enum.map(fn tag -> tag["href"] end)
  217. mentions =
  218. (object.data["to"] ++ tag_mentions)
  219. |> Enum.uniq()
  220. |> Enum.map(fn
  221. Pleroma.Constants.as_public() -> nil
  222. ^user_follower_address -> nil
  223. ap_id -> User.get_cached_by_ap_id(ap_id)
  224. end)
  225. |> Enum.filter(& &1)
  226. |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
  227. favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
  228. bookmark = Activity.get_bookmark(activity, opts[:for])
  229. bookmark_folder =
  230. if bookmark != nil do
  231. bookmark.folder_id
  232. else
  233. nil
  234. end
  235. client_posted_this_activity = opts[:for] && user.id == opts[:for].id
  236. expires_at =
  237. with true <- client_posted_this_activity,
  238. %Oban.Job{scheduled_at: scheduled_at} <-
  239. Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
  240. scheduled_at
  241. else
  242. _ -> nil
  243. end
  244. thread_muted? =
  245. cond do
  246. is_nil(opts[:for]) -> false
  247. is_boolean(activity.thread_muted?) -> activity.thread_muted?
  248. true -> CommonAPI.thread_muted?(activity, opts[:for])
  249. end
  250. attachment_data = object.data["attachment"] || []
  251. attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
  252. created_at = Utils.to_masto_date(object.data["published"])
  253. edited_at =
  254. with %{"updated" => updated} <- object.data,
  255. date <- Utils.to_masto_date(updated),
  256. true <- date != "" do
  257. date
  258. else
  259. _ ->
  260. nil
  261. end
  262. reply_to = get_reply_to(activity, opts)
  263. reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
  264. history_len =
  265. 1 +
  266. (Object.Updater.history_for(object.data)
  267. |> Map.get("orderedItems")
  268. |> length())
  269. # See render("history.json", ...) for more details
  270. # Here the implicit index of the current content is 0
  271. chrono_order = history_len - 1
  272. quote_activity = get_quote(activity, opts)
  273. quote_id =
  274. case quote_activity do
  275. %Activity{id: id} -> id
  276. _ -> nil
  277. end
  278. quote_post =
  279. if visible_for_user?(quote_activity, opts[:for]) and opts[:show_quote] != false do
  280. quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false})
  281. render("show.json", quote_rendering_opts)
  282. else
  283. nil
  284. end
  285. content =
  286. object
  287. |> render_content()
  288. content_html =
  289. content
  290. |> Activity.HTML.get_cached_scrubbed_html_for_activity(
  291. User.html_filter_policy(opts[:for]),
  292. activity,
  293. "mastoapi:content:#{chrono_order}"
  294. )
  295. content_plaintext =
  296. content
  297. |> Activity.HTML.get_cached_stripped_html_for_activity(
  298. activity,
  299. "mastoapi:content:#{chrono_order}"
  300. )
  301. summary = object.data["summary"] || ""
  302. card =
  303. case Card.get_by_activity(activity) do
  304. %Card{} = result -> render("card.json", result)
  305. _ -> nil
  306. end
  307. url =
  308. if user.local do
  309. Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
  310. else
  311. object.data["url"] || object.data["external_url"] || object.data["id"]
  312. end
  313. direct_conversation_id =
  314. with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
  315. {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
  316. {_, %User{} = for_user} <- {:for_user, opts[:for]} do
  317. Activity.direct_conversation_id(activity, for_user)
  318. else
  319. {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
  320. participation_id
  321. _e ->
  322. nil
  323. end
  324. emoji_reactions =
  325. object
  326. |> Object.get_emoji_reactions()
  327. |> EmojiReactionController.filter_allowed_users(
  328. opts[:for],
  329. Map.get(opts, :with_muted, false)
  330. )
  331. |> Stream.map(fn {emoji, users, url} ->
  332. build_emoji_map(emoji, users, url, opts[:for])
  333. end)
  334. |> Enum.to_list()
  335. # Status muted state (would do 1 request per status unless user mutes are preloaded)
  336. muted =
  337. thread_muted? ||
  338. UserRelationship.exists?(
  339. get_in(opts, [:relationships, :user_relationships]),
  340. :mute,
  341. opts[:for],
  342. user,
  343. fn for_user, user -> User.mutes?(for_user, user) end
  344. )
  345. {pinned?, pinned_at} = pin_data(object, user)
  346. %{
  347. id: to_string(activity.id),
  348. uri: object.data["id"],
  349. url: url,
  350. account:
  351. AccountView.render("show.json", %{
  352. user: user,
  353. for: opts[:for]
  354. }),
  355. in_reply_to_id: reply_to && to_string(reply_to.id),
  356. in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
  357. reblog: nil,
  358. card: card,
  359. content: content_html,
  360. text: opts[:with_source] && get_source_text(object.data["source"]),
  361. created_at: created_at,
  362. edited_at: edited_at,
  363. reblogs_count: announcement_count,
  364. replies_count: object.data["repliesCount"] || 0,
  365. favourites_count: like_count,
  366. reblogged: reblogged?(activity, opts[:for]),
  367. favourited: present?(favorited),
  368. bookmarked: present?(bookmark),
  369. muted: muted,
  370. pinned: pinned?,
  371. sensitive: sensitive,
  372. spoiler_text: summary,
  373. visibility: get_visibility(object),
  374. media_attachments: attachments,
  375. poll: render(PollView, "show.json", object: object, for: opts[:for]),
  376. mentions: mentions,
  377. tags: build_tags(tags),
  378. application: build_application(object.data["generator"]),
  379. language: get_language(object),
  380. emojis: build_emojis(object.data["emoji"]),
  381. pleroma: %{
  382. local: activity.local,
  383. conversation_id: get_context_id(activity),
  384. context: object.data["context"],
  385. in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
  386. quote: quote_post,
  387. quote_id: quote_id,
  388. quote_url: object.data["quoteUrl"],
  389. quote_visible: visible_for_user?(quote_activity, opts[:for]),
  390. content: %{"text/plain" => content_plaintext},
  391. spoiler_text: %{"text/plain" => summary},
  392. expires_at: expires_at,
  393. direct_conversation_id: direct_conversation_id,
  394. thread_muted: thread_muted?,
  395. emoji_reactions: emoji_reactions,
  396. parent_visible: visible_for_user?(reply_to, opts[:for]),
  397. pinned_at: pinned_at,
  398. quotes_count: object.data["quotesCount"] || 0,
  399. bookmark_folder: bookmark_folder,
  400. list_id: get_list_id(object, client_posted_this_activity)
  401. }
  402. }
  403. end
  404. def render("show.json", _) do
  405. nil
  406. end
  407. def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
  408. object = Object.normalize(activity, fetch: false)
  409. hashtags = Object.hashtags(object)
  410. user = CommonAPI.get_user(activity.data["actor"])
  411. past_history =
  412. Object.Updater.history_for(object.data)
  413. |> Map.get("orderedItems")
  414. |> Enum.map(&Map.put(&1, "id", object.data["id"]))
  415. |> Enum.map(&%Object{data: &1, id: object.id})
  416. history =
  417. [object | past_history]
  418. # Mastodon expects the original to be at the first
  419. |> Enum.reverse()
  420. |> Enum.with_index()
  421. |> Enum.map(fn {object, chrono_order} ->
  422. %{
  423. # The history is prepended every time there is a new edit.
  424. # In chrono_order, the oldest item is always at 0, and so on.
  425. # The chrono_order is an invariant kept between edits.
  426. chrono_order: chrono_order,
  427. object: object
  428. }
  429. end)
  430. individual_opts =
  431. opts
  432. |> Map.put(:as, :item)
  433. |> Map.put(:user, user)
  434. |> Map.put(:hashtags, hashtags)
  435. render_many(history, StatusView, "history_item.json", individual_opts)
  436. end
  437. def render(
  438. "history_item.json",
  439. %{
  440. activity: activity,
  441. user: user,
  442. item: %{object: object, chrono_order: chrono_order},
  443. hashtags: hashtags
  444. } = opts
  445. ) do
  446. sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
  447. attachment_data = object.data["attachment"] || []
  448. attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
  449. created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
  450. content =
  451. object
  452. |> render_content()
  453. content_html =
  454. content
  455. |> Activity.HTML.get_cached_scrubbed_html_for_activity(
  456. User.html_filter_policy(opts[:for]),
  457. activity,
  458. "mastoapi:content:#{chrono_order}"
  459. )
  460. summary = object.data["summary"] || ""
  461. %{
  462. account:
  463. AccountView.render("show.json", %{
  464. user: user,
  465. for: opts[:for]
  466. }),
  467. content: content_html,
  468. sensitive: sensitive,
  469. spoiler_text: summary,
  470. created_at: created_at,
  471. media_attachments: attachments,
  472. emojis: build_emojis(object.data["emoji"]),
  473. poll: render(PollView, "show.json", object: object, for: opts[:for])
  474. }
  475. end
  476. def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
  477. object = Object.normalize(activity, fetch: false)
  478. %{
  479. id: activity.id,
  480. text: get_source_text(Map.get(object.data, "source", "")),
  481. spoiler_text: Map.get(object.data, "summary", ""),
  482. content_type: get_source_content_type(object.data["source"])
  483. }
  484. end
  485. def render("card.json", %Card{fields: rich_media}) do
  486. page_url_data = URI.parse(rich_media["url"])
  487. page_url = page_url_data |> to_string
  488. image_url = proxied_url(rich_media["image"], page_url_data)
  489. audio_url = proxied_url(rich_media["audio"], page_url_data)
  490. video_url = proxied_url(rich_media["video"], page_url_data)
  491. %{
  492. type: "link",
  493. provider_name: page_url_data.host,
  494. provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
  495. url: page_url,
  496. image: image_url,
  497. image_description: rich_media["image:alt"] || "",
  498. title: rich_media["title"] || "",
  499. description: rich_media["description"] || "",
  500. pleroma: %{
  501. opengraph:
  502. rich_media
  503. |> Maps.put_if_present("image", image_url)
  504. |> Maps.put_if_present("audio", audio_url)
  505. |> Maps.put_if_present("video", video_url)
  506. }
  507. }
  508. end
  509. def render("card.json", _), do: nil
  510. def render("attachment.json", %{attachment: attachment}) do
  511. [attachment_url | _] = attachment["url"]
  512. media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
  513. href = attachment_url["href"] |> MediaProxy.url()
  514. href_preview = attachment_url["href"] |> MediaProxy.preview_url()
  515. meta = render("attachment_meta.json", %{attachment: attachment})
  516. type =
  517. cond do
  518. String.contains?(media_type, "image") -> "image"
  519. String.contains?(media_type, "video") -> "video"
  520. String.contains?(media_type, "audio") -> "audio"
  521. true -> "unknown"
  522. end
  523. attachment_id =
  524. with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
  525. {_, %Object{data: _object_data, id: object_id}} <-
  526. {:object, Object.get_by_ap_id(ap_id)} do
  527. to_string(object_id)
  528. else
  529. _ ->
  530. <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
  531. to_string(attachment["id"] || hash_id)
  532. end
  533. description =
  534. if attachment["summary"] do
  535. HTML.strip_tags(attachment["summary"])
  536. else
  537. attachment["name"]
  538. end
  539. name = if attachment["summary"], do: attachment["name"]
  540. pleroma =
  541. %{mime_type: media_type}
  542. |> Maps.put_if_present(:name, name)
  543. %{
  544. id: attachment_id,
  545. url: href,
  546. remote_url: href,
  547. preview_url: href_preview,
  548. text_url: href,
  549. type: type,
  550. description: description,
  551. pleroma: pleroma,
  552. blurhash: attachment["blurhash"]
  553. }
  554. |> Maps.put_if_present(:meta, meta)
  555. end
  556. def render("attachment_meta.json", %{
  557. attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
  558. })
  559. when is_integer(width) and is_integer(height) do
  560. %{
  561. original: %{
  562. width: width,
  563. height: height,
  564. aspect: width / height
  565. }
  566. }
  567. end
  568. def render("attachment_meta.json", _), do: nil
  569. def render("context.json", %{activity: activity, activities: activities, user: user}) do
  570. %{ancestors: ancestors, descendants: descendants} =
  571. activities
  572. |> Enum.reverse()
  573. |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
  574. |> Map.put_new(:ancestors, [])
  575. |> Map.put_new(:descendants, [])
  576. %{
  577. ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
  578. descendants: render("index.json", for: user, activities: descendants, as: :activity)
  579. }
  580. end
  581. def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
  582. object = Object.normalize(activity, fetch: false)
  583. with nil <- replied_to_activities[object.data["inReplyTo"]] do
  584. # If user didn't participate in the thread
  585. Activity.get_in_reply_to_activity(activity)
  586. end
  587. end
  588. def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
  589. object = Object.normalize(activity, fetch: false)
  590. if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
  591. Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
  592. else
  593. nil
  594. end
  595. end
  596. def get_quote(activity, %{quoted_activities: quoted_activities}) do
  597. object = Object.normalize(activity, fetch: false)
  598. with nil <- quoted_activities[object.data["quoteUrl"]] do
  599. # For when a quote post is inside an Announce
  600. Activity.get_create_by_object_ap_id_with_object(object.data["quoteUrl"])
  601. end
  602. end
  603. def get_quote(%{data: %{"object" => _object}} = activity, _) do
  604. object = Object.normalize(activity, fetch: false)
  605. if object.data["quoteUrl"] && object.data["quoteUrl"] != "" do
  606. Activity.get_create_by_object_ap_id(object.data["quoteUrl"])
  607. else
  608. nil
  609. end
  610. end
  611. def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
  612. url = object.data["url"] || object.data["id"]
  613. "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
  614. end
  615. def render_content(object), do: object.data["content"] || ""
  616. @doc """
  617. Builds a dictionary tags.
  618. ## Examples
  619. iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
  620. [{"name": "fediverse", "url": "/tag/fediverse"},
  621. {"name": "nextcloud", "url": "/tag/nextcloud"}]
  622. """
  623. @spec build_tags(list(any())) :: list(map())
  624. def build_tags(object_tags) when is_list(object_tags) do
  625. object_tags
  626. |> Enum.filter(&is_binary/1)
  627. |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"})
  628. end
  629. def build_tags(_), do: []
  630. @doc """
  631. Builds list emojis.
  632. Arguments: `nil` or list tuple of name and url.
  633. Returns list emojis.
  634. ## Examples
  635. iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
  636. [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
  637. """
  638. @spec build_emojis(nil | list(tuple())) :: list(map())
  639. def build_emojis(nil), do: []
  640. def build_emojis(emojis) do
  641. emojis
  642. |> Enum.map(fn {name, url} ->
  643. name = HTML.strip_tags(name)
  644. url =
  645. url
  646. |> HTML.strip_tags()
  647. |> MediaProxy.url()
  648. %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
  649. end)
  650. end
  651. defp present?(nil), do: false
  652. defp present?(false), do: false
  653. defp present?(_), do: true
  654. defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
  655. if pinned_at = pinned_objects[object_id] do
  656. {true, Utils.to_masto_date(pinned_at)}
  657. else
  658. {false, nil}
  659. end
  660. end
  661. defp build_emoji_map(emoji, users, url, current_user) do
  662. %{
  663. name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
  664. count: length(users),
  665. url: MediaProxy.url(url),
  666. me: !!(current_user && current_user.ap_id in users),
  667. account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
  668. }
  669. end
  670. @spec build_application(map() | nil) :: map() | nil
  671. defp build_application(%{"type" => _type, "name" => name, "url" => url}),
  672. do: %{name: name, website: url}
  673. defp build_application(_), do: nil
  674. @spec build_image_url(URI.t(), URI.t()) :: String.t()
  675. defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
  676. URI.merge(page_url_data, image_url_data) |> to_string
  677. end
  678. defp get_source_text(%{"content" => content} = _source) do
  679. content
  680. end
  681. defp get_source_text(source) when is_binary(source) do
  682. source
  683. end
  684. defp get_source_text(_) do
  685. ""
  686. end
  687. defp get_source_content_type(%{"mediaType" => type} = _source) do
  688. type
  689. end
  690. defp get_source_content_type(_source) do
  691. Utils.get_content_type(nil)
  692. end
  693. defp get_language(%{data: %{"language" => "und"}}), do: nil
  694. defp get_language(object), do: object.data["language"]
  695. defp proxied_url(url, page_url_data) do
  696. if is_binary(url) do
  697. build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()
  698. else
  699. nil
  700. end
  701. end
  702. defp get_list_id(object, client_posted_this_activity) do
  703. with true <- client_posted_this_activity,
  704. %{data: %{"listMessage" => list_ap_id}} when is_binary(list_ap_id) <- object,
  705. %{id: list_id} <- Pleroma.List.get_by_ap_id(list_ap_id) do
  706. list_id
  707. else
  708. _ -> nil
  709. end
  710. end
  711. end