activity_pub.ex 9.19 KB
Newer Older
lain's avatar
lain committed
1
defmodule Pleroma.Web.ActivityPub.ActivityPub do
Thog's avatar
Thog committed
2
  alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
lain's avatar
lain committed
3
  alias Pleroma.Web.OStatus
lain's avatar
lain committed
4
  import Ecto.Query
lain's avatar
lain committed
5
  import Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
6
  require Logger
lain's avatar
lain committed
7

lain's avatar
lain committed
8
9
  @httpoison Application.get_env(:pleroma, :httpoison)

lain's avatar
lain committed
10
11
12
13
  def get_recipients(data) do
    (data["to"] || []) ++ (data["cc"] || [])
  end

lain's avatar
lain committed
14
  def insert(map, local \\ true) when is_map(map) do
lain's avatar
lain committed
15
16
17
    with nil <- Activity.get_by_ap_id(map["id"]),
         map <- lazy_put_activity_defaults(map),
         :ok <- insert_full_object(map) do
lain's avatar
lain committed
18
      {:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"], recipients: get_recipients(map)})
19
      Notification.create_notifications(activity)
lain's avatar
lain committed
20
      stream_out(activity)
21
      {:ok, activity}
lain's avatar
lain committed
22
23
24
    else
      %Activity{} = activity -> {:ok, activity}
      error -> {:error, error}
lain's avatar
lain committed
25
    end
lain's avatar
lain committed
26
  end
lain's avatar
lain committed
27

lain's avatar
lain committed
28
29
30
31
  def stream_out(activity) do
    if activity.data["type"] in ["Create", "Announce"] do
      Pleroma.Web.Streamer.stream("user", activity)
      if Enum.member?(activity.data["to"], "https://www.w3.org/ns/activitystreams#Public") do
lain's avatar
lain committed
32
        Pleroma.Web.Streamer.stream("public", activity)
lain's avatar
lain committed
33
        if activity.local do
Roger Braun's avatar
Roger Braun committed
34
35
          Pleroma.Web.Streamer.stream("public:local", activity)
        end
lain's avatar
lain committed
36
      end
lain's avatar
lain committed
37
38
39
40
41
42
43
    end
  end

  def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do
    with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional),
         {:ok, activity} <- insert(create_data, local),
         :ok <- maybe_federate(activity) do
44
45
      {:ok, activity}
    end
lain's avatar
lain committed
46
  end
lain's avatar
lain committed
47

lain's avatar
lain committed
48
  # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
Thog's avatar
Thog committed
49
  def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
lain's avatar
lain committed
50
51
52
53
54
55
56
57
58
    with nil <- get_existing_like(ap_id, object),
         like_data <- make_like_data(user, object, activity_id),
         {:ok, activity} <- insert(like_data, local),
         {:ok, object} <- add_like_to_object(activity, object),
         :ok <- maybe_federate(activity) do
      {:ok, activity, object}
    else
      %Activity{} = activity -> {:ok, activity, object}
      error -> {:error, error}
lain's avatar
lain committed
59
    end
lain's avatar
lain committed
60
61
  end

lain's avatar
lain committed
62
63
64
65
  def unlike(%User{} = actor, %Object{} = object) do
    with %Activity{} = activity <- get_existing_like(actor.ap_id, object),
         {:ok, _activity} <- Repo.delete(activity),
         {:ok, object} <- remove_like_from_object(activity, object) do
lain's avatar
lain committed
66
      {:ok, object}
lain's avatar
lain committed
67
      else _e -> {:ok, object}
lain's avatar
lain committed
68
69
70
    end
  end

