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
6 @httpoison Application.get_env(:pleroma, :httpoison)
12 alias Pleroma.Activity
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
27 def is_representable?(%Activity{} = activity) do
28 object = Object.normalize(activity)
34 Visibility.is_public?(activity) && object.data["type"] == "Note" ->
42 def feed_path(user) do
43 "#{user.ap_id}/feed.atom"
46 def pubsub_path(user) do
47 "#{Web.base_url()}/push/hub/#{user.nickname}"
50 def salmon_path(user) do
51 "#{user.ap_id}/salmon"
54 def remote_follow_path do
55 "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
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)
63 entries = :xmerl_xpath.string('//entry', doc)
66 Enum.map(entries, fn entry ->
67 {:xmlObj, :string, object_type} =
68 :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
70 {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
71 Logger.debug("Handling #{verb}")
75 'http://activitystrea.ms/schema/1.0/delete' ->
76 with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
78 'http://activitystrea.ms/schema/1.0/follow' ->
79 with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
81 'http://activitystrea.ms/schema/1.0/unfollow' ->
82 with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
84 'http://activitystrea.ms/schema/1.0/share' ->
85 with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
86 do: [activity, retweeted_activity]
88 'http://activitystrea.ms/schema/1.0/favorite' ->
89 with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
90 do: [activity, favorited_activity]
94 'http://activitystrea.ms/schema/1.0/note' ->
95 with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
97 'http://activitystrea.ms/schema/1.0/comment' ->
98 with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
101 Logger.error("Couldn't parse incoming document")
107 Logger.error("Error occured while handling activity")
108 Logger.error(xml_string)
109 Logger.error(inspect(e))
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
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}
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
148 def get_or_build_object(entry) do
149 with {:ok, activity} <- get_or_try_fetching(entry) do
153 with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
154 NoteHandler.handle_note(object, object)
159 def get_or_try_fetching(entry) do
160 Logger.debug("Trying to get entry from db")
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
167 Logger.debug("Couldn't get, will try to fetch")
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}
174 e -> Logger.debug("Couldn't find href: #{inspect(e)}")
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}
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
194 "type" => "Attachment",
209 Gets the content from a an entry.
211 def get_content(entry) do
212 string_from_xpath("//content", entry)
216 Get the cw that mastodon uses.
219 with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
226 def get_tags(entry) do
227 :xmerl_xpath.string('//category', entry)
228 |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
230 |> Enum.map(&String.downcase/1)
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)
237 maybe_update_ostatus(doc, user)
241 def maybe_update_ostatus(doc, user) do
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),
253 avatar: avatar || old_data.avatar,
254 name: name || old_data.name,
255 bio: bio || old_data.bio
257 false <- new_data == old_data do
258 change = Ecto.Changeset.change(user, new_data)
259 User.update_and_set_cache(change)
266 def find_make_or_update_user(doc) do
267 uri = string_from_xpath("//author/uri[1]", doc)
269 with {:ok, user} <- find_or_make_user(uri) do
270 maybe_update(doc, user)
274 def find_or_make_user(uri) do
275 query = from(user in User, where: user.ap_id == ^uri)
277 user = Repo.one(query)
286 def make_user(uri, update \\ false) do
287 with {:ok, info} <- gather_user_info(uri) do
290 nickname: info["nickname"] <> "@" <> info["host"],
293 avatar: info["avatar"],
297 with false <- update,
298 %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
301 _e -> User.insert_or_update_user(data)
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)
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)}
333 Logger.debug(fn -> "Couldn't gather info for #{username}" end)
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
345 Regex.match?(@mastodon_regex, body) ->
346 [[_, match]] = Regex.scan(@mastodon_regex, body)
349 Regex.match?(@gs_regex, body) ->
350 [[_, match]] = Regex.scan(@gs_regex, body)
353 Regex.match?(@gs_classic_regex, body) ->
354 [[_, match]] = Regex.scan(@gs_classic_regex, body)
358 Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
359 {:error, "Couldn't find the Atom link"}
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 <-
368 [{:Accept, "application/atom+xml"}]
370 Logger.debug("Got document from #{url}, handling...")
371 handle_incoming(body)
374 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
379 def fetch_activity_from_html_url(url) do
380 Logger.debug("Trying to fetch #{url}")
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)
388 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
393 def fetch_activity_from_url(url) do
394 with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do
397 _e -> fetch_activity_from_html_url(url)
401 Logger.debug("Couldn't get #{url}: #{inspect(e)}")
402 {:error, "Couldn't get #{url}: #{inspect(e)}"}