ostatus.ex 11.4 KB
Newer Older
lain's avatar
lain committed
1
defmodule Pleroma.Web.OStatus do
2
3
  @httpoison Application.get_env(:pleroma, :httpoison)

4
  import Ecto.Query
lain's avatar
lain committed
5
  import Pleroma.Web.XML
6
7
  require Logger

8
  alias Pleroma.{Repo, User, Web, Object, Activity}
9
  alias Pleroma.Web.ActivityPub.ActivityPub
lain's avatar
lain committed
10
  alias Pleroma.Web.{WebFinger, Websub}
normandy's avatar
normandy committed
11
  alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
lain's avatar
lain committed
12
  alias Pleroma.Web.ActivityPub.Transmogrifier
lain's avatar
lain committed
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  def is_representable?(%Activity{data: data}) do
    object = Object.normalize(data["object"])

    cond do
      is_nil(object) ->
        false

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

      true ->
        false
    end
  end

lain's avatar
lain committed
29
30
31
32
  def feed_path(user) do
    "#{user.ap_id}/feed.atom"
  end

lain's avatar
lain committed
33
  def pubsub_path(user) do
lain's avatar
lain committed
34
    "#{Web.base_url()}/push/hub/#{user.nickname}"
lain's avatar
lain committed
35
36
  end

37
38
39
40
  def salmon_path(user) do
    "#{user.ap_id}/salmon"
  end

41
  def remote_follow_path do
lain's avatar
lain committed
42
    "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
43
44
  end

45
  def handle_incoming(xml_string) do
lain's avatar
lain committed
46
47
48
    with doc when doc != :error <- parse_document(xml_string) do
      entries = :xmerl_xpath.string('//entry', doc)

lain's avatar
lain committed
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
      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
65
66
67
              'http://activitystrea.ms/schema/1.0/unfollow' ->
                with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity

lain's avatar
lain committed
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
              '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
95
          end
lain's avatar
lain committed
96
97
        end)
        |> Enum.filter(& &1)
98

lain's avatar
lain committed
99
100
101
102
      {:ok, activities}
    else
      _e -> {:error, []}
    end
103
104
  end

lain's avatar
lain committed
105
  def make_share(entry, doc, retweeted_activity) do
lain's avatar
lain committed
106
    with {:ok, actor} <- find_make_or_update_user(doc),
107
         %Object{} = object <- Object.normalize(retweeted_activity.data["object"]),
lain's avatar
lain committed
108
109
         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
110
111
112
113
114
      {:ok, activity}
    end
  end

  def handle_share(entry, doc) do
lain's avatar
lain committed
115
    with {:ok, retweeted_activity} <- get_or_build_object(entry),
lain's avatar
lain committed
116
117
118
119
120
121
122
         {:ok, activity} <- make_share(entry, doc, retweeted_activity) do
      {:ok, activity, retweeted_activity}
    else
      e -> {:error, e}
    end
  end

lain's avatar
lain committed
123
  def make_favorite(entry, doc, favorited_activity) do
124
    with {:ok, actor} <- find_make_or_update_user(doc),
125
         %Object{} = object <- Object.normalize(favorited_activity.data["object"]),
