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