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

lain's avatar
lain committed
7
  def insert(map, local \\ true) when is_map(map) do
lain's avatar
lain committed
8
9
10
    map = map
    |> Map.put_new_lazy("id", &generate_activity_id/0)
    |> Map.put_new_lazy("published", &make_date/0)
lain's avatar
lain committed
11

lain's avatar
lain committed
12
    with %Activity{} = activity <- Activity.get_by_ap_id(map["id"]) do
lain's avatar
lain committed
13
      Logger.debug(fn -> "Already have activity, #{activity.id}, not inserting." end)
lain's avatar
lain committed
14
15
16
17
18
19
20
21
22
      {:ok, activity}
    else _e ->
      map = if is_map(map["object"]) do
        object = Map.put_new_lazy(map["object"], "id", &generate_object_id/0)
        Repo.insert!(%Object{data: object})
        Map.put(map, "object", object)
      else
        map
      end
lain's avatar
lain committed
23

lain's avatar
lain committed
24
25
      Repo.insert(%Activity{data: map, local: local})
    end
lain's avatar
lain committed
26
  end
lain's avatar
lain committed
27

lain's avatar
lain committed
28
  def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do
29
30
31
32
    published = published || make_date()

    activity = %{
      "type" => "Create",
lain's avatar
lain committed
33
      "to" => to |> Enum.uniq,
34
35
36
37
38
39
40
      "actor" => actor.ap_id,
      "object" => object,
      "published" => published,
      "context" => context
    }
    |> Map.merge(additional)

lain's avatar
lain committed
41
    with {:ok, activity} <- insert(activity, local) do
42
      if actor.local do
43
        Pleroma.Web.Federator.enqueue(:publish, activity)
44
45
46
47
       end

      {:ok, activity}
    end
lain's avatar
lain committed
48
  end
lain's avatar
lain committed
49

lain's avatar
lain committed
50
  def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, activity_id \\ nil, local \\ true) do
lain's avatar
lain committed
51
52
53
54
    cond do
      # There's already a like here, so return the original activity.
      ap_id in (object.data["likes"] || []) ->
        query = from activity in Activity,
lain's avatar
lain committed
55
          where: fragment("? @> ?", activity.data, ^%{actor: ap_id, object: id, type: "Like"})
lain's avatar
lain committed
56

lain's avatar
lain committed
57
58
59
60
61
62
63
        activity = Repo.one(query)
        {:ok, activity, object}
      true ->
        data = %{
          "type" => "Like",
          "actor" => ap_id,
          "object" => id,
lain's avatar
lain committed
64
65
          "to" => [User.ap_followers(user), object.data["actor"]],
          "context" => object.data["context"]
lain's avatar
lain committed
66
        }
lain's avatar
lain committed
67

lain's avatar
lain committed
68
69
        data = if activity_id, do: Map.put(data, "id", activity_id), else: data

70
        {:ok, activity} = insert(data, local)
lain's avatar
lain committed
71

lain's avatar
lain committed
72
73
74
75
76
77
        likes = [ap_id | (object.data["likes"] || [])] |> Enum.uniq

        new_data = object.data
        |> Map.put("like_count", length(likes))
        |> Map.put("likes", likes)

78
        changeset = Changeset.change(object, data: new_data)
lain's avatar
lain committed
79
80
        {:ok, object} = Repo.update(changeset)

lain's avatar
lain committed
81
82
        update_object_in_activities(object)

lain's avatar
lain committed
83
84
85
86
        if user.local do
          Pleroma.Web.Federator.enqueue(:publish, activity)
        end

lain's avatar
lain committed
87
88
        {:ok, activity, object}
    end
lain's avatar
lain committed
89
90
  end

lain's avatar
lain committed
91
  defp update_object_in_activities(%{data: %{"id" => id}} = object) do
lain's avatar
lain committed
92
    # TODO
lain's avatar
lain committed
93
    # Update activities that already had this. Could be done in a seperate process.
lain's avatar
lain committed
94
95
    # Alternatively, just don't do this and fetch the current object each time. Most
    # could probably be taken from cache.
lain's avatar
lain committed
96
97
98
    relevant_activities = Activity.all_by_object_ap_id(id)
    Enum.map(relevant_activities, fn (activity) ->
      new_activity_data = activity.data |> Map.put("object", object.data)
99
      changeset = Changeset.change(activity, data: new_activity_data)
lain's avatar
lain committed
100
101
102
103
      Repo.update(changeset)
    end)
  end

lain's avatar
lain committed
104
  def unlike(%User{ap_id: ap_id}, %Object{data: %{ "id" => id}} = object) do
lain's avatar
lain committed
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
    query = from activity in Activity,
      where: fragment("? @> ?", activity.data, ^%{actor: ap_id, object: id, type: "Like"})

    activity = Repo.one(query)

    if activity do
      # just delete for now...
      {:ok, _activity} = Repo.delete(activity)

      likes = (object.data["likes"] || []) |> List.delete(ap_id)

      new_data = object.data
      |> Map.put("like_count", length(likes))
      |> Map.put("likes", likes)

120
      changeset = Changeset.change(object, data: new_data)
lain's avatar
lain committed
121
122
123
124
125
126
127
128
129
130
      {:ok, object} = Repo.update(changeset)

      update_object_in_activities(object)

      {:ok, object}
    else
      {:ok, object}
    end
  end

lain's avatar
lain committed
131
  def generate_activity_id do
lain's avatar
lain committed
132
133
134
135
136
    generate_id("activities")
  end

  def generate_context_id do
    generate_id("contexts")
