ostatus.ex 11.7 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

lain's avatar
lain committed
5
defmodule Pleroma.Web.OStatus do
6
7
  @httpoison Application.get_env(:pleroma, :httpoison)

8
  import Ecto.Query
lain's avatar
lain committed
9
  import Pleroma.Web.XML
10
11
  require Logger

12
13
  alias Pleroma.Activity
  alias Pleroma.Object
Haelwenn's avatar
Haelwenn committed
14
15
16
17
18
  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.Web
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Transmogrifier
19
  alias Pleroma.Web.OStatus.DeleteHandler
Haelwenn's avatar
Haelwenn committed
20
21
  alias Pleroma.Web.OStatus.FollowHandler
  alias Pleroma.Web.OStatus.NoteHandler
22
23
24
  alias Pleroma.Web.OStatus.UnfollowHandler
  alias Pleroma.Web.WebFinger
  alias Pleroma.Web.Websub
lain's avatar
lain committed
25

26
27
  def is_representable?(%Activity{} = activity) do
    object = Object.normalize(activity)
28
29
30
31
32
33
34
35
36
37
38
39
40

    cond do
      is_nil(object) ->
        false

      object.data["type"] == "Note" ->
        true

      true ->
        false
    end
  end

lain's avatar
lain committed
41
42
43
44
  def feed_path(user) do
    "#{user.ap_id}/feed.atom"
  end

lain's avatar
lain committed
45
  def pubsub_path(user) do
lain's avatar
lain committed
46
    "#{Web.base_url()}/push/hub/#{user.nickname}"
lain's avatar
lain committed
47
48
  end

49
50
51
52
  def salmon_path(user) do
    "#{user.ap_id}/salmon"
  end

53
  def remote_follow_path do
lain's avatar
lain committed
54
    "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
55
56
  end

57
  def handle_incoming(xml_string) do
lain's avatar
lain committed
58
    with doc when doc != :error <- parse_document(xml_string) do
59
60
61
      with {:ok, actor_user} <- find_make_or_update_user(doc),
           do: Pleroma.Instances.set_reachable(actor_user.ap_id)

lain's avatar
lain committed
62
63
      entries = :xmerl_xpath.string('//entry', doc)

lain's avatar
lain committed
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
      activities =
        Enum.map(entries, fn entry ->
          {:xmlObj, :string, object_type} =
            :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)

          {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
          Logger.debug("Handling #{verb}")

          try do
            case verb do
              'http://activitystrea.ms/schema/1.0/delete' ->
                with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity

              'http://activitystrea.ms/schema/1.0/follow' ->
                with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity

normandy's avatar
normandy committed
80
81
82
              'http://activitystrea.ms/schema/1.0/unfollow' ->
                with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity

lain's avatar
lain committed
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
              'http://activitystrea.ms/schema/1.0/share' ->
                with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
                     do: [activity, retweeted_activity]

              'http://activitystrea.ms/schema/1.0/favorite' ->
                with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
                     do: [activity, favorited_activity]

              _ ->
                case object_type do
                  'http://activitystrea.ms/schema/1.0/note' ->
                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity

                  'http://activitystrea.ms/schema/1.0/comment' ->
                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity

                  _ ->
                    Logger.error("Couldn't parse incoming document")
                    nil
                end
            end
          rescue
            e ->
              Logger.error("Error occured while handling activity")
              Logger.error(xml_string)
              Logger.error(inspect(e))
              nil
lain's avatar
lain committed
110
          end
lain's avatar
lain committed
111
112
        end)
        |> Enum.filter(& &1)
113

lain's avatar
lain committed
114
115
116
117
      {:ok, activities}
    else
      _e -> {:error, []}
    end
118
119
  end

lain's avatar
lain committed
120
  def make_share(entry, doc, retweeted_activity) do
lain's avatar
lain committed
121
    with {:ok, actor} <- find_make_or_update_user(doc),
122
         %Object{} = object <- Object.normalize(retweeted_activity),