Thog's avatar
Thog committed
71
  def announce(%User{ap_id: _} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
lain's avatar
lain committed
72
73
74
75
76
77
78
79
    with announce_data <- make_announce_data(user, object, activity_id),
         {:ok, activity} <- insert(announce_data, local),
         {:ok, object} <- add_announce_to_object(activity, object),
         :ok <- maybe_federate(activity) do
      {:ok, activity, object}
    else
      error -> {:error, error}
    end
lain's avatar
lain committed
80
81
  end

lain's avatar
lain committed
82
83
84
85
86
87
  def follow(follower, followed, activity_id \\ nil, local \\ true) do
    with data <- make_follow_data(follower, followed, activity_id),
         {:ok, activity} <- insert(data, local),
         :ok <- maybe_federate(activity) do
      {:ok, activity}
    end
lain's avatar
lain committed
88
89
  end

lain's avatar
lain committed
90
91
92
93
94
95
96
  def unfollow(follower, followed, local \\ true) do
    with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
         unfollow_data <- make_unfollow_data(follower, followed, follow_activity),
         {:ok, activity} <- insert(unfollow_data, local),
         :ok, maybe_federate(activity) do
      {:ok, activity}
    end
lain's avatar
lain committed
97
98
  end

lain's avatar
lain committed
99
100
101
102
103
104
105
106
  def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
    user = User.get_cached_by_ap_id(actor)
    data = %{
      "type" => "Delete",
      "actor" => actor,
      "object" => id,
      "to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
    }
lain's avatar
lain committed
107
108
109
    with Repo.delete(object),
         Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
         {:ok, activity} <- insert(data, local),
lain's avatar
lain committed
110
111
112
113
114
         :ok <- maybe_federate(activity) do
      {:ok, activity}
    end
  end

115
  def fetch_activities_for_context(context, opts \\ %{}) do
lain's avatar
lain committed
116
    query = from activity in Activity,
lain's avatar
lain committed
117
      where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context),
118
      order_by: [desc: :id]
119
    query = restrict_blocked(query, opts)
lain's avatar
lain committed
120
    Repo.all(query)
lain's avatar
lain committed
121
122
  end

lain's avatar
lain committed
123
  def fetch_public_activities(opts \\ %{}) do
lain's avatar
lain committed
124
125
126
127
    public = ["https://www.w3.org/ns/activitystreams#Public"]
    fetch_activities(public, opts)
  end

lain's avatar
lain committed
128
129
130
131
  defp restrict_since(query, %{"since_id" => since_id}) do
    from activity in query, where: activity.id > ^since_id
  end
  defp restrict_since(query, _), do: query
lain's avatar
lain committed
132

Roger Braun's avatar
Roger Braun committed
133
134
135
136
137
138
  defp restrict_tag(query, %{"tag" => tag}) do
    from activity in query,
      where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
  end
  defp restrict_tag(query, _), do: query

lain's avatar
lain committed
139
  defp restrict_recipients(query, recipients) do
Roger Braun's avatar
Roger Braun committed
140
141
142
143
144
    Enum.reduce(recipients, query, fn (recipient, q) ->
      map = %{ to: [recipient] }
      from activity in q,
      or_where: fragment(~s(? @> ?), activity.data, ^map)
    end)
lain's avatar
lain committed
145
  end
lain's avatar
lain committed
146

lain's avatar
lain committed
147
148
  defp restrict_local(query, %{"local_only" => true}) do
    from activity in query, where: activity.local == true
lain's avatar
lain committed
149
  end
lain's avatar
lain committed
150
  defp restrict_local(query, _), do: query
lain's avatar
lain committed
151

lain's avatar
lain committed
152
153
  defp restrict_max(query, %{"max_id" => max_id}) do
    from activity in query, where: activity.id < ^max_id
154
  end
lain's avatar
lain committed
155
  defp restrict_max(query, _), do: query
156

lain's avatar
lain committed
157
158
  defp restrict_actor(query, %{"actor_id" => actor_id}) do
    from activity in query,
lain's avatar
lain committed
159
      where: activity.actor == ^actor_id
160
  end
lain's avatar
lain committed
161
  defp restrict_actor(query, _), do: query
162

lain's avatar
lain committed
163
164
165
  defp restrict_type(query, %{"type" => type}) when is_binary(type) do
    restrict_type(query, %{"type" => [type]})
  end
166
167
  defp restrict_type(query, %{"type" => type}) do
    from activity in query,
