mastodon_api_controller.ex 46.3 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

5 6
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
  use Pleroma.Web, :controller
Haelwenn's avatar
Haelwenn committed
7 8 9 10 11 12 13 14
  alias Pleroma.Activity
  alias Pleroma.Config
  alias Pleroma.Filter
  alias Pleroma.Notification
  alias Pleroma.Object
  alias Pleroma.Repo
  alias Pleroma.Stats
  alias Pleroma.User
lain's avatar
lain committed
15
  alias Pleroma.Web
lain's avatar
lain committed
16
  alias Pleroma.Web.CommonAPI
17
  alias Pleroma.Web.MediaProxy
Haelwenn's avatar
Haelwenn committed
18
  alias Pleroma.Web.Push
Haelwenn's avatar
Haelwenn committed
19
  alias Push.Subscription
minibikini's avatar
minibikini committed
20

Haelwenn's avatar
Haelwenn committed
21 22 23 24 25 26 27 28 29 30 31
  alias Pleroma.Web.MastodonAPI.AccountView
  alias Pleroma.Web.MastodonAPI.FilterView
  alias Pleroma.Web.MastodonAPI.ListView
  alias Pleroma.Web.MastodonAPI.MastodonView
  alias Pleroma.Web.MastodonAPI.PushSubscriptionView
  alias Pleroma.Web.MastodonAPI.StatusView
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
  alias Pleroma.Web.OAuth.App
  alias Pleroma.Web.OAuth.Authorization
  alias Pleroma.Web.OAuth.Token
minibikini's avatar
minibikini committed
32

33
  import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
34
  import Ecto.Query
35

Thog's avatar
Thog committed
36
  require Logger
37

38
  @httpoison Application.get_env(:pleroma, :httpoison)
39
  @local_mastodon_name "Mastodon-Local"
40

41 42
  action_fallback(:errors)

43
  def create_app(conn, params) do
44
    scopes = oauth_scopes(params, ["read"])
45 46 47 48 49

    app_attrs =
      params
      |> Map.drop(["scope", "scopes"])
      |> Map.put("scopes", scopes)
50 51

    with cs <- App.register_changeset(%App{}, app_attrs),
52 53
         false <- cs.changes[:client_name] == @local_mastodon_name,
         {:ok, app} <- Repo.insert(cs) do
54
      res = %{
55
        id: app.id |> to_string,
56
        name: app.client_name,
57
        client_id: app.client_id,
58
        client_secret: app.client_secret,
59
        redirect_uri: app.redirect_uris,
60
        website: app.website
61 62 63 64 65 66
      }

      json(conn, res)
    end
  end

lain's avatar
lain committed
67 68 69 70 71 72 73 74 75 76 77
  defp add_if_present(
         map,
         params,
         params_field,
         map_field,
         value_function \\ fn x -> {:ok, x} end
       ) do
    if Map.has_key?(params, params_field) do
      case value_function.(params[params_field]) do
        {:ok, new_value} -> Map.put(map, map_field, new_value)
        :error -> map
lain's avatar
lain committed
78
      end
lain's avatar
lain committed
79 80 81 82
    else
      map
    end
  end
83

lain's avatar
lain committed
84 85
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
86

lain's avatar
lain committed
87 88 89
    user_params =
      %{}
      |> add_if_present(params, "display_name", :name)
Maxim Filippov's avatar
Maxim Filippov committed
90
      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
