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