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