lain's avatar
lain committed
91 92 93 94
      |> add_if_present(params, "avatar", :avatar, fn value ->
        with %Plug.Upload{} <- value,
             {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
          {:ok, object.data}
lain's avatar
lain committed
95
        else
lain's avatar
lain committed
96
          _ -> :error
lain's avatar
lain committed
97
        end
lain's avatar
lain committed
98
      end)
lain's avatar
lain committed
99

lain's avatar
lain committed
100 101 102 103 104 105 106
    info_params =
      %{}
      |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
      |> add_if_present(params, "header", :banner, fn value ->
        with %Plug.Upload{} <- value,
             {:ok, object} <- ActivityPub.upload(value, type: :banner) do
          {:ok, object.data}
lain's avatar
lain committed
107
        else
lain's avatar
lain committed
108
          _ -> :error
lain's avatar
lain committed
109
        end
lain's avatar
lain committed
110
      end)
111

lain's avatar
lain committed
112
    info_cng = User.Info.mastodon_profile_update(user.info, info_params)
113

lain's avatar
lain committed
114 115
    with changeset <- User.update_changeset(user, user_params),
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
116 117 118 119
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
120

121
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
122 123 124 125 126 127 128 129
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
130
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
131
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
132 133 134
    json(conn, account)
  end

135
  def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
136 137
    with %User{} = user <- Repo.get(User, id),
         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
138
      account = AccountView.render("account.json", %{user: user, for: for_user})
139 140
      json(conn, account)
    else
lain's avatar
lain committed
141 142 143 144
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
145 146 147
    end
  end

148
  @mastodon_api_level "2.5.0"
lain's avatar
lain committed
149

lain's avatar
lain committed
150
  def masto_instance(conn, _params) do
Haelwenn's avatar
Haelwenn committed
151
    instance = Config.get(:instance)
href's avatar
href committed
152

lain's avatar
lain committed
153
    response = %{
lain's avatar
lain committed
154
      uri: Web.base_url(),
href's avatar
href committed
155 156
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
href's avatar
href committed
157
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
href's avatar
href committed
158
      email: Keyword.get(instance, :email),
lain's avatar
lain committed
159
      urls: %{
160
        streaming_api: Pleroma.Web.Endpoint.websocket_url()
lain's avatar
lain committed
161
      },
lain's avatar
lain committed
162 163
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
href's avatar
href committed
164
      max_toot_chars: Keyword.get(instance, :limit)
165 166
    }

lain's avatar
lain committed
167
    json(conn, response)
168
  end
lain's avatar
lain committed
169

170
  def peers(conn, _params) do
lain's avatar
lain committed
171
    json(conn, Stats.get_peers())
172 173
  end

174
  defp mastodonized_emoji do
href's avatar
href committed
175
    Pleroma.Emoji.get_all()
176
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
177 178
      url = to_string(URI.merge(Web.base_url(), relative_url))

179 180 181
      %{
        "shortcode" => shortcode,
        "static_url" => url,
182
        "visible_in_picker" => true,
183 184 185
        "url" => url
      }
    end)
186 187 188 189
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
190
    json(conn, mastodon_emoji)
191 192
  end

193
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
194 195
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
196

197 198 199
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
200 201 202 203

      {next_url, prev_url} =
        if param do
          {
204 205 206 207 208 209 210 211 212 213 214 215
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              param,
              Map.merge(params, %{max_id: min})
            ),
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              param,
              Map.merge(params, %{since_id: max})
            )
lain's avatar
lain committed
216 217 218
          }
        else
          {
219 220 221 222 223 224 225 226 227 228
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              Map.merge(params, %{max_id: min})
            ),
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              Map.merge(params, %{since_id: max})
            )
lain's avatar
lain committed
229 230 231
          }
        end

232 233 234 235 236 237 238
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
239
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
240 241 242 243
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
244
      |> Map.put("muting_user", user)
lain's avatar
lain committed
245
      |> Map.put("user", user)
lain's avatar
lain committed
246

lain's avatar
lain committed
247
    activities =
Haelwenn's avatar
Haelwenn committed
248 249
      [user.ap_id | user.following]
      |> ActivityPub.fetch_activities(params)
250
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
251
      |> Enum.reverse()
252 253

    conn
lain's avatar
lain committed
254
    |> add_link_headers(:home_timeline, activities)
href's avatar
href committed
255 256
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
257 258 259
  end

  def public_timeline(%{assigns: %{user: user}} = conn, params) do
260 261
    local_only = params["local"] in [true, "True", "true", "1"]

Haelwenn's avatar
Haelwenn committed
262
    activities =
lain's avatar
lain committed
263 264
      params
      |> Map.put("type", ["Create", "Announce"])
265
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
266
      |> Map.put("blocking_user", user)
267
      |> Map.put("muting_user", user)
Haelwenn's avatar
Haelwenn committed
268
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
269
      |> Enum.reverse()
lain's avatar
lain committed
270

lain's avatar
lain committed
271
    conn
272
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
href's avatar
href committed
273 274
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
275 276
  end

277 278
  def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
    with %User{} = user <- Repo.get(User, params["id"]) do
minibikini's avatar
minibikini committed
279
      activities = ActivityPub.fetch_user_activities(user, reading_user, params)
