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
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 alias Pleroma.Web.Websub
24 def is_representable?(%Activity{} = activity) do
25 object = Object.normalize(activity)
31 Visibility.is_public?(activity) && object.data["type"] == "Note" ->
39 def feed_path(user), do: "#{user.ap_id}/feed.atom"
41 def pubsub_path(user), do: "#{Web.base_url()}/push/hub/#{user.nickname}"
43 def salmon_path(user), do: "#{user.ap_id}/salmon"
45 def remote_follow_path, do: "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
47 def handle_incoming(xml_string, options \\ []) do
48 with doc when doc != :error <- parse_document(xml_string) do
49 with {:ok, actor_user} <- find_make_or_update_actor(doc),
50 do: Pleroma.Instances.set_reachable(actor_user.ap_id)
52 entries = :xmerl_xpath.string('//entry', doc)
55 Enum.map(entries, fn entry ->
56 {:xmlObj, :string, object_type} =
57 :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
59 {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
60 Logger.debug("Handling #{verb}")
64 'http://activitystrea.ms/schema/1.0/delete' ->
65 with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
67 'http://activitystrea.ms/schema/1.0/follow' ->
68 with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
70 'http://activitystrea.ms/schema/1.0/unfollow' ->
71 with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
73 'http://activitystrea.ms/schema/1.0/share' ->
74 with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
75 do: [activity, retweeted_activity]
77 'http://activitystrea.ms/schema/1.0/favorite' ->
78 with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
79 do: [activity, favorited_activity]
83 'http://activitystrea.ms/schema/1.0/note' ->
84 with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
87 'http://activitystrea.ms/schema/1.0/comment' ->
88 with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
92 Logger.error("Couldn't parse incoming document")
98 Logger.error("Error occured while handling activity")
99 Logger.error(xml_string)
100 Logger.error(inspect(e))
112 def make_share(entry, doc, retweeted_activity) do
113 with {:ok, actor} <- find_make_or_update_actor(doc),
114 %Object{} = object <- Object.normalize(retweeted_activity),
115 id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
116 {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
121 def handle_share(entry, doc) do
122 with {:ok, retweeted_activity} <- get_or_build_object(entry),
123 {:ok, activity} <- make_share(entry, doc, retweeted_activity) do
124 {:ok, activity, retweeted_activity}
130 def make_favorite(entry, doc, favorited_activity) do
131 with {:ok, actor} <- find_make_or_update_actor(doc),
132 %Object{} = object <- Object.normalize(favorited_activity),
133 id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
134 {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
139 def get_or_build_object(entry) do
140 with {:ok, activity} <- get_or_try_fetching(entry) do
144 with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
145 NoteHandler.handle_note(object, object)
150 def get_or_try_fetching(entry) do
151 Logger.debug("Trying to get entry from db")
153 with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
154 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
158 Logger.debug("Couldn't get, will try to fetch")
160 with href when not is_nil(href) <-
161 string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
162 {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
163 {:ok, favorited_activity}
165 e -> Logger.debug("Couldn't find href: #{inspect(e)}")
170 def handle_favorite(entry, doc) do
171 with {:ok, favorited_activity} <- get_or_try_fetching(entry),
172 {:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
173 {:ok, activity, favorited_activity}
179 def get_attachments(entry) do
180 :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
181 |> Enum.map(fn enclosure ->
182 with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
183 type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
185 "type" => "Attachment",
200 Gets the content from a an entry.
202 def get_content(entry) do
203 string_from_xpath("//content", entry)
207 Get the cw that mastodon uses.
210 case string_from_xpath("/*/summary", entry) do
211 cw when not is_nil(cw) -> cw
216 def get_tags(entry) do
217 :xmerl_xpath.string('//category', entry)
218 |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
220 |> Enum.map(&String.downcase/1)
223 def maybe_update(doc, user) do
224 case string_from_xpath("//author[1]/ap_enabled", doc) do
226 Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
229 maybe_update_ostatus(doc, user)
233 def maybe_update_ostatus(doc, user) do
234 old_data = Map.take(user, [:bio, :avatar, :name])
236 with false <- user.local,
237 avatar <- make_avatar_object(doc),
238 bio <- string_from_xpath("//author[1]/summary", doc),
239 name <- string_from_xpath("//author[1]/poco:displayName", doc),
241 avatar: avatar || old_data.avatar,
242 name: name || old_data.name,
243 bio: bio || old_data.bio
245 false <- new_data == old_data do
246 change = Ecto.Changeset.change(user, new_data)
247 User.update_and_set_cache(change)
254 def find_make_or_update_actor(doc) do
255 uri = string_from_xpath("//author/uri[1]", doc)
257 with {:ok, %User{} = user} <- find_or_make_user(uri),
258 {:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do
259 maybe_update(doc, user)
261 {:ap_enabled, true} ->
262 {:error, :invalid_protocol}
265 {:error, :unknown_user}
269 @spec find_or_make_user(String.t()) :: {:ok, User.t()}
270 def find_or_make_user(uri) do
271 case User.get_by_ap_id(uri) do
272 %User{} = user -> {:ok, user}
277 @spec make_user(String.t(), boolean()) :: {:ok, User.t()} | {:error, any()}
278 def make_user(uri, update \\ false) do
279 with {:ok, info} <- gather_user_info(uri) do
280 with false <- update,
281 %User{} = user <- User.get_cached_by_ap_id(info["uri"]) do
284 _e -> User.insert_or_update_user(build_user_data(info))
289 defp build_user_data(info) do
292 nickname: info["nickname"] <> "@" <> info["host"],
295 avatar: info["avatar"],
300 # TODO: Just takes the first one for now.
301 def make_avatar_object(author_doc, rel \\ "avatar") do
302 href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
303 type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
308 "url" => [%{"type" => "Link", "mediaType" => type, "href" => href}]
315 @spec gather_user_info(String.t()) :: {:ok, map()} | {:error, any()}
316 def gather_user_info(username) do
317 with {:ok, webfinger_data} <- WebFinger.finger(username),
318 {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
321 |> Map.merge(feed_data)
322 |> Map.put("fqn", username)
327 Logger.debug(fn -> "Couldn't gather info for #{username}" end)
332 # Regex-based 'parsing' so we don't have to pull in a full html parser
333 # It's a hack anyway. Maybe revisit this in the future
334 @mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
335 @gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
336 @gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
337 def get_atom_url(body) do
339 Regex.match?(@mastodon_regex, body) ->
340 [[_, match]] = Regex.scan(@mastodon_regex, body)
343 Regex.match?(@gs_regex, body) ->
344 [[_, match]] = Regex.scan(@gs_regex, body)
347 Regex.match?(@gs_classic_regex, body) ->
348 [[_, match]] = Regex.scan(@gs_classic_regex, body)
352 Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
353 {:error, "Couldn't find the Atom link"}
357 def fetch_activity_from_atom_url(url, options \\ []) do
358 with true <- String.starts_with?(url, "http"),
359 {:ok, %{body: body, status: code}} when code in 200..299 <-
360 HTTP.get(url, [{:Accept, "application/atom+xml"}]) do
361 Logger.debug("Got document from #{url}, handling...")
362 handle_incoming(body, options)
365 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
370 def fetch_activity_from_html_url(url, options \\ []) do
371 Logger.debug("Trying to fetch #{url}")
373 with true <- String.starts_with?(url, "http"),
374 {:ok, %{body: body}} <- HTTP.get(url, []),
375 {:ok, atom_url} <- get_atom_url(body) do
376 fetch_activity_from_atom_url(atom_url, options)
379 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
384 def fetch_activity_from_url(url, options \\ []) do
385 with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do
388 _e -> fetch_activity_from_html_url(url, options)
392 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
393 {:error, "Couldn't get #{url}: #{inspect(e)}"}