activity_pub_controller.ex 15.4 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 4
# SPDX-License-Identifier: AGPL-3.0-only

lain's avatar
lain committed
5 6
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
  use Pleroma.Web, :controller
7

Haelwenn's avatar
Haelwenn committed
8
  alias Pleroma.Activity
9
  alias Pleroma.Delivery
Haelwenn's avatar
Haelwenn committed
10
  alias Pleroma.Object
11
  alias Pleroma.Object.Fetcher
12
  alias Pleroma.User
lain's avatar
lain committed
13
  alias Pleroma.Web.ActivityPub.ActivityPub
14
  alias Pleroma.Web.ActivityPub.InternalFetchActor
15
  alias Pleroma.Web.ActivityPub.ObjectView
16
  alias Pleroma.Web.ActivityPub.Relay
17
  alias Pleroma.Web.ActivityPub.Transmogrifier
18
  alias Pleroma.Web.ActivityPub.UserView
19
  alias Pleroma.Web.ActivityPub.Utils
20
  alias Pleroma.Web.ActivityPub.Visibility
lain's avatar
lain committed
21
  alias Pleroma.Web.Federator
lain's avatar
lain committed
22

lain's avatar
lain committed
23 24
  require Logger

lain's avatar
lain committed
25
  action_fallback(:errors)
26

27 28
  plug(
    Pleroma.Plugs.Cache,
29
    [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
30 31 32
    when action in [:activity, :object]
  )

href's avatar
href committed
33
  plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
34
  plug(:set_requester_reachable when action in [:inbox])
35 36 37
  plug(:relay_active? when action in [:relay])

  def relay_active?(conn, _) do
minibikini's avatar
minibikini committed
38
    if Pleroma.Config.get([:instance, :allow_relay]) do
39 40 41
      conn
    else
      conn
42 43
      |> render_error(:not_found, "not found")
      |> halt()
44 45 46
    end
  end

lain's avatar
lain committed
47
  def user(conn, %{"nickname" => nickname}) do
48
    with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
49
         {:ok, user} <- User.ensure_keys_present(user) do
lain's avatar
lain committed
50
      conn
51
      |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
52 53
      |> put_view(UserView)
      |> render("user.json", %{user: user})
Rachel H's avatar
Rachel H committed
54 55
    else
      nil -> {:error, :not_found}
56
      %{local: false} -> {:error, :not_found}
lain's avatar
lain committed
57 58 59
    end
  end

60 61
  def object(conn, %{"uuid" => uuid}) do
    with ap_id <- o_status_url(conn, :object, uuid),
lain's avatar
lain committed
62
         %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
lain's avatar
lain committed
63
         {_, true} <- {:public?, Visibility.is_public?(object)} do
lain's avatar
lain committed
64
      conn
65
      |> assign(:tracking_fun_data, object.id)
minibikini's avatar
minibikini committed
66
      |> set_cache_ttl_for(object)
67
      |> put_resp_content_type("application/activity+json")
minibikini's avatar
minibikini committed
68 69
      |> put_view(ObjectView)
      |> render("object.json", object: object)
lain's avatar
lain committed
70 71
    else
      {:public?, false} ->
72
        {:error, :not_found}
73 74 75
    end
  end

76 77
  def track_object_fetch(conn, nil), do: conn

78
  def track_object_fetch(conn, object_id) do
79 80
    with %{assigns: %{user: %User{id: user_id}}} <- conn do
      Delivery.create(object_id, user_id)
81 82 83 84 85
    end

    conn
  end

86 87 88
  def activity(conn, %{"uuid" => uuid}) do
    with ap_id <- o_status_url(conn, :activity, uuid),
         %Activity{} = activity <- Activity.normalize(ap_id),
lain's avatar
lain committed
89
         {_, true} <- {:public?, Visibility.is_public?(activity)} do
90
      conn
91
      |> maybe_set_tracking_data(activity)
minibikini's avatar
minibikini committed
92
      |> set_cache_ttl_for(activity)
93
      |> put_resp_content_type("application/activity+json")
minibikini's avatar
minibikini committed
94 95
      |> put_view(ObjectView)
      |> render("object.json", object: activity)
96
    else
minibikini's avatar
minibikini committed
97 98
      {:public?, false} -> {:error, :not_found}
      nil -> {:error, :not_found}
99 100 101
    end
  end

102 103 104 105 106
  defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
    object_id = Object.normalize(activity).id
    assign(conn, :tracking_fun_data, object_id)
  end

107
  defp maybe_set_tracking_data(conn, _activity), do: conn
108

minibikini's avatar
minibikini committed
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
  defp set_cache_ttl_for(conn, %Activity{object: object}) do
    set_cache_ttl_for(conn, object)
  end

  defp set_cache_ttl_for(conn, entity) do
    ttl =
      case entity do
        %Object{data: %{"type" => "Question"}} ->
          Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])

        %Object{} ->
          Pleroma.Config.get([:web_cache_ttl, :activity_pub])

        _ ->
          nil
      end

    assign(conn, :cache_ttl, ttl)
  end