lain's avatar
lain committed
280

281 282
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
href's avatar
href committed
283 284
      |> put_view(StatusView)
      |> render("index.json", %{
285 286 287 288
        activities: activities,
        for: reading_user,
        as: :activity
      })
lain's avatar
lain committed
289 290 291
    end
  end

292
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
293
    query =
294 295 296 297
      ActivityPub.fetch_activities_query(
        [user.ap_id],
        Map.merge(params, %{"type" => "Create", visibility: "direct"})
      )
298

299 300 301
    activities = Repo.all(query)

    conn
302
    |> add_link_headers(:dm_timeline, activities)
href's avatar
href committed
303 304
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
305 306
  end

lain's avatar
lain committed
307
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
308 309
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
href's avatar
href committed
310 311 312
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user})
lain's avatar
lain committed
313 314 315
    end
  end

lain's avatar
lain committed
316 317
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
318 319 320 321 322 323 324 325 326 327
         activities <-
           ActivityPub.fetch_activities_for_context(activity.data["context"], %{
             "blocking_user" => user,
             "user" => user
           }),
         activities <-
           activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
         activities <-
           activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
         grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
lain's avatar
lain committed
328
      result = %{
lain's avatar
lain committed
329 330 331 332 333 334 335 336
        ancestors:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[true] || [],
            as: :activity
          )
          |> Enum.reverse(),
Haelwenn's avatar
Haelwenn committed
337
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
338 339 340 341 342 343 344 345
        descendants:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[false] || [],
            as: :activity
          )
          |> Enum.reverse()
Haelwenn's avatar
Haelwenn committed
346
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
347 348 349 350 351 352
      }

      json(conn, result)
    end
  end

353 354 355 356 357 358 359 360 361
  def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
      when length(media_ids) > 0 do
    params =
      params
      |> Map.put("status", ".")

    post_status(conn, params)
  end

Thog's avatar
Thog committed
362
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
363 364 365
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
lain's avatar
lain committed
366

lain's avatar
lain committed
367 368 369 370 371 372 373
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

    {:ok, activity} =
Thog's avatar
Thog committed
374
      Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
lain's avatar
lain committed
375

href's avatar
href committed
376 377 378
    conn
    |> put_view(StatusView)
    |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
379
  end
lain's avatar
lain committed
380 381 382 383 384 385 386 387 388 389 390

  def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
      json(conn, %{})
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Can't delete this post"})
    end
  end
lain's avatar
lain committed
391 392

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
393
    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
href's avatar
href committed
394 395 396
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
397 398
    end
  end
lain's avatar
lain committed
399

normandy's avatar
normandy committed
400
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
401
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
402
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
403 404 405
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
normandy's avatar
normandy committed
406 407 408
    end
  end

lain's avatar
lain committed
409
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
410
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
411
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
412 413 414
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
415 416 417 418
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
419
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
420
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
421 422 423
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
424 425
    end
  end
426

minibikini's avatar
minibikini committed
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
  def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
    end
  end

  def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
  end

