1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.OStatus do
10 alias Pleroma.Activity
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Transmogrifier
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.OStatus.DeleteHandler
20 alias Pleroma.Web.OStatus.FollowHandler
21 alias Pleroma.Web.OStatus.NoteHandler
22 alias Pleroma.Web.OStatus.UnfollowHandler
23 alias Pleroma.Web.WebFinger
24 alias Pleroma.Web.Websub
26 def is_representable?(%Activity{} = activity) do
27 object = Object.normalize(activity)
33 Visibility.is_public?(activity) && object.data["type"] == "Note" ->
41 def feed_path(user) do
42 "#{user.ap_id}/feed.atom"
45 def pubsub_path(user) do
46 "#{Web.base_url()}/push/hub/#{user.nickname}"
49 def salmon_path(user) do
50 "#{user.ap_id}/salmon"
53 def remote_follow_path do
54 "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
57 def handle_incoming(xml_string, options \\ []) do
58 with doc when doc != :error <- parse_document(xml_string) do
59 with {:ok, actor_user} <- find_make_or_update_user(doc),
60 do: Pleroma.Instances.set_reachable(actor_user.ap_id)
62 entries = :xmerl_xpath.string('//entry', doc)
65 Enum.map(entries, fn entry ->
66 {:xmlObj, :string, object_type} =
67 :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
69 {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
70 Logger.debug("Handling #{verb}")
74 'http://activitystrea.ms/schema/1.0/delete' ->
75 with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
77 'http://activitystrea.ms/schema/1.0/follow' ->
78 with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
80 'http://activitystrea.ms/schema/1.0/unfollow' ->
81 with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
83 'http://activitystrea.ms/schema/1.0/share' ->
84 with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
85 do: [activity, retweeted_activity]
87 'http://activitystrea.ms/schema/1.0/favorite' ->
88 with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
89 do: [activity, favorited_activity]
93 'http://activitystrea.ms/schema/1.0/note' ->
94 with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
97 'http://activitystrea.ms/schema/1.0/comment' ->
98 with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
102 Logger.error("Couldn't parse incoming document")
108 Logger.error("Error occured while handling activity")
109 Logger.error(xml_string)
110 Logger.error(inspect(e))
122 def make_share(entry, doc, retweeted_activity) do
123 with {:ok, actor} <- find_make_or_update_user(doc),
124 %Object{} = object <- Object.normalize(retweeted_activity),
125 id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
126 {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
131 def handle_share(entry, doc) do
132 with {:ok, retweeted_activity} <- get_or_build_object(entry),
133 {:ok, activity} <- make_share(entry, doc, retweeted_activity) do
134 {:ok, activity, retweeted_activity}
140 def make_favorite(entry, doc, favorited_activity) do
141 with {:ok, actor} <- find_make_or_update_user(doc),
142 %Object{} = object <- Object.normalize(favorited_activity),
143 id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
144 {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
149 def get_or_build_object(entry) do
150 with {:ok, activity} <- get_or_try_fetching(entry) do
154 with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
155 NoteHandler.handle_note(object, object)
160 def get_or_try_fetching(entry) do
161 Logger.debug("Trying to get entry from db")
163 with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
164 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
168 Logger.debug("Couldn't get, will try to fetch")
170 with href when not is_nil(href) <-
171 string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
172 {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
173 {:ok, favorited_activity}
175 e -> Logger.debug("Couldn't find href: #{inspect(e)}")
180 def handle_favorite(entry, doc) do
181 with {:ok, favorited_activity} <- get_or_try_fetching(entry),
182 {:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
183 {:ok, activity, favorited_activity}
189 def get_attachments(entry) do
190 :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
191 |> Enum.map(fn enclosure ->
192 with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
193 type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
195 "type" => "Attachment",
210 Gets the content from a an entry.
212 def get_content(entry) do
213 string_from_xpath("//content", entry)
217 Get the cw that mastodon uses.
220 with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
227 def get_tags(entry) do
228 :xmerl_xpath.string('//category', entry)
229 |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
231 |> Enum.map(&String.downcase/1)
234 def maybe_update(doc, user) do
235 if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
236 Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
238 maybe_update_ostatus(doc, user)
242 def maybe_update_ostatus(doc, user) do
249 with false <- user.local,
250 avatar <- make_avatar_object(doc),
251 bio <- string_from_xpath("//author[1]/summary", doc),
252 name <- string_from_xpath("//author[1]/poco:displayName", doc),
254 avatar: avatar || old_data.avatar,
255 name: name || old_data.name,
256 bio: bio || old_data.bio
258 false <- new_data == old_data do
259 change = Ecto.Changeset.change(user, new_data)
260 User.update_and_set_cache(change)
267 def find_make_or_update_user(doc) do
268 uri = string_from_xpath("//author/uri[1]", doc)
270 with {:ok, user} <- find_or_make_user(uri) do
271 maybe_update(doc, user)
275 def find_or_make_user(uri) do
276 query = from(user in User, where: user.ap_id == ^uri)
278 user = Repo.one(query)
287 def make_user(uri, update \\ false) do
288 with {:ok, info} <- gather_user_info(uri) do
291 nickname: info["nickname"] <> "@" <> info["host"],
294 avatar: info["avatar"],
298 with false <- update,
299 %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
302 _e -> User.insert_or_update_user(data)
307 # TODO: Just takes the first one for now.
308 def make_avatar_object(author_doc, rel \\ "avatar") do
309 href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
310 type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
328 def gather_user_info(username) do
329 with {:ok, webfinger_data} <- WebFinger.finger(username),
330 {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
331 {:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
334 Logger.debug(fn -> "Couldn't gather info for #{username}" end)
339 # Regex-based 'parsing' so we don't have to pull in a full html parser
340 # It's a hack anyway. Maybe revisit this in the future
341 @mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
342 @gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
343 @gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
344 def get_atom_url(body) do
346 Regex.match?(@mastodon_regex, body) ->
347 [[_, match]] = Regex.scan(@mastodon_regex, body)
350 Regex.match?(@gs_regex, body) ->
351 [[_, match]] = Regex.scan(@gs_regex, body)
354 Regex.match?(@gs_classic_regex, body) ->
355 [[_, match]] = Regex.scan(@gs_classic_regex, body)
359 Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
360 {:error, "Couldn't find the Atom link"}
364 def fetch_activity_from_atom_url(url, options \\ []) do
365 with true <- String.starts_with?(url, "http"),
366 {:ok, %{body: body, status: code}} when code in 200..299 <-
369 [{:Accept, "application/atom+xml"}]
371 Logger.debug("Got document from #{url}, handling...")
372 handle_incoming(body, options)
375 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
380 def fetch_activity_from_html_url(url, options \\ []) do
381 Logger.debug("Trying to fetch #{url}")
383 with true <- String.starts_with?(url, "http"),
384 {:ok, %{body: body}} <- HTTP.get(url, []),
385 {:ok, atom_url} <- get_atom_url(body) do
386 fetch_activity_from_atom_url(atom_url, options)
389 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
394 def fetch_activity_from_url(url, options \\ []) do
395 with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do
398 _e -> fetch_activity_from_html_url(url, options)
402 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
403 {:error, "Couldn't get #{url}: #{inspect(e)}"}