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) 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), do: activity
96 'http://activitystrea.ms/schema/1.0/comment' ->
97 with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
100 Logger.error("Couldn't parse incoming document")
106 Logger.error("Error occured while handling activity")
107 Logger.error(xml_string)
108 Logger.error(inspect(e))
120 def make_share(entry, doc, retweeted_activity) do
121 with {:ok, actor} <- find_make_or_update_user(doc),
122 %Object{} = object <- Object.normalize(retweeted_activity),
123 id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
124 {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
129 def handle_share(entry, doc) do
130 with {:ok, retweeted_activity} <- get_or_build_object(entry),
131 {:ok, activity} <- make_share(entry, doc, retweeted_activity) do
132 {:ok, activity, retweeted_activity}
138 def make_favorite(entry, doc, favorited_activity) do
139 with {:ok, actor} <- find_make_or_update_user(doc),
140 %Object{} = object <- Object.normalize(favorited_activity),
141 id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
142 {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
147 def get_or_build_object(entry) do
148 with {:ok, activity} <- get_or_try_fetching(entry) do
152 with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
153 NoteHandler.handle_note(object, object)
158 def get_or_try_fetching(entry) do
159 Logger.debug("Trying to get entry from db")
161 with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
162 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
166 Logger.debug("Couldn't get, will try to fetch")
168 with href when not is_nil(href) <-
169 string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
170 {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
171 {:ok, favorited_activity}
173 e -> Logger.debug("Couldn't find href: #{inspect(e)}")
178 def handle_favorite(entry, doc) do
179 with {:ok, favorited_activity} <- get_or_try_fetching(entry),
180 {:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
181 {:ok, activity, favorited_activity}
187 def get_attachments(entry) do
188 :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
189 |> Enum.map(fn enclosure ->
190 with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
191 type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
193 "type" => "Attachment",
208 Gets the content from a an entry.
210 def get_content(entry) do
211 string_from_xpath("//content", entry)
215 Get the cw that mastodon uses.
218 with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
225 def get_tags(entry) do
226 :xmerl_xpath.string('//category', entry)
227 |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
229 |> Enum.map(&String.downcase/1)
232 def maybe_update(doc, user) do
233 if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
234 Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
236 maybe_update_ostatus(doc, user)
240 def maybe_update_ostatus(doc, user) do
247 with false <- user.local,
248 avatar <- make_avatar_object(doc),
249 bio <- string_from_xpath("//author[1]/summary", doc),
250 name <- string_from_xpath("//author[1]/poco:displayName", doc),
252 avatar: avatar || old_data.avatar,
253 name: name || old_data.name,
254 bio: bio || old_data.bio
256 false <- new_data == old_data do
257 change = Ecto.Changeset.change(user, new_data)
258 User.update_and_set_cache(change)
265 def find_make_or_update_user(doc) do
266 uri = string_from_xpath("//author/uri[1]", doc)
268 with {:ok, user} <- find_or_make_user(uri) do
269 maybe_update(doc, user)
273 def find_or_make_user(uri) do
274 query = from(user in User, where: user.ap_id == ^uri)
276 user = Repo.one(query)
285 def make_user(uri, update \\ false) do
286 with {:ok, info} <- gather_user_info(uri) do
289 nickname: info["nickname"] <> "@" <> info["host"],
292 avatar: info["avatar"],
296 with false <- update,
297 %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
300 _e -> User.insert_or_update_user(data)
305 # TODO: Just takes the first one for now.
306 def make_avatar_object(author_doc, rel \\ "avatar") do
307 href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
308 type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
326 def gather_user_info(username) do
327 with {:ok, webfinger_data} <- WebFinger.finger(username),
328 {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
329 {:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
332 Logger.debug(fn -> "Couldn't gather info for #{username}" end)
337 # Regex-based 'parsing' so we don't have to pull in a full html parser
338 # It's a hack anyway. Maybe revisit this in the future
339 @mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
340 @gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
341 @gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
342 def get_atom_url(body) do
344 Regex.match?(@mastodon_regex, body) ->
345 [[_, match]] = Regex.scan(@mastodon_regex, body)
348 Regex.match?(@gs_regex, body) ->
349 [[_, match]] = Regex.scan(@gs_regex, body)
352 Regex.match?(@gs_classic_regex, body) ->
353 [[_, match]] = Regex.scan(@gs_classic_regex, body)
357 Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
358 {:error, "Couldn't find the Atom link"}
362 def fetch_activity_from_atom_url(url) do
363 with true <- String.starts_with?(url, "http"),
364 {:ok, %{body: body, status: code}} when code in 200..299 <-
367 [{:Accept, "application/atom+xml"}]
369 Logger.debug("Got document from #{url}, handling...")
370 handle_incoming(body)
373 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
378 def fetch_activity_from_html_url(url) do
379 Logger.debug("Trying to fetch #{url}")
381 with true <- String.starts_with?(url, "http"),
382 {:ok, %{body: body}} <- HTTP.get(url, []),
383 {:ok, atom_url} <- get_atom_url(body) do
384 fetch_activity_from_atom_url(atom_url)
387 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
392 def fetch_activity_from_url(url) do
393 with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do
396 _e -> fetch_activity_from_html_url(url)
400 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
401 {:error, "Couldn't get #{url}: #{inspect(e)}"}