Maksim's avatar
Maksim committed
129 130 131
  # GET /relay/following
  def following(%{assigns: %{relay: true}} = conn, _params) do
    conn
132
    |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
133 134
    |> put_view(UserView)
    |> render("following.json", %{user: Relay.get_actor()})
Maksim's avatar
Maksim committed
135 136
  end

137
  def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
138
    with %User{} = user <- User.get_cached_by_nickname(nickname),
139 140
         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
         {:show_follows, true} <-
141
           {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
142
      {page, _} = Integer.parse(page)
lain's avatar
lain committed
143

144
      conn
145
      |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
146 147
      |> put_view(UserView)
      |> render("following.json", %{user: user, page: page, for: for_user})
148 149 150
    else
      {:show_follows, _} ->
        conn
151
        |> put_resp_content_type("application/activity+json")
152
        |> send_resp(403, "")
153 154 155
    end
  end

156
  def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
157
    with %User{} = user <- User.get_cached_by_nickname(nickname),
158
         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
159
      conn
160
      |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
161 162
      |> put_view(UserView)
      |> render("following.json", %{user: user, for: for_user})
163 164 165
    end
  end

Maksim's avatar
Maksim committed
166 167 168
  # GET /relay/followers
  def followers(%{assigns: %{relay: true}} = conn, _params) do
    conn
169
    |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
170 171
    |> put_view(UserView)
    |> render("followers.json", %{user: Relay.get_actor()})
Maksim's avatar
Maksim committed
172 173
  end

174
  def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
175
    with %User{} = user <- User.get_cached_by_nickname(nickname),
176 177
         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
         {:show_followers, true} <-
178
           {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
179
      {page, _} = Integer.parse(page)
lain's avatar
lain committed
180

181
      conn
182
      |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
183 184
      |> put_view(UserView)
      |> render("followers.json", %{user: user, page: page, for: for_user})
185 186 187
    else
      {:show_followers, _} ->
        conn
188
        |> put_resp_content_type("application/activity+json")
189
        |> send_resp(403, "")
190 191 192
    end
  end

193
  def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
194
    with %User{} = user <- User.get_cached_by_nickname(nickname),
195
         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
196
      conn
197
      |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
198 199
      |> put_view(UserView)
      |> render("followers.json", %{user: user, for: for_user})
200 201 202
    end
  end

203 204 205
  def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
      when page? in [true, "true"] do
    with %User{} = user <- User.get_cached_by_nickname(nickname),
206 207 208 209 210
         {:ok, user} <- User.ensure_keys_present(user) do
      activities =
        if params["max_id"] do
          ActivityPub.fetch_user_activities(user, nil, %{
            "max_id" => params["max_id"],
rinpatch's avatar
rinpatch committed
211 212
            # This is a hack because postgres generates inefficient queries when filtering by
            # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
213 214 215 216 217 218 219 220 221 222
            "include_poll_votes" => true,
            "limit" => 10
          })
        else
          ActivityPub.fetch_user_activities(user, nil, %{
            "limit" => 10,
            "include_poll_votes" => true
          })
        end

223 224 225 226 227 228 229 230 231 232 233
      conn
      |> put_resp_content_type("application/activity+json")
      |> put_view(UserView)
      |> render("activity_collection_page.json", %{
        activities: activities,
        iri: "#{user.ap_id}/outbox"
      })
    end
  end

  def outbox(conn, %{"nickname" => nickname}) do
kaniini's avatar
kaniini committed
234
    with %User{} = user <- User.get_cached_by_nickname(nickname),
235
         {:ok, user} <- User.ensure_keys_present(user) do
kaniini's avatar
kaniini committed
236
      conn
237
      |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
238
      |> put_view(UserView)
239
      |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
kaniini's avatar
kaniini committed
240 241 242
    end
  end

243
  def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
244
    with %User{} = recipient <- User.get_cached_by_nickname(nickname),
Alexander Strizhakov's avatar
Alexander Strizhakov committed
245
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
246 247
         true <- Utils.recipient_in_message(recipient, actor, params),
         params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
minibikini's avatar
minibikini committed
248
      Federator.incoming_ap_doc(params)
249 250 251 252
      json(conn, "ok")
    end
  end

253
  def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
minibikini's avatar
minibikini committed
254
    Federator.incoming_ap_doc(params)
lain's avatar
lain committed
255
    json(conn, "ok")
lain's avatar
lain committed
256
  end
257

258 259 260 261 262 263
  # only accept relayed Creates
  def inbox(conn, %{"type" => "Create"} = params) do
    Logger.info(
      "Signature missing or not from author, relayed Create message, fetching object from source"
    )

264
    Fetcher.fetch_object_from_id(params["object"]["id"])
265 266 267 268

    json(conn, "ok")
  end

lain's avatar
lain committed
269
  def inbox(conn, params) do
lain's avatar
Fix.  
lain committed
270
    headers = Enum.into(conn.req_headers, %{})
lain's avatar
lain committed
271

272 273 274 275 276
    if String.contains?(headers["signature"], params["actor"]) do
      Logger.info(
        "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
      )

lain's avatar
lain committed
277 278 279
      Logger.info(inspect(conn.req_headers))
    end

280
    json(conn, dgettext("errors", "error"))
lain's avatar
lain committed
281
  end
lain's avatar
lain committed
282

283 284
  defp represent_service_actor(%User{} = user, conn) do
    with {:ok, user} <- User.ensure_keys_present(user) do
285
      conn
286
      |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
287 288
      |> put_view(UserView)
      |> render("user.json", %{user: user})
289 290 291 292 293
    else
      nil -> {:error, :not_found}
    end
  end

294 295 296 297 298 299 300
  defp represent_service_actor(nil, _), do: {:error, :not_found}

  def relay(conn, _params) do
    Relay.get_actor()
    |> represent_service_actor(conn)
  end

301 302 303 304 305
  def internal_fetch(conn, _params) do
    InternalFetchActor.get_actor()
    |> represent_service_actor(conn)
  end

Haelwenn's avatar
Haelwenn committed
306
  @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
307 308
  def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
    conn
309
    |> put_resp_content_type("application/activity+json")
Steven Fuchs's avatar
Steven Fuchs committed
310 311
    |> put_view(UserView)
    |> render("user.json", %{user: user})
312 313 314 315
  end

  def whoami(_conn, _params), do: {:error, :not_found}

316 317
  def read_inbox(
        %{assigns: %{user: %{nickname: nickname} = user}} = conn,
318 319 320
        %{"nickname" => nickname, "page" => page?} = params
      )
      when page? in [true, "true"] do
rinpatch's avatar
rinpatch committed
321 322
    activities =
      if params["max_id"] do
323
        ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
rinpatch's avatar
rinpatch committed
324 325 326 327
          "max_id" => params["max_id"],
          "limit" => 10
        })
      else
