Merge branch 'spc-fix-3' 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.Web.CommonAPI.Utils
8 alias Pleroma.User
9 alias Pleroma.Web.TwitterAPI.UserView
10 alias Pleroma.Web.TwitterAPI.ActivityView
11 alias Pleroma.Web.TwitterAPI.TwitterAPI
12 alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
13 alias Pleroma.Web.MastodonAPI.StatusView
14 alias Pleroma.Activity
15 alias Pleroma.HTML
16 alias Pleroma.Object
17 alias Pleroma.User
18 alias Pleroma.Repo
19 alias Pleroma.Formatter
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 }
315 end
316
317 def render("activity.json", %{activity: unhandled_activity}) do
318 Logger.warn("#{__MODULE__} unhandled activity: #{inspect(unhandled_activity)}")
319 nil
320 end
321
322 def render_content(%{"type" => "Note"} = object) do
323 summary = object["summary"]
324
325 content =
326 if !!summary and summary != "" do
327 "<p>#{summary}</p>#{object["content"]}"
328 else
329 object["content"]
330 end
331
332 {summary, content}
333 end
334
335 def render_content(%{"type" => object_type} = object)
336 when object_type in ["Article", "Page", "Video"] do
337 summary = object["name"] || object["summary"]
338
339 content =
340 if !!summary and summary != "" and is_bitstring(object["url"]) do
341 "<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
342 else
343 object["content"]
344 end
345
346 {summary, content}
347 end
348
349 def render_content(object) do
350 summary = object["summary"] || "Unhandled activity type: #{object["type"]}"
351 content = "<p>#{summary}</p>#{object["content"]}"
352
353 {summary, content}
354 end
355 end