Merge remote-tracking branch 'upstream/develop' into admin-create-users
[akkoma] / lib / pleroma / web / mastodon_api / views / status_view.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.StatusView do
6 use Pleroma.Web, :view
7
8 alias Pleroma.Activity
9 alias Pleroma.HTML
10 alias Pleroma.Object
11 alias Pleroma.Repo
12 alias Pleroma.User
13 alias Pleroma.Web.CommonAPI
14 alias Pleroma.Web.CommonAPI.Utils
15 alias Pleroma.Web.MastodonAPI.AccountView
16 alias Pleroma.Web.MastodonAPI.StatusView
17 alias Pleroma.Web.MediaProxy
18
19 import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
20
21 # TODO: Add cached version.
22 defp get_replied_to_activities(activities) do
23 activities
24 |> Enum.map(fn
25 %{data: %{"type" => "Create", "object" => object}} ->
26 object = Object.normalize(object)
27 object.data["inReplyTo"] != "" && object.data["inReplyTo"]
28
29 _ ->
30 nil
31 end)
32 |> Enum.filter(& &1)
33 |> Activity.create_by_object_ap_id()
34 |> Repo.all()
35 |> Enum.reduce(%{}, fn activity, acc ->
36 object = Object.normalize(activity)
37 Map.put(acc, object.data["id"], activity)
38 end)
39 end
40
41 defp get_user(ap_id) do
42 cond do
43 user = User.get_cached_by_ap_id(ap_id) ->
44 user
45
46 user = User.get_by_guessed_nickname(ap_id) ->
47 user
48
49 true ->
50 User.error_user(ap_id)
51 end
52 end
53
54 defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
55 do: context_id
56
57 defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
58 do: Utils.context_to_conversation_id(context)
59
60 defp get_context_id(_), do: nil
61
62 defp reblogged?(activity, user) do
63 object = Object.normalize(activity) || %{}
64 present?(user && user.ap_id in (object.data["announcements"] || []))
65 end
66
67 def render("index.json", opts) do
68 replied_to_activities = get_replied_to_activities(opts.activities)
69
70 opts.activities
71 |> safe_render_many(
72 StatusView,
73 "status.json",
74 Map.put(opts, :replied_to_activities, replied_to_activities)
75 )
76 end
77
78 def render(
79 "status.json",
80 %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
81 ) do
82 user = get_user(activity.data["actor"])
83 created_at = Utils.to_masto_date(activity.data["published"])
84 activity_object = Object.normalize(activity)
85
86 reblogged_activity =
87 Activity.create_by_object_ap_id(activity_object.data["id"])
88 |> Activity.with_preloaded_bookmark(opts[:for])
89 |> Repo.one()
90
91 reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
92
93 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
94
95 bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
96
97 mentions =
98 activity.recipients
99 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
100 |> Enum.filter(& &1)
101 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
102
103 %{
104 id: to_string(activity.id),
105 uri: activity_object.data["id"],
106 url: activity_object.data["id"],
107 account: AccountView.render("account.json", %{user: user}),
108 in_reply_to_id: nil,
109 in_reply_to_account_id: nil,
110 reblog: reblogged,
111 content: reblogged[:content] || "",
112 created_at: created_at,
113 reblogs_count: 0,
114 replies_count: 0,
115 favourites_count: 0,
116 reblogged: reblogged?(reblogged_activity, opts[:for]),
117 favourited: present?(favorited),
118 bookmarked: present?(bookmarked),
119 muted: false,
120 pinned: pinned?(activity, user),
121 sensitive: false,
122 spoiler_text: "",
123 visibility: "public",
124 media_attachments: reblogged[:media_attachments] || [],
125 mentions: mentions,
126 tags: reblogged[:tags] || [],
127 application: %{
128 name: "Web",
129 website: nil
130 },
131 language: nil,
132 emojis: [],
133 pleroma: %{
134 local: activity.local
135 }
136 }
137 end
138
139 def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
140 object = Object.normalize(activity)
141
142 user = get_user(activity.data["actor"])
143
144 like_count = object.data["like_count"] || 0
145 announcement_count = object.data["announcement_count"] || 0
146
147 tags = object.data["tag"] || []
148 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
149
150 mentions =
151 activity.recipients
152 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
153 |> Enum.filter(& &1)
154 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
155
156 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
157
158 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
159
160 thread_muted? =
161 case activity.thread_muted? do
162 thread_muted? when is_boolean(thread_muted?) -> thread_muted?
163 nil -> CommonAPI.thread_muted?(user, activity)
164 end
165
166 attachment_data = object.data["attachment"] || []
167 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
168
169 created_at = Utils.to_masto_date(object.data["published"])
170
171 reply_to = get_reply_to(activity, opts)
172
173 reply_to_user = reply_to && get_user(reply_to.data["actor"])
174
175 content =
176 object
177 |> render_content()
178
179 content_html =
180 content
181 |> HTML.get_cached_scrubbed_html_for_activity(
182 User.html_filter_policy(opts[:for]),
183 activity,
184 "mastoapi:content"
185 )
186
187 content_plaintext =
188 content
189 |> HTML.get_cached_stripped_html_for_activity(
190 activity,
191 "mastoapi:content"
192 )
193
194 summary = object.data["summary"] || ""
195
196 summary_html =
197 summary
198 |> HTML.get_cached_scrubbed_html_for_activity(
199 User.html_filter_policy(opts[:for]),
200 activity,
201 "mastoapi:summary"
202 )
203
204 summary_plaintext =
205 summary
206 |> HTML.get_cached_stripped_html_for_activity(
207 activity,
208 "mastoapi:summary"
209 )
210
211 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
212
213 url =
214 if user.local do
215 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
216 else
217 object.data["external_url"] || object.data["id"]
218 end
219
220 %{
221 id: to_string(activity.id),
222 uri: object.data["id"],
223 url: url,
224 account: AccountView.render("account.json", %{user: user}),
225 in_reply_to_id: reply_to && to_string(reply_to.id),
226 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
227 reblog: nil,
228 card: card,
229 content: content_html,
230 created_at: created_at,
231 reblogs_count: announcement_count,
232 replies_count: object.data["repliesCount"] || 0,
233 favourites_count: like_count,
234 reblogged: reblogged?(activity, opts[:for]),
235 favourited: present?(favorited),
236 bookmarked: present?(bookmarked),
237 muted: thread_muted? || User.mutes?(opts[:for], user),
238 pinned: pinned?(activity, user),
239 sensitive: sensitive,
240 spoiler_text: summary_html,
241 visibility: get_visibility(object),
242 media_attachments: attachments,
243 mentions: mentions,
244 tags: build_tags(tags),
245 application: %{
246 name: "Web",
247 website: nil
248 },
249 language: nil,
250 emojis: build_emojis(object.data["emoji"]),
251 pleroma: %{
252 local: activity.local,
253 conversation_id: get_context_id(activity),
254 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
255 content: %{"text/plain" => content_plaintext},
256 spoiler_text: %{"text/plain" => summary_plaintext}
257 }
258 }
259 end
260
261 def render("status.json", _) do
262 nil
263 end
264
265 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
266 page_url_data = URI.parse(page_url)
267
268 page_url_data =
269 if rich_media[:url] != nil do
270 URI.merge(page_url_data, URI.parse(rich_media[:url]))
271 else
272 page_url_data
273 end
274
275 page_url = page_url_data |> to_string
276
277 image_url =
278 if rich_media[:image] != nil do
279 URI.merge(page_url_data, URI.parse(rich_media[:image]))
280 |> to_string
281 else
282 nil
283 end
284
285 site_name = rich_media[:site_name] || page_url_data.host
286
287 %{
288 type: "link",
289 provider_name: site_name,
290 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
291 url: page_url,
292 image: image_url |> MediaProxy.url(),
293 title: rich_media[:title] || "",
294 description: rich_media[:description] || "",
295 pleroma: %{
296 opengraph: rich_media
297 }
298 }
299 end
300
301 def render("card.json", _) do
302 nil
303 end
304
305 def render("attachment.json", %{attachment: attachment}) do
306 [attachment_url | _] = attachment["url"]
307 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
308 href = attachment_url["href"] |> MediaProxy.url()
309
310 type =
311 cond do
312 String.contains?(media_type, "image") -> "image"
313 String.contains?(media_type, "video") -> "video"
314 String.contains?(media_type, "audio") -> "audio"
315 true -> "unknown"
316 end
317
318 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
319
320 %{
321 id: to_string(attachment["id"] || hash_id),
322 url: href,
323 remote_url: href,
324 preview_url: href,
325 text_url: href,
326 type: type,
327 description: attachment["name"],
328 pleroma: %{mime_type: media_type}
329 }
330 end
331
332 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
333 object = Object.normalize(activity)
334
335 with nil <- replied_to_activities[object.data["inReplyTo"]] do
336 # If user didn't participate in the thread
337 Activity.get_in_reply_to_activity(activity)
338 end
339 end
340
341 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
342 object = Object.normalize(activity)
343
344 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
345 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
346 else
347 nil
348 end
349 end
350
351 def render_content(%{data: %{"type" => "Video"}} = object) do
352 with name when not is_nil(name) and name != "" <- object.data["name"] do
353 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
354 else
355 _ -> object.data["content"] || ""
356 end
357 end
358
359 def render_content(%{data: %{"type" => object_type}} = object)
360 when object_type in ["Article", "Page"] do
361 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
362 url when is_bitstring(url) <- object.data["url"] do
363 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
364 else
365 _ -> object.data["content"] || ""
366 end
367 end
368
369 def render_content(object), do: object.data["content"] || ""
370
371 @doc """
372 Builds a dictionary tags.
373
374 ## Examples
375
376 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
377 [{"name": "fediverse", "url": "/tag/fediverse"},
378 {"name": "nextcloud", "url": "/tag/nextcloud"}]
379
380 """
381 @spec build_tags(list(any())) :: list(map())
382 def build_tags(object_tags) when is_list(object_tags) do
383 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
384
385 Enum.reduce(object_tags, [], fn tag, tags ->
386 tags ++ [%{name: tag, url: "/tag/#{tag}"}]
387 end)
388 end
389
390 def build_tags(_), do: []
391
392 @doc """
393 Builds list emojis.
394
395 Arguments: `nil` or list tuple of name and url.
396
397 Returns list emojis.
398
399 ## Examples
400
401 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
402 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
403
404 """
405 @spec build_emojis(nil | list(tuple())) :: list(map())
406 def build_emojis(nil), do: []
407
408 def build_emojis(emojis) do
409 emojis
410 |> Enum.map(fn {name, url} ->
411 name = HTML.strip_tags(name)
412
413 url =
414 url
415 |> HTML.strip_tags()
416 |> MediaProxy.url()
417
418 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
419 end)
420 end
421
422 defp present?(nil), do: false
423 defp present?(false), do: false
424 defp present?(_), do: true
425
426 defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
427 do: id in pinned_activities
428 end