448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
  def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
         %User{} = user <- User.get_by_nickname(user.nickname),
         true <- ActivityPub.visible_for_user?(activity, user),
         {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
  end

  def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
         %User{} = user <- User.get_by_nickname(user.nickname),
         true <- ActivityPub.visible_for_user?(activity, user),
         {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
  end

470
  def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
471
    activity = Activity.get_by_id(id)
472

473
    with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
474 475 476
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
477 478 479 480 481
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
482 483 484 485
    end
  end

  def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
486
    activity = Activity.get_by_id(id)
487

488
    with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
489 490 491
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
492 493 494
    end
  end

495 496
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
497 498

    result =
Haelwenn's avatar
Haelwenn committed
499 500
      notifications
      |> Enum.map(fn x -> render_notification(user, x) end)
lain's avatar
lain committed
501
      |> Enum.filter(& &1)
502

lain's avatar
lain committed
503 504 505
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
506 507
  end

508 509 510 511 512 513 514
  def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    with {:ok, notification} <- Notification.get(user, id) do
      json(conn, render_notification(user, notification))
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
515
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
    end
  end

  def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
    Notification.clear(user)
    json(conn, %{})
  end

  def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    with {:ok, _notif} <- Notification.dismiss(user, id) do
      json(conn, %{})
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
531
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
532 533 534
    end
  end

535 536
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
537
    q = from(u in User, where: u.id in ^id)
538
    targets = Repo.all(q)
href's avatar
href committed
539 540 541 542

    conn
    |> put_view(AccountView)
    |> render("relationships.json", %{user: user, targets: targets})
543 544
  end

545
  # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
Maksim's avatar
Maksim committed
546
  def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
547

548
  def update_media(%{assigns: %{user: user}} = conn, data) do
549
    with %Object{} = object <- Repo.get(Object, data["id"]),
550
         true <- Object.authorize_mutation(object, user),
551 552 553 554
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

555 556 557 558
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
559

560
      attachment_data = Map.put(new_data, "id", object.id)
href's avatar
href committed
561 562 563 564

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
lain's avatar
lain committed
565 566 567
    end
  end

568 569
  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
    with {:ok, object} <-
kaniini's avatar
kaniini committed
570 571
           ActivityPub.upload(
             file,
572 573 574 575
             actor: User.ap_id(user),
             description: Map.get(data, "description")
           ) do
      attachment_data = Map.put(object.data, "id", object.id)
href's avatar
href committed
576 577 578 579

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
580 581 582
    end
  end

583
  def favourited_by(conn, %{"id" => id}) do
Thog's avatar
Thog committed
584
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
585
      q = from(u in User, where: u.ap_id in ^likes)
586
      users = Repo.all(q)
href's avatar
href committed
587 588 589 590

      conn
      |> put_view(AccountView)
      |> render(AccountView, "accounts.json", %{users: users, as: :user})
591 592 593 594 595 596 597
    else
      _ -> json(conn, [])
    end
  end

  def reblogged_by(conn, %{"id" => id}) do
    with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
598
      q = from(u in User, where: u.ap_id in ^announces)
599
      users = Repo.all(q)
href's avatar
href committed
600 601 602 603

      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
604 605 606 607 608
    else
      _ -> json(conn, [])
    end
  end

Roger Braun's avatar
Roger Braun committed
609
  def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
610 611
    local_only = params["local"] in [true, "True", "true", "1"]

612
    tags =
613 614
      [params["tag"], params["any"]]
      |> List.flatten()
615 616 617 618
      |> Enum.uniq()
      |> Enum.filter(& &1)
      |> Enum.map(&String.downcase(&1))

619 620 621 622 623
    tag_all =
      params["all"] ||
        []
        |> Enum.map(&String.downcase(&1))

624 625 626 627 628
    tag_reject =
      params["none"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
629
    activities =
lain's avatar
lain committed
630 631
      params
      |> Map.put("type", "Create")
632
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
633
      |> Map.put("blocking_user", user)
634
      |> Map.put("muting_user", user)
635
      |> Map.put("tag", tags)
636
      |> Map.put("tag_all", tag_all)
637
      |> Map.put("tag_reject", tag_reject)
Haelwenn's avatar
Haelwenn committed
638
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
639
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
640 641

    conn
642
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
href's avatar
href committed
643 644
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
645 646
  end

647
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
648 649
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
650 651 652
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
653
          user.info.hide_followers -> []
654 655 656
          true -> followers
        end

href's avatar
href committed
657 658 659
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
660 661 662
    end
  end

663
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
664 665
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
666 667 668
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
669
          user.info.hide_follows -> []
670 671 672
          true -> followers
        end

href's avatar
href committed
673 674 675
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
676 677 678
    end
  end

679 680
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
href's avatar
href committed
681 682 683
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: follow_requests, as: :user})
684 685 686
    end
  end

kaniini's avatar
kaniini committed
687 688
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
689
         {:ok, follower} <- User.maybe_follow(follower, followed),
kaniini's avatar
kaniini committed
690
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
kaniini's avatar
kaniini committed
691
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
kaniini's avatar
kaniini committed
692 693
         {:ok, _activity} <-
           ActivityPub.accept(%{
kaniini's avatar
kaniini committed
694
             to: [follower.ap_id],
695
             actor: followed,
kaniini's avatar
kaniini committed
696 697 698
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
href's avatar
href committed
699 700 701
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
702 703 704 705 706 707 708 709
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

kaniini's avatar
kaniini committed
710 711 712 713 714 715 716
  def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
         {:ok, _activity} <-
           ActivityPub.reject(%{
             to: [follower.ap_id],
717
             actor: followed,
kaniini's avatar
kaniini committed
718 719 720
             object: follow_activity.data["id"],
             type: "Reject"
           }) do
href's avatar
href committed
721 722 723
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
724 725 726 727 728 729 730
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
731

eal's avatar
eal committed
732 733
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
734
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
735
         {:ok, _activity} <- ActivityPub.follow(follower, followed),
736
         {:ok, follower, followed} <-
href's avatar
href committed
737
           User.wait_and_refresh(
Haelwenn's avatar
Haelwenn committed
738
             Config.get([:activitypub, :follow_handshake_timeout]),
href's avatar
href committed
739 740 741
             follower,
             followed
           ) do
href's avatar
href committed
742 743 744
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
745
    else
Thog's avatar
Thog committed
746
      {:error, message} ->
eal's avatar
eal committed
747 748
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
749
        |> send_resp(403, Jason.encode!(%{"error" => message}))
750 751 752
    end
  end

eal's avatar
eal committed
753
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
754
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
755
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
Thog's avatar
Thog committed
756
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
href's avatar
href committed
757 758 759
      conn
      |> put_view(AccountView)
      |> render("account.json", %{user: followed, for: follower})
eal's avatar
eal committed
760
    else
Thog's avatar
Thog committed
761
      {:error, message} ->
eal's avatar
eal committed
762 763
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
764
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
765 766 767
    end
  end

768 769
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
770 771
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
href's avatar
href committed
772 773 774
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
775 776 777
    end
  end

778 779 780
  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
    with %User{} = muted <- Repo.get(User, id),
         {:ok, muter} <- User.mute(muter, muted) do
781 782 783 784 785 786 787 788
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: muter, target: muted})
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
789 790 791 792 793 794
    end
  end

  def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
    with %User{} = muted <- Repo.get(User, id),
         {:ok, muter} <- User.unmute(muter, muted) do
795 796 797 798 799 800 801 802
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: muter, target: muted})
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
803 804 805
    end
  end