328
        ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
rinpatch's avatar
rinpatch committed
329 330 331 332 333 334 335 336 337
      end

    conn
    |> put_resp_content_type("application/activity+json")
    |> put_view(UserView)
    |> render("activity_collection_page.json", %{
      activities: activities,
      iri: "#{user.ap_id}/inbox"
    })
338 339 340 341 342 343 344 345 346 347 348
  end

  def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
        "nickname" => nickname
      }) do
    with {:ok, user} <- User.ensure_keys_present(user) do
      conn
      |> put_resp_content_type("application/activity+json")
      |> put_view(UserView)
      |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
    end
349
  end
350

351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
  def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
    err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)

    conn
    |> put_status(:forbidden)
    |> json(err)
  end

  def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
        "nickname" => nickname
      }) do
    err =
      dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
        nickname: nickname,
        as_nickname: as_nickname
      )

    conn
    |> put_status(:forbidden)
    |> json(err)
371 372
  end

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
  def handle_user_activity(user, %{"type" => "Create"} = params) do
    object =
      params["object"]
      |> Map.merge(Map.take(params, ["to", "cc"]))
      |> Map.put("attributedTo", user.ap_id())
      |> Transmogrifier.fix_object()

    ActivityPub.create(%{
      to: params["to"],
      actor: user,
      context: object["context"],
      object: object,
      additional: Map.take(params, ["cc"])
    })
  end