lain's avatar
lain committed
168
      where: fragment("?->>'type' = ANY(?)", activity.data, ^type)
169
170
171
  end
  defp restrict_type(query, _), do: query

172
173
174
175
176
177
  defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
    from activity in query,
      where: fragment("? <@ (? #> '{\"object\",\"likes\"}')", ^ap_id, activity.data)
  end
  defp restrict_favorited_by(query, _), do: query

eal's avatar
eal committed
178
  defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
eal's avatar
eal committed
179
180
181
182
183
    from activity in query,
      where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[])
  end
  defp restrict_media(query, _), do: query

184
  # Only search through last 100_000 activities by default
185
  defp restrict_recent(query, %{"whole_db" => true}), do: query
186
  defp restrict_recent(query, _) do
lain's avatar
lain committed
187
    since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
188
189
190
191
192

    from activity in query,
      where: activity.id > ^since
  end

lain's avatar
lain committed
193
194
  defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
    blocks = info["blocks"] || []
195
    from activity in query,
lain's avatar
lain committed
196
      where: fragment("not (? = ANY(?))", activity.actor, ^blocks)
197
198
199
  end
  defp restrict_blocked(query, _), do: query

lain's avatar
lain committed
200
201
202
  def fetch_activities(recipients, opts \\ %{}) do
    base_query = from activity in Activity,
      limit: 20,
lain's avatar
lain committed
203
      order_by: [fragment("? desc nulls last", activity.id)]
lain's avatar
lain committed
204

lain's avatar
lain committed
205
206
    base_query
    |> restrict_recipients(recipients)
Roger Braun's avatar
Roger Braun committed
207
    |> restrict_tag(opts)
lain's avatar
lain committed
208
209
210
211
    |> restrict_since(opts)
    |> restrict_local(opts)
    |> restrict_max(opts)
    |> restrict_actor(opts)
212
    |> restrict_type(opts)
213
    |> restrict_favorited_by(opts)
214
    |> restrict_recent(opts)
215
    |> restrict_blocked(opts)
eal's avatar
eal committed
216
    |> restrict_media(opts)
lain's avatar
lain committed
217
218
    |> Repo.all
    |> Enum.reverse
dtluna's avatar
dtluna committed
219
220
  end

lain's avatar
lain committed
221
  def upload(file) do
lain's avatar
lain committed
222
223
224
    data = Upload.store(file)
    Repo.insert(%Object{data: data})
  end
lain's avatar
lain committed
225

lain's avatar
lain committed
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
  def make_user_from_ap_id(ap_id) do
    with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]),
    {:ok, data} <- Poison.decode(body)
      do
      user_data = %{
        ap_id: data["id"],
        info: %{
          "ap_enabled" => true,
          "source_data" => data
        },
        nickname: "#{data["preferredUsername"]}@#{URI.parse(ap_id).host}",
        name: data["name"]
      }

      User.insert_or_update_user(user_data)
    end
  end
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274

  # TODO: Extract to own module, align as close to Mastodon format as possible.
  def sanitize_outgoing_activity_data(data) do
    data
    |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
  end

  def prepare_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
    with {:ok, user} <- OStatus.find_or_make_user(data["actor"]) do
      {:ok, data}
    else
      _e -> :error
    end
  end

  def prepare_incoming(_) do
    :error
  end

  def publish(actor, activity) do
    remote_users = Pleroma.Web.Salmon.remote_users(activity)
    data = sanitize_outgoing_activity_data(activity.data)
    Enum.each remote_users, fn(user) ->
      if user.info["ap_enabled"] do
        inbox = user.info["source_data"]["inbox"]
        Logger.info("Federating #{activity.data["id"]} to #{inbox}")
        host = URI.parse(inbox).host
        signature = Pleroma.Web.HTTPSignatures.sign(actor, %{host: host})
        @httpoison.post(inbox, Poison.encode!(data), [{"Content-Type", "application/activity+json"}, {"signature", signature}])
      end
    end
  end
lain's avatar
lain committed
275
end