lain's avatar
lain committed
126
127
         id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
         {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
128
129
130
131
      {:ok, activity}
    end
  end

lain's avatar
lain committed
132
133
134
135
136
137
  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
138
          NoteHandler.handle_note(object, object)
lain's avatar
lain committed
139
140
141
142
        end
    end
  end

143
  def get_or_try_fetching(entry) do
lain's avatar
lain committed
144
    Logger.debug("Trying to get entry from db")
lain's avatar
lain committed
145

146
147
148
    with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
      {:ok, activity}
lain's avatar
lain committed
149
150
    else
      _ ->
lain's avatar
lain committed
151
        Logger.debug("Couldn't get, will try to fetch")
lain's avatar
lain committed
152
153
154

        with href when not is_nil(href) <-
               string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
lain's avatar
lain committed
155
             {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
156
          {:ok, favorited_activity}
lain's avatar
lain committed
157
158
        else
          e -> Logger.debug("Couldn't find href: #{inspect(e)}")
159
160
161
162
        end
    end
  end

163
  def handle_favorite(entry, doc) do
164
    with {:ok, favorited_activity} <- get_or_try_fetching(entry),
165
166
167
168
169
170
171
         {:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
      {:ok, activity, favorited_activity}
    else
      e -> {:error, e}
    end
  end

172
173
  def get_attachments(entry) do
    :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
lain's avatar
lain committed
174
    |> Enum.map(fn enclosure ->
175
176
177
178
      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
179
180
181
182
183
184
185
          "url" => [
            %{
              "type" => "Link",
              "mediaType" => type,
              "href" => href
            }
          ]
186
187
188
        }
      end
    end)
lain's avatar
lain committed
189
    |> Enum.filter(& &1)
190
191
  end

lain's avatar
lain committed
192
  @doc """
lain's avatar
lain committed
193
    Gets the content from a an entry.
lain's avatar
lain committed
194
  """
lain's avatar
lain committed
195
  def get_content(entry) do
lain's avatar
lain committed
196
197
    string_from_xpath("//content", entry)
  end
lain's avatar
lain committed
198

lain's avatar
lain committed
199
200
201
202
  @doc """
    Get the cw that mastodon uses.
  """
  def get_cw(entry) do
lain's avatar
lain committed
203
    with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
lain's avatar
lain committed
204
      cw
lain's avatar
lain committed
205
206
    else
      _e -> nil
lain's avatar
lain committed
207
208
209
    end
  end

lain's avatar
lain committed
210
211
  def get_tags(entry) do
    :xmerl_xpath.string('//category', entry)
lain's avatar
lain committed
212
213
    |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
    |> Enum.filter(& &1)
lain's avatar
lain committed
214
    |> Enum.map(&String.downcase/1)
lain's avatar
lain committed
215
216
  end

217
  def maybe_update(doc, user) do
lain's avatar
lain committed
218
219
220
221
222
223
    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
224

lain's avatar
lain committed
225
  def maybe_update_ostatus(doc, user) do
226
227
228
    old_data = %{
      avatar: user.avatar,
      bio: user.bio,
lain's avatar
lain committed
229
      name: user.name
230
231
232
233
    }

    with false <- user.local,
         avatar <- make_avatar_object(doc),
lain's avatar
lain committed
234
         bio <- string_from_xpath("//author[1]/summary", doc),
lain's avatar
lain committed
235
         name <- string_from_xpath("//author[1]/poco:displayName", doc),
lain's avatar
lain committed
236
237
238
         new_data <- %{
           avatar: avatar || old_data.avatar,
           name: name || old_data.name,
lain's avatar
lain committed
239
           bio: bio || old_data.bio
lain's avatar
lain committed
240
         },
241
242
         false <- new_data == old_data do
      change = Ecto.Changeset.change(user, new_data)
lain's avatar
lain committed
243
      User.update_and_set_cache(change)
lain's avatar
lain committed
244
245
246
    else
      _ ->
        {:ok, user}
247
248
249
    end
  end

250
251
  def find_make_or_update_user(doc) do
    uri = string_from_xpath("//author/uri[1]", doc)
lain's avatar
lain committed
252

253
    with {:ok, user} <- find_or_make_user(uri) do
254
      maybe_update(doc, user)
255
256
257
    end
  end

lain's avatar
lain committed
258
  def find_or_make_user(uri) do
lain's avatar
lain committed
259
    query = from(user in User, where: user.ap_id == ^uri)
260
261
262
263

    user = Repo.one(query)

    if is_nil(user) do
lain's avatar
lain committed
264
      make_user(uri)
265
266
267
268
269
    else
      {:ok, user}
    end
  end

270
  def make_user(uri, update \\ false) do
lain's avatar
lain committed
271
272
    with {:ok, info} <- gather_user_info(uri) do
      data = %{
273
274
275
        name: info["name"],
        nickname: info["nickname"] <> "@" <> info["host"],
        ap_id: info["uri"],
lain's avatar
lain committed
276
        info: info,
277
278
        avatar: info["avatar"],
        bio: info["bio"]
lain's avatar
lain committed
279
      }
lain's avatar
lain committed
280

281
282
      with false <- update,
           %User{} = user <- User.get_by_ap_id(data.ap_id) do
283
        {:ok, user}
lain's avatar
lain committed
284
285
      else
        _e -> User.insert_or_update_user(data)
286
      end
lain's avatar
lain committed
287
    end
288
289
290
  end

  # TODO: Just takes the first one for now.
lain's avatar
lain committed
291
292
293
  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)
294
295
296
297

    if href do
      %{
        "type" => "Image",
lain's avatar
lain committed
298
299
300
301
302
303
304
        "url" => [
          %{
            "type" => "Link",
            "mediaType" => type,
            "href" => href
          }
        ]
305
306
307
308
      }
    else
      nil
    end
lain's avatar
lain committed
309
  end
lain's avatar
lain committed
310
311
312

  def gather_user_info(username) do
    with {:ok, webfinger_data} <- WebFinger.finger(username),
313
314
         {: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
315
316
317
318
    else
      e ->
        Logger.debug(fn -> "Couldn't gather info for #{username}" end)
        {:error, e}
lain's avatar
lain committed
319
320
    end
  end
321
322
323
324
325

  # 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
326
  @gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
327
328
329
330
331
  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
332

333
334
335
      Regex.match?(@gs_regex, body) ->
        [[_, match]] = Regex.scan(@gs_regex, body)
        {:ok, match}
lain's avatar
lain committed
336

lain's avatar
lain committed
337
338
339
      Regex.match?(@gs_classic_regex, body) ->
        [[_, match]] = Regex.scan(@gs_classic_regex, body)
        {:ok, match}
lain's avatar
lain committed
340

341
      true ->
feld's avatar
feld committed
342
343
        Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
        {:error, "Couldn't find the Atom link"}
344
345
346
    end
  end

lain's avatar
lain committed
347
  def fetch_activity_from_atom_url(url) do
lain's avatar
lain committed
348
    with true <- String.starts_with?(url, "http"),
Maksim's avatar
Maksim committed
349
         {:ok, %{body: body, status: code}} when code in 200..299 <-
lain's avatar
lain committed
350
351
           @httpoison.get(
             url,
Hakaba Hitoyo's avatar
Hakaba Hitoyo committed
352
             [{:Accept, "application/atom+xml"}]
lain's avatar
lain committed
353
           ) do
lain's avatar
lain committed
354
355
      Logger.debug("Got document from #{url}, handling...")
      handle_incoming(body)
lain's avatar
lain committed
356
357
358
359
    else
      e ->
        Logger.debug("Couldn't get #{url}: #{inspect(e)}")
        e
lain's avatar
lain committed
360
361
362
    end
  end

363
  def fetch_activity_from_html_url(url) do
lain's avatar
lain committed
364
    Logger.debug("Trying to fetch #{url}")
lain's avatar
lain committed
365

lain's avatar
lain committed
366
    with true <- String.starts_with?(url, "http"),
Hakaba Hitoyo's avatar
Hakaba Hitoyo committed
367
         {:ok, %{body: body}} <- @httpoison.get(url, []),
lain's avatar
lain committed
368
         {:ok, atom_url} <- get_atom_url(body) do
lain's avatar
lain committed
369
      fetch_activity_from_atom_url(atom_url)
lain's avatar
lain committed
370
371
372
373
    else
      e ->
        Logger.debug("Couldn't get #{url}: #{inspect(e)}")
        e
374
    end
lain's avatar
lain committed
375
  end
lain's avatar
lain committed
376
377

  def fetch_activity_from_url(url) do
lain's avatar
lain committed
378
379
380
381
    try do
      with {:ok, activities} when length(activities) > 0 <- fetch_activity_from_atom_url(url) do
        {:ok, activities}
      else
lain's avatar
lain committed
382
383
384
385
        _e ->
          with {:ok, activities} <- fetch_activity_from_html_url(url) do
            {:ok, activities}
          end
lain's avatar
lain committed
386
387
388
389
390
      end
    rescue
      e ->
        Logger.debug("Couldn't get #{url}: #{inspect(e)}")
        {:error, "Couldn't get #{url}: #{inspect(e)}"}
lain's avatar
lain committed
391
392
    end
  end
lain's avatar
lain committed
393
end