sxsdv1's avatar
sxsdv1 committed
389 390
  def handle_user_activity(user, %{"type" => "Delete"} = params) do
    with %Object{} = object <- Object.normalize(params["object"]),
391
         true <- user.is_moderator || user.ap_id == object.data["actor"],
sxsdv1's avatar
sxsdv1 committed
392 393 394
         {:ok, delete} <- ActivityPub.delete(object) do
      {:ok, delete}
    else
395
      _ -> {:error, dgettext("errors", "Can't delete object")}
sxsdv1's avatar
sxsdv1 committed
396 397 398
    end
  end

399 400 401 402 403
  def handle_user_activity(user, %{"type" => "Like"} = params) do
    with %Object{} = object <- Object.normalize(params["object"]),
         {:ok, activity, _object} <- ActivityPub.like(user, object) do
      {:ok, activity}
    else
404
      _ -> {:error, dgettext("errors", "Can't like object")}
405 406 407
    end
  end

408
  def handle_user_activity(_, _) do
409
    {:error, dgettext("errors", "Unhandled activity type")}
410 411
  end

412
  def update_outbox(
413
        %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
414
        %{"nickname" => nickname} = params
415
      ) do
416
    actor = user.ap_id()
417

418 419 420 421 422
    params =
      params
      |> Map.drop(["id"])
      |> Map.put("actor", actor)
      |> Transmogrifier.fix_addressing()
423

424
    with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
425
      conn
426 427 428 429 430 431 432 433
      |> put_status(:created)
      |> put_resp_header("location", activity.data["id"])
      |> json(activity.data)
    else
      {:error, message} ->
        conn
        |> put_status(:bad_request)
        |> json(message)
434 435 436
    end
  end

437 438 439 440 441 442 443 444 445 446 447 448
  def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
    err =
      dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
        nickname: nickname,
        as_nickname: user.nickname
      )

    conn
    |> put_status(:forbidden)
    |> json(err)
  end

449 450
  def errors(conn, {:error, :not_found}) do
    conn
451 452
    |> put_status(:not_found)
    |> json(dgettext("errors", "Not found"))
453 454
  end

455 456
  def errors(conn, _e) do
    conn
457 458
    |> put_status(:internal_server_error)
    |> json(dgettext("errors", "error"))
459
  end
460 461 462 463 464 465 466 467 468

  defp set_requester_reachable(%Plug.Conn{} = conn, _) do
    with actor <- conn.params["actor"],
         true <- is_binary(actor) do
      Pleroma.Instances.set_reachable(actor)
    end

    conn
  end
469 470 471 472 473 474 475 476 477 478 479 480 481

  defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
    {:ok, new_user} = User.ensure_keys_present(user)

    for_user =
      if new_user != user and match?(%User{}, for_user) do
        User.get_cached_by_nickname(for_user.nickname)
      else
        for_user
      end

    {new_user, for_user}
  end
Haelwenn's avatar
Haelwenn committed
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508

  # TODO: Add support for "object" field
  @doc """
  Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>

  Parameters:
  - (required) `file`: data of the media
  - (optionnal) `description`: description of the media, intended for accessibility

  Response:
  - HTTP Code: 201 Created
  - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
  """
  def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
    with {:ok, object} <-
           ActivityPub.upload(
             file,
             actor: User.ap_id(user),
             description: Map.get(data, "description")
           ) do
      Logger.debug(inspect(object))

      conn
      |> put_status(:created)
      |> json(object.data)
    end
  end
lain's avatar
lain committed
509
end