CI: Add auto-deployment via dokku.
[akkoma] / lib / pleroma / web / ostatus / ostatus.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.OStatus do
6 @httpoison Application.get_env(:pleroma, :httpoison)
7
8 import Ecto.Query
9 import Pleroma.Web.XML
10 require Logger
11
12 alias Pleroma.Activity
13 alias Pleroma.Object
14 alias Pleroma.Repo
15 alias Pleroma.User
16 alias Pleroma.Web
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Transmogrifier
19 alias Pleroma.Web.ActivityPub.Visibility
20 alias Pleroma.Web.OStatus.DeleteHandler
21 alias Pleroma.Web.OStatus.FollowHandler
22 alias Pleroma.Web.OStatus.NoteHandler
23 alias Pleroma.Web.OStatus.UnfollowHandler
24 alias Pleroma.Web.WebFinger
25 alias Pleroma.Web.Websub
26
27 def is_representable?(%Activity{} = activity) do
28 object = Object.normalize(activity)
29
30 cond do
31 is_nil(object) ->
32 false
33
34 Visibility.is_public?(activity) && object.data["type"] == "Note" ->
35 true
36
37 true ->
38 false
39 end
40 end
41
42 def feed_path(user) do
43 "#{user.ap_id}/feed.atom"
44 end
45
46 def pubsub_path(user) do
47 "#{Web.base_url()}/push/hub/#{user.nickname}"
48 end
49
50 def salmon_path(user) do
51 "#{user.ap_id}/salmon"
52 end
53
54 def remote_follow_path do
55 "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
56 end
57
58 def handle_incoming(xml_string) do
59 with doc when doc != :error <- parse_document(xml_string) do
60 with {:ok, actor_user} <- find_make_or_update_user(doc),
61 do: Pleroma.Instances.set_reachable(actor_user.ap_id)
62
63 entries = :xmerl_xpath.string('//entry', doc)
64
65 activities =
66 Enum.map(entries, fn entry ->
67 {:xmlObj, :string, object_type} =
68 :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
69
70 {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
71 Logger.debug("Handling #{verb}")
72
73 try do
74 case verb do
75 'http://activitystrea.ms/schema/1.0/delete' ->
76 with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
77
78 'http://activitystrea.ms/schema/1.0/follow' ->
79 with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
80
81 'http://activitystrea.ms/schema/1.0/unfollow' ->
82 with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
83
84 'http://activitystrea.ms/schema/1.0/share' ->
85 with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
86 do: [activity, retweeted_activity]
87
88 'http://activitystrea.ms/schema/1.0/favorite' ->
89 with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
90 do: [activity, favorited_activity]
91
92 _ ->
93 case object_type do
94 'http://activitystrea.ms/schema/1.0/note' ->
95 with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
96
97 'http://activitystrea.ms/schema/1.0/comment' ->
98 with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
99
100 _ ->
101 Logger.error("Couldn't parse incoming document")
102 nil
103 end
104 end
105 rescue
106 e ->
107 Logger.error("Error occured while handling activity")
108 Logger.error(xml_string)
109 Logger.error(inspect(e))
110 nil
111 end
112 end)
113 |> Enum.filter(& &1)
114
115 {:ok, activities}
116 else
117 _e -> {:error, []}
118 end
119 end
120
121 def make_share(entry, doc, retweeted_activity) do
122 with {:ok, actor} <- find_make_or_update_user(doc),
123 %Object{} = object <- Object.normalize(retweeted_activity),
124 id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
125 {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
126 {:ok, activity}
127 end
128 end
129
130 def handle_share(entry, doc) do
131 with {:ok, retweeted_activity} <- get_or_build_object(entry),
132 {:ok, activity} <- make_share(entry, doc, retweeted_activity) do
133 {:ok, activity, retweeted_activity}
134 else
135 e -> {:error, e}
136 end
137 end
138
139 def make_favorite(entry, doc, favorited_activity) do
140 with {:ok, actor} <- find_make_or_update_user(doc),
141 %Object{} = object <- Object.normalize(favorited_activity),
142 id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
143 {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
144 {:ok, activity}
145 end
146 end
147
148 def get_or_build_object(entry) do
149 with {:ok, activity} <- get_or_try_fetching(entry) do
150 {:ok, activity}
151 else
152 _e ->
153 with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
154 NoteHandler.handle_note(object, object)
155 end
156 end
157 end
158
159 def get_or_try_fetching(entry) do
160 Logger.debug("Trying to get entry from db")
161
162 with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
163 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
164 {:ok, activity}
165 else
166 _ ->
167 Logger.debug("Couldn't get, will try to fetch")
168
169 with href when not is_nil(href) <-
170 string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
171 {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
172 {:ok, favorited_activity}
173 else
174 e -> Logger.debug("Couldn't find href: #{inspect(e)}")
175 end
176 end
177 end
178
179 def handle_favorite(entry, doc) do
180 with {:ok, favorited_activity} <- get_or_try_fetching(entry),
181 {:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
182 {:ok, activity, favorited_activity}
183 else
184 e -> {:error, e}
185 end
186 end
187
188 def get_attachments(entry) do
189 :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
190 |> Enum.map(fn enclosure ->
191 with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
192 type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
193 %{
194 "type" => "Attachment",
195 "url" => [
196 %{
197 "type" => "Link",
198 "mediaType" => type,
199 "href" => href
200 }
201 ]
202 }
203 end
204 end)
205 |> Enum.filter(& &1)
206 end
207
208 @doc """
209 Gets the content from a an entry.
210 """
211 def get_content(entry) do
212 string_from_xpath("//content", entry)
213 end
214
215 @doc """
216 Get the cw that mastodon uses.
217 """
218 def get_cw(entry) do
219 with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
220 cw
221 else
222 _e -> nil
223 end
224 end
225
226 def get_tags(entry) do
227 :xmerl_xpath.string('//category', entry)
228 |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
229 |> Enum.filter(& &1)
230 |> Enum.map(&String.downcase/1)
231 end
232
233 def maybe_update(doc, user) do
234 if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
235 Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
236 else
237 maybe_update_ostatus(doc, user)
238 end
239 end
240
241 def maybe_update_ostatus(doc, user) do
242 old_data = %{
243 avatar: user.avatar,
244 bio: user.bio,
245 name: user.name
246 }
247
248 with false <- user.local,
249 avatar <- make_avatar_object(doc),
250 bio <- string_from_xpath("//author[1]/summary", doc),
251 name <- string_from_xpath("//author[1]/poco:displayName", doc),
252 new_data <- %{
253 avatar: avatar || old_data.avatar,
254 name: name || old_data.name,
255 bio: bio || old_data.bio
256 },
257 false <- new_data == old_data do
258 change = Ecto.Changeset.change(user, new_data)
259 User.update_and_set_cache(change)
260 else
261 _ ->
262 {:ok, user}
263 end
264 end
265
266 def find_make_or_update_user(doc) do
267 uri = string_from_xpath("//author/uri[1]", doc)
268
269 with {:ok, user} <- find_or_make_user(uri) do
270 maybe_update(doc, user)
271 end
272 end
273
274 def find_or_make_user(uri) do
275 query = from(user in User, where: user.ap_id == ^uri)
276
277 user = Repo.one(query)
278
279 if is_nil(user) do
280 make_user(uri)
281 else
282 {:ok, user}
283 end
284 end
285
286 def make_user(uri, update \\ false) do
287 with {:ok, info} <- gather_user_info(uri) do
288 data = %{
289 name: info["name"],
290 nickname: info["nickname"] <> "@" <> info["host"],
291 ap_id: info["uri"],
292 info: info,
293 avatar: info["avatar"],
294 bio: info["bio"]
295 }
296
297 with false <- update,
298 %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
299 {:ok, user}
300 else
301 _e -> User.insert_or_update_user(data)
302 end
303 end
304 end
305
306 # TODO: Just takes the first one for now.
307 def make_avatar_object(author_doc, rel \\ "avatar") do
308 href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
309 type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
310
311 if href do
312 %{
313 "type" => "Image",
314 "url" => [
315 %{
316 "type" => "Link",
317 "mediaType" => type,
318 "href" => href
319 }
320 ]
321 }
322 else
323 nil
324 end
325 end
326
327 def gather_user_info(username) do
328 with {:ok, webfinger_data} <- WebFinger.finger(username),
329 {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
330 {:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
331 else
332 e ->
333 Logger.debug(fn -> "Couldn't gather info for #{username}" end)
334 {:error, e}
335 end
336 end
337
338 # Regex-based 'parsing' so we don't have to pull in a full html parser
339 # It's a hack anyway. Maybe revisit this in the future
340 @mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
341 @gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
342 @gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
343 def get_atom_url(body) do
344 cond do
345 Regex.match?(@mastodon_regex, body) ->
346 [[_, match]] = Regex.scan(@mastodon_regex, body)
347 {:ok, match}
348
349 Regex.match?(@gs_regex, body) ->
350 [[_, match]] = Regex.scan(@gs_regex, body)
351 {:ok, match}
352
353 Regex.match?(@gs_classic_regex, body) ->
354 [[_, match]] = Regex.scan(@gs_classic_regex, body)
355 {:ok, match}
356
357 true ->
358 Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
359 {:error, "Couldn't find the Atom link"}
360 end
361 end
362
363 def fetch_activity_from_atom_url(url) do
364 with true <- String.starts_with?(url, "http"),
365 {:ok, %{body: body, status: code}} when code in 200..299 <-
366 @httpoison.get(
367 url,
368 [{:Accept, "application/atom+xml"}]
369 ) do
370 Logger.debug("Got document from #{url}, handling...")
371 handle_incoming(body)
372 else
373 e ->
374 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
375 e
376 end
377 end
378
379 def fetch_activity_from_html_url(url) do
380 Logger.debug("Trying to fetch #{url}")
381
382 with true <- String.starts_with?(url, "http"),
383 {:ok, %{body: body}} <- @httpoison.get(url, []),
384 {:ok, atom_url} <- get_atom_url(body) do
385 fetch_activity_from_atom_url(atom_url)
386 else
387 e ->
388 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
389 e
390 end
391 end
392
393 def fetch_activity_from_url(url) do
394 with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do
395 {:ok, activities}
396 else
397 _e -> fetch_activity_from_html_url(url)
398 end
399 rescue
400 e ->
401 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
402 {:error, "Couldn't get #{url}: #{inspect(e)}"}
403 end
404 end