transmogrifier.ex 6.24 KB
Newer Older
1
2
3
4
5
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
  @moduledoc """
  A module to handle coding from internal to wire ActivityPub and back.
  """
  alias Pleroma.User
lain's avatar
lain committed
6
  alias Pleroma.Object
7
  alias Pleroma.Activity
lain's avatar
lain committed
8
  alias Pleroma.Repo
9
10
  alias Pleroma.Web.ActivityPub.ActivityPub

lain's avatar
lain committed
11
12
  import Ecto.Query

13
14
15
16
17
18
  @doc """
  Modifies an incoming AP object (mastodon format) to our internal format.
  """
  def fix_object(object) do
    object
    |> Map.put("actor", object["attributedTo"])
lain's avatar
lain committed
19
    |> fix_attachments
lain's avatar
lain committed
20
21
22
23
24
25
    |> fix_context
  end

  def fix_context(object) do
    object
    |> Map.put("context", object["conversation"])
lain's avatar
lain committed
26
27
28
  end

  def fix_attachments(object) do
lain's avatar
lain committed
29
    attachments = (object["attachment"] || [])
lain's avatar
lain committed
30
    |> Enum.map(fn (data) ->
lain's avatar
lain committed
31
      url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
lain's avatar
lain committed
32
33
34
35
36
      Map.put(data, "url", url)
    end)

    object
    |> Map.put("attachment", attachments)
37
38
39
40
41
42
  end

  # TODO: validate those with a Ecto scheme
  # - tags
  # - emoji
  def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
lain's avatar
lain committed
43
44
    with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
         %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
45
46
47
48
49
50
51
52
53
54
55
56
57
58
      object = fix_object(data["object"])
      params = %{
        to: data["to"],
        object: object,
        actor: user,
        context: data["object"]["conversation"],
        local: false,
        published: data["published"],
        additional: Map.take(data, [
              "cc",
              "id"
            ])
      }

59
60
61
62
      if object["inReplyTo"] do
        {:ok, object} = ActivityPub.fetch_object_from_id(object["inReplyTo"])
      end

63
64
      ActivityPub.create(params)
    else
lain's avatar
lain committed
65
      %Activity{} = activity -> {:ok, activity}
66
67
68
69
      _e -> :error
    end
  end

70
71
  def handle_incoming(%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data) do
    with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
72
73
         %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
74
      ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true})
75
76
77
78
79
80
81
      User.follow(follower, followed)
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
82
83
  def handle_incoming(%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data) do
    with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
84
         {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
lain's avatar
lain committed
85
86
87
88
89
90
91
         {:ok, activity, object} <- ActivityPub.like(actor, object, id, false) do
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
92
93
  def handle_incoming(%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data) do
    with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
94
         {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
lain's avatar
lain committed
95
96
97
98
99
100
101
         {:ok, activity, object} <- ActivityPub.announce(actor, object, id, false) do
      {:ok, activity}
    else
      _e -> :error
    end
  end

lain's avatar
lain committed
102
103
104
105
  # TODO
  # Accept
  # Undo

106
107
  def handle_incoming(_), do: :error

108
109
110
111
  def get_obj_helper(id) do
    if object = Object.get_by_ap_id(id), do: {:ok, object}, else: nil
  end

lain's avatar
lain committed
112
113
114
115
116
117
  @doc
  """
  internal -> Mastodon
  """
  def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
    object = object
lain's avatar
lain committed
118
    |> set_sensitive
lain's avatar
lain committed
119
    |> add_hashtags
lain's avatar
lain committed
120
121
    |> add_mention_tags
    |> add_attributed_to
lain's avatar
lain committed
122
    |> prepare_attachments
lain's avatar
lain committed
123
    |> set_conversation
lain's avatar
lain committed
124
125
126
127
128
129
130
131

    data = data
    |> Map.put("object", object)
    |> Map.put("@context", "https://www.w3.org/ns/activitystreams")

    {:ok, data}
  end

132
  def prepare_outgoing(%{"type" => type} = data) do
133
134
135
136
137
138
    data = data
    |> Map.put("@context", "https://www.w3.org/ns/activitystreams")

    {:ok, data}
  end

lain's avatar
lain committed
139
140
141
142
143
144
145
146
  def add_hashtags(object) do
    tags = (object["tag"] || [])
    |> Enum.map fn (tag) -> %{"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}", "name" => "##{tag}", "type" => "Hashtag"} end

    object
    |> Map.put("tag", tags)
  end

lain's avatar
lain committed
147
  def add_mention_tags(object) do
lain's avatar
lain committed
148
149
    recipients = object["to"] ++ (object["cc"] || [])
    mentions = recipients
lain's avatar
lain committed
150
151
    |> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end)
    |> Enum.filter(&(&1))
lain's avatar
lain committed
152
    |> Enum.map(fn(user) -> %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"} end)
lain's avatar
lain committed
153

lain's avatar
lain committed
154
    tags = object["tag"] || []
lain's avatar
lain committed
155
156

    object
lain's avatar
lain committed
157
    |> Map.put("tag", tags ++ mentions)
lain's avatar
lain committed
158
159
  end

lain's avatar
lain committed
160
161
162
163
  def set_conversation(object) do
    Map.put(object, "conversation", object["context"])
  end

lain's avatar
lain committed
164
165
166
167
168
  def set_sensitive(object) do
    tags = object["tag"] || []
    Map.put(object, "sensitive", "nsfw" in tags)
  end

lain's avatar
lain committed
169
170
171
172
173
  def add_attributed_to(object) do
    attributedTo = object["attributedTo"] || object["actor"]

    object
    |> Map.put("attributedTo", attributedTo)
174
  end
lain's avatar
lain committed
175
176
177
178
179
180
181
182
183
184
185

  def prepare_attachments(object) do
    attachments = (object["attachment"] || [])
    |> Enum.map(fn (data) ->
      [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
      %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
    end)

    object
    |> Map.put("attachment", attachments)
  end
lain's avatar
lain committed
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214

  def upgrade_user_from_ap_id(ap_id) do
    with %User{} = user <- User.get_by_ap_id(ap_id),
         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
      data = data
      |> Map.put(:info, Map.merge(user.info, data[:info]))

      old_follower_address = user.follower_address
      {:ok, user} = User.upgrade_changeset(user, data)
      |> Repo.update()

      # This could potentially take a long time, do it in the background
      Task.start(fn ->
        q  = from a in Activity,
        where: ^old_follower_address in a.recipients,
        update: [set: [recipients: fragment("array_replace(?,?,?)", a.recipients, ^old_follower_address, ^user.follower_address)]]
        Repo.update_all(q, [])

        q  = from u in User,
        where: ^old_follower_address in u.following,
        update: [set: [following: fragment("array_replace(?,?,?)", u.following, ^old_follower_address, ^user.follower_address)]]
        Repo.update_all(q, [])
      end)

      {:ok, user}
    else
      e -> e
    end
  end
215
end