Merge branch 'feature/add-pagination-to-users-admin-api' into 'develop'
[akkoma] / lib / pleroma / web / twitter_api / views / activity_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.TwitterAPI.ActivityView do
6 use Pleroma.Web, :view
7 alias Pleroma.Activity
8 alias Pleroma.Formatter
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.StatusView
16 alias Pleroma.Web.TwitterAPI.ActivityView
17 alias Pleroma.Web.TwitterAPI.TwitterAPI
18 alias Pleroma.Web.TwitterAPI.UserView
19 alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
20
21 import Ecto.Query
22 require Logger
23
24 defp query_context_ids([]), do: []
25
26 defp query_context_ids(contexts) do
27 query = from(o in Object, where: fragment("(?)->>'id' = ANY(?)", o.data, ^contexts))
28
29 Repo.all(query)
30 end
31
32 defp query_users([]), do: []
33
34 defp query_users(user_ids) do
35 query = from(user in User, where: user.ap_id in ^user_ids)
36
37 Repo.all(query)
38 end
39
40 defp collect_context_ids(activities) do
41 _contexts =
42 activities
43 |> Enum.reject(& &1.data["context_id"])
44 |> Enum.map(fn %{data: data} ->
45 data["context"]
46 end)
47 |> Enum.filter(& &1)
48 |> query_context_ids()
49 |> Enum.reduce(%{}, fn %{data: %{"id" => ap_id}, id: id}, acc ->
50 Map.put(acc, ap_id, id)
51 end)
52 end
53
54 defp collect_users(activities) do
55 activities
56 |> Enum.map(fn activity ->
57 case activity.data do
58 data = %{"type" => "Follow"} ->
59 [data["actor"], data["object"]]
60
61 data ->
62 [data["actor"]]
63 end ++ activity.recipients
64 end)
65 |> List.flatten()
66 |> Enum.uniq()
67 |> query_users()
68 |> Enum.reduce(%{}, fn user, acc ->
69 Map.put(acc, user.ap_id, user)
70 end)
71 end
72
73 defp get_context_id(%{data: %{"context_id" => context_id}}, _) when not is_nil(context_id),
74 do: context_id
75
76 defp get_context_id(%{data: %{"context" => nil}}, _), do: nil
77
78 defp get_context_id(%{data: %{"context" => context}}, options) do
79 cond do
80 id = options[:context_ids][context] -> id
81 true -> TwitterAPI.context_to_conversation_id(context)
82 end
83 end
84
85 defp get_context_id(_, _), do: nil
86
87 defp get_user(ap_id, opts) do
88 cond do
89 user = opts[:users][ap_id] ->
90 user
91
92 String.ends_with?(ap_id, "/followers") ->
93 nil
94
95 ap_id == "https://www.w3.org/ns/activitystreams#Public" ->
96 nil
97
98 user = User.get_cached_by_ap_id(ap_id) ->
99 user
100
101 user = User.get_by_guessed_nickname(ap_id) ->
102 user
103
104 true ->
105 User.error_user(ap_id)
106 end
107 end
108
109 def render("index.json", opts) do
110 context_ids = collect_context_ids(opts.activities)
111 users = collect_users(opts.activities)
112
113 opts =
114 opts
115 |> Map.put(:context_ids, context_ids)
116 |> Map.put(:users, users)
117
118 safe_render_many(
119 opts.activities,
120 ActivityView,
121 "activity.json",
122 opts
123 )
124 end
125
126 def render("activity.json", %{activity: %{data: %{"type" => "Delete"}} = activity} = opts) do
127 user = get_user(activity.data["actor"], opts)
128 created_at = activity.data["published"] |> Utils.date_to_asctime()
129
130 %{
131 "id" => activity.id,
132 "uri" => activity.data["object"],
133 "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
134 "attentions" => [],
135 "statusnet_html" => "deleted notice {{tag",
136 "text" => "deleted notice {{tag",
137 "is_local" => activity.local,
138 "is_post_verb" => false,
139 "created_at" => created_at,
140 "in_reply_to_status_id" => nil,
141 "external_url" => activity.data["id"],
142 "activity_type" => "delete"
143 }
144 end
145
146 def render("activity.json", %{activity: %{data: %{"type" => "Follow"}} = activity} = opts) do
147 user = get_user(activity.data["actor"], opts)
148 created_at = activity.data["published"] || DateTime.to_iso8601(activity.inserted_at)
149 created_at = created_at |> Utils.date_to_asctime()
150
151 followed = get_user(activity.data["object"], opts)
152 text = "#{user.nickname} started following #{followed.nickname}"
153
154 %{
155 "id" => activity.id,
156 "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
157 "attentions" => [],
158 "statusnet_html" => text,
159 "text" => text,
160 "is_local" => activity.local,
161 "is_post_verb" => false,
162 "created_at" => created_at,
163 "in_reply_to_status_id" => nil,
164 "external_url" => activity.data["id"],
165 "activity_type" => "follow"
166 }
167 end
168
169 def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do
170 user = get_user(activity.data["actor"], opts)
171 created_at = activity.data["published"] |> Utils.date_to_asctime()
172 announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
173
174 text = "#{user.nickname} retweeted a status."
175
176 retweeted_status = render("activity.json", Map.merge(opts, %{activity: announced_activity}))
177
178 %{
179 "id" => activity.id,
180 "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
181 "statusnet_html" => text,
182 "text" => text,
183 "is_local" => activity.local,
184 "is_post_verb" => false,
185 "uri" => "tag:#{activity.data["id"]}:objectType=note",
186 "created_at" => created_at,
187 "retweeted_status" => retweeted_status,
188 "statusnet_conversation_id" => get_context_id(announced_activity, opts),
189 "external_url" => activity.data["id"],
190 "activity_type" => "repeat"
191 }
192 end
193
194 def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do
195 user = get_user(activity.data["actor"], opts)
196 liked_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
197 liked_activity_id = if liked_activity, do: liked_activity.id, else: nil
198
199 created_at =
200 activity.data["published"]
201 |> Utils.date_to_asctime()
202
203 text = "#{user.nickname} favorited a status."
204
205 favorited_status =
206 if liked_activity,
207 do: render("activity.json", Map.merge(opts, %{activity: liked_activity})),
208 else: nil
209
210 %{
211 "id" => activity.id,
212 "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
213 "statusnet_html" => text,
214 "text" => text,
215 "is_local" => activity.local,
216 "is_post_verb" => false,
217 "uri" => "tag:#{activity.data["id"]}:objectType=Favourite",
218 "created_at" => created_at,
219 "favorited_status" => favorited_status,
220 "in_reply_to_status_id" => liked_activity_id,
221 "external_url" => activity.data["id"],
222 "activity_type" => "like"
223 }
224 end
225
226 def render(
227 "activity.json",
228 %{activity: %{data: %{"type" => "Create", "object" => object}} = activity} = opts
229 ) do
230 user = get_user(activity.data["actor"], opts)
231
232 created_at = object["published"] |> Utils.date_to_asctime()
233 like_count = object["like_count"] || 0
234 announcement_count = object["announcement_count"] || 0
235 favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
236 repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
237 pinned = activity.id in user.info.pinned_activities
238
239 attentions =
240 []
241 |> Utils.maybe_notify_to_recipients(activity)
242 |> Utils.maybe_notify_mentioned_recipients(activity)
243 |> Enum.map(fn ap_id -> get_user(ap_id, opts) end)
244 |> Enum.filter(& &1)
245 |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
246
247 conversation_id = get_context_id(activity, opts)
248
249 tags = activity.data["object"]["tag"] || []
250 possibly_sensitive = activity.data["object"]["sensitive"] || Enum.member?(tags, "nsfw")
251
252 tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
253
254 {summary, content} = render_content(object)
255
256 html =
257 content
258 |> HTML.get_cached_scrubbed_html_for_object(
259 User.html_filter_policy(opts[:for]),
260 activity,
261 __MODULE__
262 )
263 |> Formatter.emojify(object["emoji"])
264
265 text =
266 if content do
267 content
268 |> String.replace(~r/<br\s?\/?>/, "\n")
269 |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__)
270 end
271
272 reply_parent = Activity.get_in_reply_to_activity(activity)
273
274 reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
275
276 summary = HTML.strip_tags(summary)
277
278 card =
279 StatusView.render(
280 "card.json",
281 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
282 )
283
284 %{
285 "id" => activity.id,
286 "uri" => activity.data["object"]["id"],
287 "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
288 "statusnet_html" => html,
289 "text" => text,
290 "is_local" => activity.local,
291 "is_post_verb" => true,
292 "created_at" => created_at,
293 "in_reply_to_status_id" => object["inReplyToStatusId"],
294 "in_reply_to_screen_name" => reply_user && reply_user.nickname,
295 "in_reply_to_profileurl" => User.profile_url(reply_user),
296 "in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id,
297 "in_reply_to_user_id" => reply_user && reply_user.id,
298 "statusnet_conversation_id" => conversation_id,
299 "attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
300 "attentions" => attentions,
301 "fave_num" => like_count,
302 "repeat_num" => announcement_count,
303 "favorited" => !!favorited,
304 "repeated" => !!repeated,
305 "pinned" => pinned,
306 "external_url" => object["external_url"] || object["id"],
307 "tags" => tags,
308 "activity_type" => "post",
309 "possibly_sensitive" => possibly_sensitive,
310 "visibility" => StatusView.get_visibility(object),
311 "summary" => summary,
312 "summary_html" => summary |> Formatter.emojify(object["emoji"]),
313 "card" => card,
314 "muted" => CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user)
315 }
316 end
317
318 def render("activity.json", %{activity: unhandled_activity}) do
319 Logger.warn("#{__MODULE__} unhandled activity: #{inspect(unhandled_activity)}")
320 nil
321 end
322
323 def render_content(%{"type" => "Note"} = object) do
324 summary = object["summary"]
325
326 content =
327 if !!summary and summary != "" do
328 "<p>#{summary}</p>#{object["content"]}"
329 else
330 object["content"]
331 end
332
333 {summary, content}
334 end
335
336 def render_content(%{"type" => object_type} = object)
337 when object_type in ["Article", "Page", "Video"] do
338 summary = object["name"] || object["summary"]
339
340 content =
341 if !!summary and summary != "" and is_bitstring(object["url"]) do
342 "<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
343 else
344 object["content"]
345 end
346
347 {summary, content}
348 end
349
350 def render_content(object) do
351 summary = object["summary"] || "Unhandled activity type: #{object["type"]}"
352 content = "<p>#{summary}</p>#{object["content"]}"
353
354 {summary, content}
355 end
356 end