vaartis's avatar
vaartis committed
806
  def mutes(%{assigns: %{user: user}} = conn, _) do
807 808
    with muted_accounts <- User.muted_users(user) do
      res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
vaartis's avatar
vaartis committed
809 810 811 812
      json(conn, res)
    end
  end

lain's avatar
lain committed
813 814
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
815 816
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
href's avatar
href committed
817 818 819
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
820
    else
Thog's avatar
Thog committed
821
      {:error, message} ->
lain's avatar
lain committed
822 823
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
824
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
825 826 827 828 829
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
830 831
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
href's avatar
href committed
832 833 834
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
835
    else
Thog's avatar
Thog committed
836
      {:error, message} ->
lain's avatar
lain committed
837 838
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
839
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
840 841 842
    end
  end

lain's avatar
lain committed
843
  def blocks(%{assigns: %{user: user}} = conn, _) do
844 845
    with blocked_accounts <- User.blocked_users(user) do
      res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
lain's avatar
lain committed
846 847 848 849
      json(conn, res)
    end
  end

eal's avatar
eal committed
850
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
lain's avatar
lain committed
851
    json(conn, info.domain_blocks || [])
eal's avatar
eal committed
852 853 854 855 856 857 858 859 860 861 862 863
  end

  def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
    User.block_domain(blocker, domain)
    json(conn, %{})
  end

  def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
    User.unblock_domain(blocker, domain)
    json(conn, %{})
  end

864
  def status_search(user, query) do
865 866
    fetched =
      if Regex.match?(~r/https?:/, query) do
867
        with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
868
             %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
869 870
             true <- ActivityPub.visible_for_user?(activity, user) do
          [activity]
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890
        else
          _e -> []
        end
      end || []

    q =
      from(
        a in Activity,
        where: fragment("?->>'type' = 'Create'", a.data),
        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
        where:
          fragment(
            "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
            a.data,
            ^query
          ),
        limit: 20,
        order_by: [desc: :id]
      )

891 892 893 894
    Repo.all(q) ++ fetched
  end

  def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
895
    accounts = User.search(query, params["resolve"] == "true", user)
896

897
    statuses = status_search(user, query)
898 899 900 901

    tags_path = Web.base_url() <> "/tag/"

    tags =
Haelwenn's avatar
Haelwenn committed
902 903
      query
      |> String.split()
