logo

pleroma

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

status_view.ex (25936B)


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