logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma

ostatus.ex (12337B)


      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   alias Pleroma.Web.Websub
     23 
     24   def is_representable?(%Activity{} = activity) do
     25     object = Object.normalize(activity)
     26 
     27     cond do
     28       is_nil(object) ->
     29         false
     30 
     31       Visibility.is_public?(activity) && object.data["type"] == "Note" ->
     32         true
     33 
     34       true ->
     35         false
     36     end
     37   end
     38 
     39   def feed_path(user), do: "#{user.ap_id}/feed.atom"
     40 
     41   def pubsub_path(user), do: "#{Web.base_url()}/push/hub/#{user.nickname}"
     42 
     43   def salmon_path(user), do: "#{user.ap_id}/salmon"
     44 
     45   def remote_follow_path, do: "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
     46 
     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)
     51 
     52       entries = :xmerl_xpath.string('//entry', doc)
     53 
     54       activities =
     55         Enum.map(entries, fn entry ->
     56           {:xmlObj, :string, object_type} =
     57             :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
     58 
     59           {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
     60           Logger.debug("Handling #{verb}")
     61 
     62           try do
     63             case verb do
     64               'http://activitystrea.ms/schema/1.0/delete' ->
     65                 with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
     66 
     67               'http://activitystrea.ms/schema/1.0/follow' ->
     68                 with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
     69 
     70               'http://activitystrea.ms/schema/1.0/unfollow' ->
     71                 with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
     72 
     73               'http://activitystrea.ms/schema/1.0/share' ->
     74                 with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
     75                      do: [activity, retweeted_activity]
     76 
     77               'http://activitystrea.ms/schema/1.0/favorite' ->
     78                 with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
     79                      do: [activity, favorited_activity]
     80 
     81               _ ->
     82                 case object_type do
     83                   'http://activitystrea.ms/schema/1.0/note' ->
     84                     with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
     85                          do: activity
     86 
     87                   'http://activitystrea.ms/schema/1.0/comment' ->
     88                     with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
     89                          do: activity
     90 
     91                   _ ->
     92                     Logger.error("Couldn't parse incoming document")
     93                     nil
     94                 end
     95             end
     96           rescue
     97             e ->
     98               Logger.error("Error occured while handling activity")
     99               Logger.error(xml_string)
    100               Logger.error(inspect(e))
    101               nil
    102           end
    103         end)
    104         |> Enum.filter(& &1)
    105 
    106       {:ok, activities}
    107     else
    108       _e -> {:error, []}
    109     end
    110   end
    111 
    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
    117       {:ok, activity}
    118     end
    119   end
    120 
    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}
    125     else
    126       e -> {:error, e}
    127     end
    128   end
    129 
    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
    135       {:ok, activity}
    136     end
    137   end
    138 
    139   def get_or_build_object(entry) do
    140     with {:ok, activity} <- get_or_try_fetching(entry) do
    141       {:ok, activity}
    142     else
    143       _e ->
    144         with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
    145           NoteHandler.handle_note(object, object)
    146         end
    147     end
    148   end
    149 
    150   def get_or_try_fetching(entry) do
    151     Logger.debug("Trying to get entry from db")
    152 
    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
    155       {:ok, activity}
    156     else
    157       _ ->
    158         Logger.debug("Couldn't get, will try to fetch")
    159 
    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}
    164         else
    165           e -> Logger.debug("Couldn't find href: #{inspect(e)}")
    166         end
    167     end
    168   end
    169 
    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}
    174     else
    175       e -> {:error, e}
    176     end
    177   end
    178 
    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
    184         %{
    185           "type" => "Attachment",
    186           "url" => [
    187             %{
    188               "type" => "Link",
    189               "mediaType" => type,
    190               "href" => href
    191             }
    192           ]
    193         }
    194       end
    195     end)
    196     |> Enum.filter(& &1)
    197   end
    198 
    199   @doc """
    200     Gets the content from a an entry.
    201   """
    202   def get_content(entry) do
    203     string_from_xpath("//content", entry)
    204   end
    205 
    206   @doc """
    207     Get the cw that mastodon uses.
    208   """
    209   def get_cw(entry) do
    210     case string_from_xpath("/*/summary", entry) do
    211       cw when not is_nil(cw) -> cw
    212       _ -> nil
    213     end
    214   end
    215 
    216   def get_tags(entry) do
    217     :xmerl_xpath.string('//category', entry)
    218     |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
    219     |> Enum.filter(& &1)
    220     |> Enum.map(&String.downcase/1)
    221   end
    222 
    223   def maybe_update(doc, user) do
    224     case string_from_xpath("//author[1]/ap_enabled", doc) do
    225       "true" ->
    226         Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
    227 
    228       _ ->
    229         maybe_update_ostatus(doc, user)
    230     end
    231   end
    232 
    233   def maybe_update_ostatus(doc, user) do
    234     old_data = Map.take(user, [:bio, :avatar, :name])
    235 
    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),
    240          new_data <- %{
    241            avatar: avatar || old_data.avatar,
    242            name: name || old_data.name,
    243            bio: bio || old_data.bio
    244          },
    245          false <- new_data == old_data do
    246       change = Ecto.Changeset.change(user, new_data)
    247       User.update_and_set_cache(change)
    248     else
    249       _ ->
    250         {:ok, user}
    251     end
    252   end
    253 
    254   def find_make_or_update_actor(doc) do
    255     uri = string_from_xpath("//author/uri[1]", doc)
    256 
    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)
    260     else
    261       {:ap_enabled, true} ->
    262         {:error, :invalid_protocol}
    263 
    264       _ ->
    265         {:error, :unknown_user}
    266     end
    267   end
    268 
    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}
    273       _ -> make_user(uri)
    274     end
    275   end
    276 
    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
    282         {:ok, user}
    283       else
    284         _e -> User.insert_or_update_user(build_user_data(info))
    285       end
    286     end
    287   end
    288 
    289   defp build_user_data(info) do
    290     %{
    291       name: info["name"],
    292       nickname: info["nickname"] <> "@" <> info["host"],
    293       ap_id: info["uri"],
    294       info: info,
    295       avatar: info["avatar"],
    296       bio: info["bio"]
    297     }
    298   end
    299 
    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)
    304 
    305     if href do
    306       %{
    307         "type" => "Image",
    308         "url" => [%{"type" => "Link", "mediaType" => type, "href" => href}]
    309       }
    310     else
    311       nil
    312     end
    313   end
    314 
    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
    319       data =
    320         webfinger_data
    321         |> Map.merge(feed_data)
    322         |> Map.put("fqn", username)
    323 
    324       {:ok, data}
    325     else
    326       e ->
    327         Logger.debug(fn -> "Couldn't gather info for #{username}" end)
    328         {:error, e}
    329     end
    330   end
    331 
    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
    338     cond do
    339       Regex.match?(@mastodon_regex, body) ->
    340         [[_, match]] = Regex.scan(@mastodon_regex, body)
    341         {:ok, match}
    342 
    343       Regex.match?(@gs_regex, body) ->
    344         [[_, match]] = Regex.scan(@gs_regex, body)
    345         {:ok, match}
    346 
    347       Regex.match?(@gs_classic_regex, body) ->
    348         [[_, match]] = Regex.scan(@gs_classic_regex, body)
    349         {:ok, match}
    350 
    351       true ->
    352         Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
    353         {:error, "Couldn't find the Atom link"}
    354     end
    355   end
    356 
    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)
    363     else
    364       e ->
    365         Logger.debug("Couldn't get #{url}: #{inspect(e)}")
    366         e
    367     end
    368   end
    369 
    370   def fetch_activity_from_html_url(url, options \\ []) do
    371     Logger.debug("Trying to fetch #{url}")
    372 
    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)
    377     else
    378       e ->
    379         Logger.debug("Couldn't get #{url}: #{inspect(e)}")
    380         e
    381     end
    382   end
    383 
    384   def fetch_activity_from_url(url, options \\ []) do
    385     with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do
    386       {:ok, activities}
    387     else
    388       _e -> fetch_activity_from_html_url(url, options)
    389     end
    390   rescue
    391     e ->
    392       Logger.debug("Couldn't get #{url}: #{inspect(e)}")
    393       {:error, "Couldn't get #{url}: #{inspect(e)}"}
    394   end
    395 end