904 905 906 907 908 909 910 911 912 913 914 915 916 917 918
      |> Enum.uniq()
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
      |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
      "hashtags" => tags
    }

    json(conn, res)
  end

919
  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
920
    accounts = User.search(query, params["resolve"] == "true", user)
lain's avatar
lain committed
921

922
    statuses = status_search(user, query)
lain's avatar
lain committed
923 924

    tags =
Haelwenn's avatar
Haelwenn committed
925 926
      query
      |> String.split()
lain's avatar
lain committed
927 928 929
      |> Enum.uniq()
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
lain's avatar
lain committed
930 931 932

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
933 934
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
935
      "hashtags" => tags
lain's avatar
lain committed
936 937 938 939 940
    }

    json(conn, res)
  end

lain's avatar
lain committed
941
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
942
    accounts = User.search(query, params["resolve"] == "true", user)
943 944 945 946 947 948

    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)

    json(conn, res)
  end

949
  def favourites(%{assigns: %{user: user}} = conn, params) do
Haelwenn's avatar
Haelwenn committed
950
    activities =
951
      params
lain's avatar
lain committed
952 953 954
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
Haelwenn's avatar
Haelwenn committed
955
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
956
      |> Enum.reverse()
957 958

    conn
959
    |> add_link_headers(:favourites, activities)
href's avatar
href committed
960 961
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
962 963
  end

964 965 966 967 968 969 970 971 972 973 974 975 976
  def bookmarks(%{assigns: %{user: user}} = conn, _) do
    user = Repo.get(User, user.id)

    activities =
      user.bookmarks
      |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
      |> Enum.reverse()

    conn
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
  end

eal's avatar
eal committed
977 978 979 980 981 982 983
  def get_lists(%{assigns: %{user: user}} = conn, opts) do
    lists = Pleroma.List.for_user(user, opts)
    res = ListView.render("lists.json", lists: lists)
    json(conn, res)
  end

  def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
984
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
985 986 987
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
988 989 990 991
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Record not found"})
eal's avatar
eal committed
992 993 994
    end
  end

995 996 997 998 999 1000
  def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
    lists = Pleroma.List.get_lists_account_belongs(user, account_id)
    res = ListView.render("lists.json", lists: lists)
    json(conn, res)
  end

eal's avatar
eal committed
1001
  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
1002
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
         {:ok, _list} <- Pleroma.List.delete(list) do
      json(conn, %{})
    else
      _e ->
        json(conn, "error")
    end
  end

  def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
    with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
      res = ListView.render("list.json", list: list)
      json(conn, res)
    end
  end

  def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
    accounts
    |> Enum.each(fn account_id ->
eal's avatar
eal committed
1021
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
1022
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
1023
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
1024 1025 1026 1027 1028 1029 1030 1031 1032
      end
    end)

    json(conn, %{})
  end

  def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
    accounts
    |> Enum.each(fn account_id ->
eal's avatar
eal committed
1033
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
1034 1035 1036 1037 1038 1039 1040 1041 1042
           %User{} = followed <- Repo.get(Pleroma.User, account_id) do
        Pleroma.List.unfollow(list, followed)
      end
    end)

    json(conn, %{})
  end

  def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
1043
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
1044
         {:ok, users} = Pleroma.List.get_following(list) do
href's avatar
href committed
1045 1046 1047
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
eal's avatar
eal committed
1048 1049 1050 1051
    end
  end

  def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
eal's avatar
eal committed
1052
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
1053 1054 1055 1056 1057 1058 1059 1060 1061 1062
         {:ok, list} <- Pleroma.List.rename(list, title) do
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e ->
        json(conn, "error")
    end
  end

  def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
Maksim's avatar
Maksim committed
1063
    with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
1064 1065 1066 1067
      params =
        params
        |> Map.put("type", "Create")
        |> Map.put("blocking_user", user)
1068
        |> Map.put("muting_user", user)
eal's avatar
eal committed
1069

1070 1071
      # we must filter the following list for the user to avoid leaking statuses the user
      # does not actually have permission to see (for more info, peruse security issue #270).
Haelwenn's avatar
Haelwenn committed
1072
      activities =
1073 1074
        following
        |> Enum.filter(fn x -> x in user.following end)
Haelwenn's avatar
Haelwenn committed