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