make bulk user creation from admin works as a transaction
[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 attachment_data = object.data["attachment"] || []
161 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
162
163 created_at = Utils.to_masto_date(object.data["published"])
164
165 reply_to = get_reply_to(activity, opts)
166
167 reply_to_user = reply_to && get_user(reply_to.data["actor"])
168
169 content =
170 object
171 |> render_content()
172
173 content_html =
174 content
175 |> HTML.get_cached_scrubbed_html_for_activity(
176 User.html_filter_policy(opts[:for]),
177 activity,
178 "mastoapi:content"
179 )
180
181 content_plaintext =
182 content
183 |> HTML.get_cached_stripped_html_for_activity(
184 activity,
185 "mastoapi:content"
186 )
187
188 summary = object.data["summary"] || ""
189
190 summary_html =
191 summary
192 |> HTML.get_cached_scrubbed_html_for_activity(
193 User.html_filter_policy(opts[:for]),
194 activity,
195 "mastoapi:summary"
196 )
197
198 summary_plaintext =
199 summary
200 |> HTML.get_cached_stripped_html_for_activity(
201 activity,
202 "mastoapi:summary"
203 )
204
205 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
206
207 url =
208 if user.local do
209 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
210 else
211 object.data["external_url"] || object.data["id"]
212 end
213
214 %{
215 id: to_string(activity.id),
216 uri: object.data["id"],
217 url: url,
218 account: AccountView.render("account.json", %{user: user}),
219 in_reply_to_id: reply_to && to_string(reply_to.id),
220 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
221 reblog: nil,
222 card: card,
223 content: content_html,
224 created_at: created_at,
225 reblogs_count: announcement_count,
226 replies_count: object.data["repliesCount"] || 0,
227 favourites_count: like_count,
228 reblogged: reblogged?(activity, opts[:for]),
229 favourited: present?(favorited),
230 bookmarked: present?(bookmarked),
231 muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),
232 pinned: pinned?(activity, user),
233 sensitive: sensitive,
234 spoiler_text: summary_html,
235 visibility: get_visibility(object),
236 media_attachments: attachments,
237 mentions: mentions,
238 tags: build_tags(tags),
239 application: %{
240 name: "Web",
241 website: nil
242 },
243 language: nil,
244 emojis: build_emojis(object.data["emoji"]),
245 pleroma: %{
246 local: activity.local,
247 conversation_id: get_context_id(activity),
248 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
249 content: %{"text/plain" => content_plaintext},
250 spoiler_text: %{"text/plain" => summary_plaintext}
251 }
252 }
253 end
254
255 def render("status.json", _) do
256 nil
257 end
258
259 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
260 page_url_data = URI.parse(page_url)
261
262 page_url_data =
263 if rich_media[:url] != nil do
264 URI.merge(page_url_data, URI.parse(rich_media[:url]))
265 else
266 page_url_data
267 end
268
269 page_url = page_url_data |> to_string
270
271 image_url =
272 if rich_media[:image] != nil do
273 URI.merge(page_url_data, URI.parse(rich_media[:image]))
274 |> to_string
275 else
276 nil
277 end
278
279 site_name = rich_media[:site_name] || page_url_data.host
280
281 %{
282 type: "link",
283 provider_name: site_name,
284 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
285 url: page_url,
286 image: image_url |> MediaProxy.url(),
287 title: rich_media[:title],
288 description: rich_media[:description],
289 pleroma: %{
290 opengraph: rich_media
291 }
292 }
293 end
294
295 def render("card.json", _) do
296 nil
297 end
298
299 def render("attachment.json", %{attachment: attachment}) do
300 [attachment_url | _] = attachment["url"]
301 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
302 href = attachment_url["href"] |> MediaProxy.url()
303
304 type =
305 cond do
306 String.contains?(media_type, "image") -> "image"
307 String.contains?(media_type, "video") -> "video"
308 String.contains?(media_type, "audio") -> "audio"
309 true -> "unknown"
310 end
311
312 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
313
314 %{
315 id: to_string(attachment["id"] || hash_id),
316 url: href,
317 remote_url: href,
318 preview_url: href,
319 text_url: href,
320 type: type,
321 description: attachment["name"],
322 pleroma: %{mime_type: media_type}
323 }
324 end
325
326 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
327 object = Object.normalize(activity)
328
329 with nil <- replied_to_activities[object.data["inReplyTo"]] do
330 # If user didn't participate in the thread
331 Activity.get_in_reply_to_activity(activity)
332 end
333 end
334
335 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
336 object = Object.normalize(activity)
337
338 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
339 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
340 else
341 nil
342 end
343 end
344
345 def render_content(%{data: %{"type" => "Video"}} = object) do
346 with name when not is_nil(name) and name != "" <- object.data["name"] do
347 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
348 else
349 _ -> object.data["content"] || ""
350 end
351 end
352
353 def render_content(%{data: %{"type" => object_type}} = object)
354 when object_type in ["Article", "Page"] do
355 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
356 url when is_bitstring(url) <- object.data["url"] do
357 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
358 else
359 _ -> object.data["content"] || ""
360 end
361 end
362
363 def render_content(object), do: object.data["content"] || ""
364
365 @doc """
366 Builds a dictionary tags.
367
368 ## Examples
369
370 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
371 [{"name": "fediverse", "url": "/tag/fediverse"},
372 {"name": "nextcloud", "url": "/tag/nextcloud"}]
373
374 """
375 @spec build_tags(list(any())) :: list(map())
376 def build_tags(object_tags) when is_list(object_tags) do
377 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
378
379 Enum.reduce(object_tags, [], fn tag, tags ->
380 tags ++ [%{name: tag, url: "/tag/#{tag}"}]
381 end)
382 end
383
384 def build_tags(_), do: []
385
386 @doc """
387 Builds list emojis.
388
389 Arguments: `nil` or list tuple of name and url.
390
391 Returns list emojis.
392
393 ## Examples
394
395 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
396 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
397
398 """
399 @spec build_emojis(nil | list(tuple())) :: list(map())
400 def build_emojis(nil), do: []
401
402 def build_emojis(emojis) do
403 emojis
404 |> Enum.map(fn {name, url} ->
405 name = HTML.strip_tags(name)
406
407 url =
408 url
409 |> HTML.strip_tags()
410 |> MediaProxy.url()
411
412 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
413 end)
414 end
415
416 defp present?(nil), do: false
417 defp present?(false), do: false
418 defp present?(_), do: true
419
420 defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
421 do: id in pinned_activities
422 end