lain's avatar
lain committed
137
138
  end

lain's avatar
lain committed
139
  def generate_object_id do
140
    Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :object, Ecto.UUID.generate)
lain's avatar
lain committed
141
142
143
  end

  def generate_id(type) do
144
    "#{Web.base_url()}/#{type}/#{UUID.generate}"
lain's avatar
lain committed
145
146
  end

lain's avatar
lain committed
147
  def fetch_public_activities(opts \\ %{}) do
lain's avatar
lain committed
148
149
150
151
152
    public = ["https://www.w3.org/ns/activitystreams#Public"]
    fetch_activities(public, opts)
  end

  def fetch_activities(recipients, opts \\ %{}) do
lain's avatar
lain committed
153
    since_id = opts["since_id"] || 0
lain's avatar
lain committed
154

lain's avatar
lain committed
155
    query = from activity in Activity,
lain's avatar
lain committed
156
157
      limit: 20,
      order_by: [desc: :inserted_at]
lain's avatar
lain committed
158

lain's avatar
lain committed
159
160
161
162
163
164
165
166
167
    query = Enum.reduce(recipients, query, fn (recipient, q) ->
      map = %{ to: [recipient] }
      from activity in q,
      or_where: fragment(~s(? @> ?), activity.data, ^map)
    end)

    query = from activity in query,
      where: activity.id > ^since_id

lain's avatar
lain committed
168
169
170
171
172
173
    query = if opts["local_only"] do
      from activity in query, where: activity.local == true
    else
      query
    end

174
175
176
177
178
179
    query = if opts["max_id"] do
      from activity in query, where: activity.id < ^opts["max_id"]
    else
      query
    end

dtluna's avatar
dtluna committed
180
181
182
183
184
185
186
    query = if opts["actor_id"] do
      from activity in query,
        where: fragment("? @> ?", activity.data, ^%{actor: opts["actor_id"]})
    else
      query
    end

187
    Enum.reverse(Repo.all(query))
lain's avatar
lain committed
188
  end
lain's avatar
lain committed
189

lain's avatar
lain committed
190
  def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, activity_id \\ nil, local \\ true) do
lain's avatar
lain committed
191
192
193
194
    data = %{
      "type" => "Announce",
      "actor" => ap_id,
      "object" => id,
lain's avatar
lain committed
195
196
      "to" => [User.ap_followers(user), object.data["actor"]],
      "context" => object.data["context"]
lain's avatar
lain committed
197
198
    }

lain's avatar
lain committed
199
200
    data = if activity_id, do: Map.put(data, "id", activity_id), else: data

lain's avatar
lain committed
201
    {:ok, activity} = insert(data, local)
lain's avatar
lain committed
202
203
204
205
206
207
208

    announcements = [ap_id | (object.data["announcements"] || [])] |> Enum.uniq

    new_data = object.data
    |> Map.put("announcement_count", length(announcements))
    |> Map.put("announcements", announcements)

209
    changeset = Changeset.change(object, data: new_data)
lain's avatar
lain committed
210
211
212
213
    {:ok, object} = Repo.update(changeset)

    update_object_in_activities(object)

lain's avatar
lain committed
214
215
216
217
    if user.local do
      Pleroma.Web.Federator.enqueue(:publish, activity)
    end

lain's avatar
lain committed
218
219
220
    {:ok, activity, object}
  end

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
  def follow(%User{ap_id: follower_id, local: actor_local}, %User{ap_id: followed_id}, local \\ true) do
    data = %{
      "type" => "Follow",
      "actor" => follower_id,
      "to" => [followed_id],
      "object" => followed_id,
      "published" => make_date()
    }

    with {:ok, activity} <- insert(data, local) do
      if actor_local do
        Pleroma.Web.Federator.enqueue(:publish, activity)
       end

      {:ok, activity}
    end
  end

  def unfollow(follower, followed, local \\ true) do
    with follow_activity when not is_nil(follow_activity) <- fetch_latest_follow(follower, followed) do
      data = %{
        "type" => "Undo",
        "actor" => follower.ap_id,
        "to" => [followed.ap_id],
        "object" => follow_activity.data["id"],
        "published" => make_date()
      }

      with {:ok, activity} <- insert(data, local) do
        if follower.local do
          Pleroma.Web.Federator.enqueue(:publish, activity)
        end

        {:ok, activity}
      end
    end
  end

lain's avatar
lain committed
259
260
261
262
263
  def fetch_activities_for_context(context) do
    query = from activity in Activity,
      where: fragment("? @> ?", activity.data, ^%{ context: context })
    Repo.all(query)
  end
lain's avatar
lain committed
264

dtluna's avatar
dtluna committed
265
266
267
268
269
270
271
272
273
274
  def fetch_latest_follow(%User{ap_id: follower_id},
                          %User{ap_id: followed_id}) do
    query = from activity in Activity,
      where: fragment("? @> ?", activity.data, ^%{type: "Follow", actor: follower_id,
                                                  object: followed_id}),
      order_by: [desc: :inserted_at],
      limit: 1
    Repo.one(query)
  end

lain's avatar
lain committed
275
  def upload(file) do
lain's avatar
lain committed
276
277
278
    data = Upload.store(file)
    Repo.insert(%Object{data: data})
  end
lain's avatar
lain committed
279
280
281
282

  defp make_date do
    DateTime.utc_now() |> DateTime.to_iso8601
  end
lain's avatar
lain committed
283
end