lain's avatar
lain committed
123
124
         id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
         {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
lain's avatar
lain committed
125
126
127
128
129
      {:ok, activity}
    end
  end

  def handle_share(entry, doc) do
lain's avatar
lain committed
130
    with {:ok, retweeted_activity} <- get_or_build_object(entry),
lain's avatar
lain committed
131
132
133
134
135
136
137
         {:ok, activity} <- make_share(entry, doc, retweeted_activity) do
      {:ok, activity, retweeted_activity}
    else
      e -> {:error, e}
    end
  end

lain's avatar
lain committed
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),
lain's avatar
lain committed
141
142
         id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
         {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
143
144
145
146
      {:ok, activity}
    end
  end

lain's avatar
lain committed
147
148
149
150
151
152
  def get_or_build_object(entry) do
    with {:ok, activity} <- get_or_try_fetching(entry) do
      {:ok, activity}
    else
      _e ->
        with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
lain's avatar
lain committed
153
          NoteHandler.handle_note(object, object)
lain's avatar
lain committed
154
155
156
157
        end
    end
  end

158
  def get_or_try_fetching(entry) do
lain's avatar
lain committed
159
    Logger.debug("Trying to get entry from db")
lain's avatar
lain committed
160

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
163
      {:ok, activity}
lain's avatar
lain committed
164
165
    else
      _ ->
lain's avatar
lain committed
166
        Logger.debug("Couldn't get, will try to fetch")
lain's avatar
lain committed
167
168
169

        with href when not is_nil(href) <-
               string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
lain's avatar
lain committed
170
             {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
171
          {:ok, favorited_activity}
lain's avatar
lain committed
172
173
        else
          e -> Logger.debug("Couldn't find href: #{inspect(e)}")
174
175
176
177
        end
    end
  end

178
  def handle_favorite(entry, doc) do
179
    with {:ok, favorited_activity} <- get_or_try_fetching(entry),
180
181
182
183
184
185
186
         {:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
      {:ok, activity, favorited_activity}
    else
      e -> {:error, e}
    end
  end

187
188
  def get_attachments(entry) do
    :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
lain's avatar
lain committed
189
    |> Enum.map(fn enclosure ->
190
191
192
193
      with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
           type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
        %{
          "type" => "Attachment",
lain's avatar
lain committed
194
195
196
197
198
199
200
          "url" => [
            %{
              "type" => "Link",
              "mediaType" => type,
              "href" => href
            }
          ]
201
202
203
        }
      end
    end)
lain's avatar
lain committed
204
    |> Enum.filter(& &1)
205
206
  end

lain's avatar
lain committed
207
  @doc """
lain's avatar
lain committed
208
    Gets the content from a an entry.
lain's avatar
lain committed
209
  """
lain's avatar
lain committed
210
  def get_content(entry) do
lain's avatar
lain committed
211
212
    string_from_xpath("//content", entry)
  end
lain's avatar
lain committed
213

lain's avatar
lain committed
214
215
216
217
  @doc """
    Get the cw that mastodon uses.
  """
  def get_cw(entry) do
lain's avatar
lain committed
218
    with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
lain's avatar
lain committed
219
      cw
lain's avatar
lain committed
220
221
    else
      _e -> nil
lain's avatar
lain committed
222
223
224
    end
  end

lain's avatar
lain committed
225
226
  def get_tags(entry) do
    :xmerl_xpath.string('//category', entry)
lain's avatar
lain committed
227
228
    |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
    |> Enum.filter(& &1)
lain's avatar
lain committed
229
    |> Enum.map(&String.downcase/1)
lain's avatar
lain committed
230
231
  end

232
  def maybe_update(doc, user) do
lain's avatar
lain committed
233
234
235
236
237
238
    if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
      Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
    else
      maybe_update_ostatus(doc, user)
    end
  end
lain's avatar
lain committed
239

lain's avatar
lain committed
240
  def maybe_update_ostatus(doc, user) do
241
242
243
    old_data = %{
      avatar: user.avatar,
      bio: user.bio,
lain's avatar
lain committed
244
      name: user.name
245
246
247
248
    }

    with false <- user.local,
         avatar <- make_avatar_object(doc),
lain's avatar
lain committed
249
         bio <- string_from_xpath("//author[1]/summary", doc),
lain's avatar
lain committed
250
         name <- string_from_xpath("//author[1]/poco:displayName", doc),
lain's avatar
lain committed
251
252
253
         new_data <- %{
           avatar: avatar || old_data.avatar,
           name: name || old_data.name,
lain's avatar
lain committed
254
           bio: bio || old_data.bio
lain's avatar
lain committed
255
         },
256
257
         false <- new_data == old_data do
      change = Ecto.Changeset.change(user, new_data)
lain's avatar
lain committed
258
      User.update_and_set_cache(change)
lain's avatar
lain committed
259
260
261
    else
      _ ->
        {:ok, user}
262
263
264
    end
  end

265
266
  def find_make_or_update_user(doc) do
    uri = string_from_xpath("//author/uri[1]", doc)
lain's avatar
lain committed
267

268
    with {:ok, user} <- find_or_make_user(uri) do
269
      maybe_update(doc, user)
270
271
272
    end
  end

lain's avatar
lain committed
273
  def find_or_make_user(uri) do
lain's avatar
lain committed
274
    query = from(user in User, where: user.ap_id == ^uri)
275
276
277
278

    user = Repo.one(query)

    if is_nil(user) do
lain's avatar
lain committed
279
      make_user(uri)
280
281
282
283
284
    else
      {:ok, user}
    end
  end

285
  def make_user(uri, update \\ false) do
lain's avatar
lain committed
286
287
    with {:ok, info} <- gather_user_info(uri) do
      data = %{
288
289
290
        name: info["name"],
        nickname: info["nickname"] <> "@" <> info["host"],
        ap_id: info["uri"],
lain's avatar
lain committed
291
        info: info,
292
293
        avatar: info["avatar"],
        bio: info["bio"]
lain's avatar
lain committed
294
      }
lain's avatar
lain committed
295

296
      with false <- update,
minibikini's avatar
minibikini committed
297
           %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
298
        {:ok, user}
lain's avatar
lain committed
299
300
      else
        _e -> User.insert_or_update_user(data)
301
      end
lain's avatar
lain committed
302
    end
303
304
305
  end

  # TODO: Just takes the first one for now.
lain's avatar
lain committed
306
307
308
  def make_avatar_object(author_doc, rel \\ "avatar") do
    href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
    type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
309
310
311
312

    if href do
      %{
        "type" => "Image",
lain's avatar
lain committed
313
314
315
316
317
318
319
        "url" => [
          %{
            "type" => "Link",
            "mediaType" => type,
            "href" => href
          }
        ]
320
321
322
323
      }
    else
      nil
    end
lain's avatar
lain committed
324
  end
lain's avatar
lain committed
325
326
327

  def gather_user_info(username) do
    with {:ok, webfinger_data} <- WebFinger.finger(username),
328
329
         {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
      {:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
lain's avatar
lain committed
330
331
332
333
    else
      e ->
        Logger.debug(fn -> "Couldn't gather info for #{username}" end)
        {:error, e}
lain's avatar
lain committed
334
335
    end
  end
336
337
338
339
340

  # Regex-based 'parsing' so we don't have to pull in a full html parser
  # It's a hack anyway. Maybe revisit this in the future
  @mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
  @gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
lain's avatar
lain committed
341
  @gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
342
343
344
345
346
  def get_atom_url(body) do
    cond do
      Regex.match?(@mastodon_regex, body) ->
        [[_, match]] = Regex.scan(@mastodon_regex, body)
        {:ok, match}
lain's avatar
lain committed
347

348
349
350
      Regex.match?(@gs_regex, body) ->
        [[_, match]] = Regex.scan(@gs_regex, body)
        {:ok, match}
lain's avatar
lain committed
351

lain's avatar
lain committed
352
353
354
      Regex.match?(@gs_classic_regex, body) ->
        [[_, match]] = Regex.scan(@gs_classic_regex, body)
        {:ok, match}
lain's avatar
lain committed
355

356
      true ->
feld's avatar
feld committed
357
358
        Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
        {:error, "Couldn't find the Atom link"}
359
360
361
    end
  end

lain's avatar
lain committed
362
  def fetch_activity_from_atom_url(url) do
lain's avatar
lain committed
363
    with true <- String.starts_with?(url, "http"),
Maksim's avatar
Maksim committed
364
         {:ok, %{body: body, status: code}} when code in 200..299 <-
lain's avatar
lain committed
365
366
           @httpoison.get(
             url,
Hakaba Hitoyo's avatar
Hakaba Hitoyo committed
367
             [{:Accept, "application/atom+xml"}]
lain's avatar
lain committed
368
           ) do
lain's avatar
lain committed
369
370
      Logger.debug("Got document from #{url}, handling...")
      handle_incoming(body)
lain's avatar
lain committed
371
372
373
374
    else
      e ->
        Logger.debug("Couldn't get #{url}: #{inspect(e)}")
        e
lain's avatar
lain committed
375
376
377
    end
  end

378
  def fetch_activity_from_html_url(url) do
lain's avatar
lain committed
379
    Logger.debug("Trying to fetch #{url}")
lain's avatar
lain committed
380

lain's avatar
lain committed
381
    with true <- String.starts_with?(url, "http"),
Hakaba Hitoyo's avatar
Hakaba Hitoyo committed
382
         {:ok, %{body: body}} <- @httpoison.get(url, []),
lain's avatar
lain committed
383
         {:ok, atom_url} <- get_atom_url(body) do
lain's avatar
lain committed
384
      fetch_activity_from_atom_url(atom_url)
lain's avatar
lain committed
385
386
387
388
    else
      e ->
        Logger.debug("Couldn't get #{url}: #{inspect(e)}")
        e
389
    end
lain's avatar
lain committed
390
  end
lain's avatar
lain committed
391
392

  def fetch_activity_from_url(url) do
Maksim's avatar
updates    
Maksim committed
393
394
395
396
    with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do
      {:ok, activities}
    else
      _e -> fetch_activity_from_html_url(url)
lain's avatar
lain committed
397
    end
Maksim's avatar
updates    
Maksim committed
398
399
400
401
  rescue
    e ->
      Logger.debug("Couldn't get #{url}: #{inspect(e)}")
      {:error, "Couldn't get #{url}: #{inspect(e)}"}
lain's avatar
lain committed
402
  end
lain's avatar
lain committed
403
end