mastodon_api_controller.ex 40.9 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
7
  alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
lain's avatar
lain committed
8
  alias Pleroma.Web
minibikini's avatar
cleanup  
minibikini committed
9 10 11 12 13 14 15 16 17 18

  alias Pleroma.Web.MastodonAPI.{
    StatusView,
    AccountView,
    MastodonView,
    ListView,
    FilterView,
    PushSubscriptionView
  }

lain's avatar
lain committed
19
  alias Pleroma.Web.ActivityPub.ActivityPub
kaniini's avatar
kaniini committed
20
  alias Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
21
  alias Pleroma.Web.CommonAPI
lain's avatar
lain committed
22
  alias Pleroma.Web.OAuth.{Authorization, Token, App}
23
  alias Pleroma.Web.MediaProxy
minibikini's avatar
cleanup  
minibikini committed
24

Roger Braun's avatar
Roger Braun committed
25
  import Ecto.Query
Thog's avatar
Thog committed
26
  require Logger
27

28 29
  @httpoison Application.get_env(:pleroma, :httpoison)

30 31
  action_fallback(:errors)

32
  def create_app(conn, params) do
lain's avatar
lain committed
33 34
    with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
         {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
35
      res = %{
36
        id: app.id |> to_string,
37
        name: app.client_name,
38
        client_id: app.client_id,
39
        client_secret: app.client_secret,
40
        redirect_uri: app.redirect_uris,
41
        website: app.website
42 43 44 45 46 47
      }

      json(conn, res)
    end
  end

lain's avatar
lain committed
48 49 50 51 52 53 54 55 56 57 58
  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
59
      end
lain's avatar
lain committed
60 61 62 63
    else
      map
    end
  end
64

lain's avatar
lain committed
65 66
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
67

lain's avatar
lain committed
68 69 70
    user_params =
      %{}
      |> add_if_present(params, "display_name", :name)
Maxim Filippov's avatar
Maxim Filippov committed
71
      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
lain's avatar
lain committed
72 73 74 75
      |> 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
76
        else
lain's avatar
lain committed
77
          _ -> :error
lain's avatar
lain committed
78
        end
lain's avatar
lain committed
79
      end)
lain's avatar
lain committed
80

lain's avatar
lain committed
81 82 83 84 85 86 87
    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
88
        else
lain's avatar
lain committed
89
          _ -> :error
lain's avatar
lain committed
90
        end
lain's avatar
lain committed
91
      end)
92

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

lain's avatar
lain committed
95 96
    with changeset <- User.update_changeset(user, user_params),
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
97 98 99 100
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
101

102
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
103 104 105 106 107 108 109 110
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
111
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
112
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
113 114 115
    json(conn, account)
  end

116
  def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
117 118
    with %User{} = user <- Repo.get(User, id),
         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
119
      account = AccountView.render("account.json", %{user: user, for: for_user})
Roger Braun's avatar
Roger Braun committed
120 121
      json(conn, account)
    else
lain's avatar
lain committed
122 123 124 125
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
Roger Braun's avatar
Roger Braun committed
126 127 128
    end
  end

129
  @mastodon_api_level "2.5.0"
lain's avatar
lain committed
130

lain's avatar
lain committed
131
  def masto_instance(conn, _params) do
href's avatar
href committed
132 133
    instance = Pleroma.Config.get(:instance)

lain's avatar
lain committed
134
    response = %{
lain's avatar
lain committed
135
      uri: Web.base_url(),
href's avatar
href committed
136 137
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
href's avatar
href committed
138
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
href's avatar
href committed
139
      email: Keyword.get(instance, :email),
lain's avatar
lain committed
140
      urls: %{
lain's avatar
lain committed
141
        streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
lain's avatar
lain committed
142
      },
lain's avatar
lain committed
143 144
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
href's avatar
href committed
145
      max_toot_chars: Keyword.get(instance, :limit)
146 147
    }

lain's avatar
lain committed
148
    json(conn, response)
149
  end
lain's avatar
lain committed
150

151
  def peers(conn, _params) do
lain's avatar
lain committed
152
    json(conn, Stats.get_peers())
153 154
  end

155
  defp mastodonized_emoji do
href's avatar
href committed
156
    Pleroma.Emoji.get_all()
157
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
158 159
      url = to_string(URI.merge(Web.base_url(), relative_url))

160 161 162
      %{
        "shortcode" => shortcode,
        "static_url" => url,
163
        "visible_in_picker" => true,
164 165 166
        "url" => url
      }
    end)
167 168 169 170
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
171
    json(conn, mastodon_emoji)
172 173
  end

174
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
175 176
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
177

178 179 180
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
181 182 183 184

      {next_url, prev_url} =
        if param do
          {
185 186 187 188 189 190 191 192 193 194 195 196
            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
197 198 199
          }
        else
          {
200 201 202 203 204 205 206 207 208 209
            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
210 211 212
          }
        end

213 214 215 216 217 218 219
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
220
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
221 222 223 224 225
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
lain's avatar
lain committed
226

lain's avatar
lain committed
227 228
    activities =
      ActivityPub.fetch_activities([user.ap_id | user.following], params)
229
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
230
      |> Enum.reverse()
231 232

    conn
lain's avatar
lain committed
233
    |> add_link_headers(:home_timeline, activities)
href's avatar
href committed
234 235
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
236 237 238
  end

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

lain's avatar
lain committed
241 242 243
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
244
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
245
      |> Map.put("blocking_user", user)
lain's avatar
lain committed
246

lain's avatar
lain committed
247 248 249
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
lain's avatar
lain committed
250

lain's avatar
lain committed
251
    conn
252
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
href's avatar
href committed
253 254
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
255 256
  end

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

261 262
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
href's avatar
href committed
263 264
      |> put_view(StatusView)
      |> render("index.json", %{
265 266 267 268
        activities: activities,
        for: reading_user,
        as: :activity
      })
lain's avatar
lain committed
269 270 271
    end
  end

272
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
273
    query =
274 275 276 277
      ActivityPub.fetch_activities_query(
        [user.ap_id],
        Map.merge(params, %{"type" => "Create", visibility: "direct"})
      )
278

279 280 281
    activities = Repo.all(query)

    conn
282
    |> add_link_headers(:dm_timeline, activities)
href's avatar
href committed
283 284
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
285 286
  end

lain's avatar
lain committed
287
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
288 289
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
href's avatar
href committed
290 291 292
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user})
lain's avatar
lain committed
293 294 295
    end
  end

lain's avatar
lain committed
296 297
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
298 299 300 301 302 303 304 305 306 307
         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
308
      result = %{
lain's avatar
lain committed
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
        ancestors:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[true] || [],
            as: :activity
          )
          |> Enum.reverse(),
        descendants:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[false] || [],
            as: :activity
          )
          |> Enum.reverse()
lain's avatar
lain committed
325 326 327 328 329 330
      }

      json(conn, result)
    end
  end

331 332 333 334 335 336 337 338 339
  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
340
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
341 342 343
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
lain's avatar
lain committed
344

lain's avatar
lain committed
345 346 347 348 349 350 351
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

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

href's avatar
href committed
354 355 356
    conn
    |> put_view(StatusView)
    |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
357
  end
lain's avatar
lain committed
358 359 360 361 362 363 364 365 366 367 368

  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
369 370

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
371
    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
href's avatar
href committed
372 373 374
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
375 376
    end
  end
lain's avatar
lain committed
377

normandy's avatar
normandy committed
378
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
379
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
380
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
381 382 383
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
normandy's avatar
normandy committed
384 385 386
    end
  end

lain's avatar
lain committed
387
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
388
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
389
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
390 391 392
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
393 394 395 396
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
397
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
398
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
399 400 401
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
402 403
    end
  end
404

minibikini's avatar
minibikini committed
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
  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

426 427
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
428 429 430 431 432 433

    result =
      Enum.map(notifications, fn x ->
        render_notification(user, x)
      end)
      |> Enum.filter(& &1)
434

lain's avatar
lain committed
435 436 437
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
438 439
  end

440 441 442 443 444 445 446
  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
447
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
    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
463
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
464 465 466
    end
  end

Roger Braun's avatar
Roger Braun committed
467 468
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
469
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
470
    targets = Repo.all(q)
href's avatar
href committed
471 472 473 474

    conn
    |> put_view(AccountView)
    |> render("relationships.json", %{user: user, targets: targets})
Roger Braun's avatar
Roger Braun committed
475 476
  end

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

480
  def update_media(%{assigns: %{user: user}} = conn, data) do
481
    with %Object{} = object <- Repo.get(Object, data["id"]),
482
         true <- Object.authorize_mutation(object, user),
483 484 485 486
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

487 488 489 490
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
491

492
      attachment_data = Map.put(new_data, "id", object.id)
href's avatar
href committed
493 494 495 496

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

500 501 502 503 504 505 506
  def upload(%{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
      attachment_data = Map.put(object.data, "id", object.id)
href's avatar
href committed
507 508 509 510

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
511 512 513
    end
  end

514
  def favourited_by(conn, %{"id" => id}) do
Thog's avatar
Thog committed
515
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
516
      q = from(u in User, where: u.ap_id in ^likes)
517
      users = Repo.all(q)
href's avatar
href committed
518 519 520 521

      conn
      |> put_view(AccountView)
      |> render(AccountView, "accounts.json", %{users: users, as: :user})
522 523 524 525 526 527 528
    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
529
      q = from(u in User, where: u.ap_id in ^announces)
530
      users = Repo.all(q)
href's avatar
href committed
531 532 533 534

      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
535 536 537 538 539
    else
      _ -> json(conn, [])
    end
  end

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

lain's avatar
lain committed
543 544 545
    params =
      params
      |> Map.put("type", "Create")
546
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
547
      |> Map.put("blocking_user", user)
feld's avatar
feld committed
548
      |> Map.put("tag", String.downcase(params["tag"]))
Roger Braun's avatar
Roger Braun committed
549

lain's avatar
lain committed
550 551 552
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
553 554

    conn
555
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
href's avatar
href committed
556 557
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
558 559
  end

560
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
561 562
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
563 564 565 566 567 568 569
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

href's avatar
href committed
570 571 572
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
573 574 575
    end
  end

576
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
577 578
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
579 580 581 582 583 584 585
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

href's avatar
href committed
586 587 588
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
589 590 591
    end
  end

592 593
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
href's avatar
href committed
594 595 596
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: follow_requests, as: :user})
597 598 599
    end
  end

kaniini's avatar
kaniini committed
600 601
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
602
         {:ok, follower} <- User.maybe_follow(follower, followed),
kaniini's avatar
kaniini committed
603
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
kaniini's avatar
kaniini committed
604
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
kaniini's avatar
kaniini committed
605 606
         {:ok, _activity} <-
           ActivityPub.accept(%{
kaniini's avatar
kaniini committed
607
             to: [follower.ap_id],
kaniini's avatar
kaniini committed
608 609 610 611
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
href's avatar
href committed
612 613 614
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
615 616 617 618 619 620 621 622
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

kaniini's avatar
kaniini committed
623 624 625 626 627 628 629 630 631 632 633
  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],
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Reject"
           }) do
href's avatar
href committed
634 635 636
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
637 638 639 640 641 642 643
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
644

eal's avatar
eal committed
645 646
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
647
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
648
         {:ok, _activity} <- ActivityPub.follow(follower, followed),
649
         {:ok, follower, followed} <-
href's avatar
href committed
650 651 652 653 654
           User.wait_and_refresh(
             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
             follower,
             followed
           ) do
href's avatar
href committed
655 656 657
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
658
    else
Thog's avatar
Thog committed
659
      {:error, message} ->
eal's avatar
eal committed
660 661
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
662
        |> send_resp(403, Jason.encode!(%{"error" => message}))
663 664 665
    end
  end

eal's avatar
eal committed
666
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
667
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
668
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
Thog's avatar
Thog committed
669
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
href's avatar
href committed
670 671 672
      conn
      |> put_view(AccountView)
      |> render("account.json", %{user: followed, for: follower})
eal's avatar
eal committed
673
    else
Thog's avatar
Thog committed
674
      {:error, message} ->
eal's avatar
eal committed
675 676
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
677
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
678 679 680
    end
  end

681 682
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
683 684
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
href's avatar
href committed
685 686 687
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
688 689 690
    end
  end

lain's avatar
lain committed
691 692
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
693 694
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
href's avatar
href committed
695 696 697
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
698
    else
Thog's avatar
Thog committed
699
      {:error, message} ->
lain's avatar
lain committed
700 701
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
702
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
703 704 705 706 707
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
708 709
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
href's avatar
href committed
710 711 712
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
713
    else
Thog's avatar
Thog committed
714
      {:error, message} ->
lain's avatar
lain committed
715 716
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
717
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
718 719 720
    end
  end

lain's avatar
lain committed
721
  def blocks(%{assigns: %{user: user}} = conn, _) do
722 723
    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
724 725 726 727
      json(conn, res)
    end
  end

eal's avatar
eal committed
728
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
lain's avatar
lain committed
729
    json(conn, info.domain_blocks || [])
eal's avatar
eal committed
730 731 732 733 734 735 736 737 738 739 740 741
  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

742
  def status_search(user, query) do
743 744
    fetched =
      if Regex.match?(~r/https?:/, query) do
745
        with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
746
             %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
747 748
             true <- ActivityPub.visible_for_user?(activity, user) do
          [activity]
749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768
        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]
      )

769 770 771 772
    Repo.all(q) ++ fetched
  end

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

775
    statuses = status_search(user, query)
776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795

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

    tags =
      String.split(query)
      |> 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

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

799
    statuses = status_search(user, query)
lain's avatar
lain committed
800 801 802 803 804 805

    tags =
      String.split(query)
      |> 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
806 807 808

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
809 810
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
811
      "hashtags" => tags
lain's avatar
lain committed
812 813 814 815 816
    }

    json(conn, res)
  end

lain's avatar
lain committed
817
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
818
    accounts = User.search(query, params["resolve"] == "true", user)
819 820 821 822 823 824

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

    json(conn, res)
  end

825
  def favourites(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
826
    params =
827
      params
lain's avatar
lain committed
828 829 830
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
831

lain's avatar
lain committed
832 833 834
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
835 836

    conn
837
    |> add_link_headers(:favourites, activities)
href's avatar
href committed
838 839
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
840 841
  end

eal's avatar
eal committed
842 843 844 845 846 847 848
  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
849
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
850 851 852 853 854 855 856
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e -> json(conn, "error")
    end
  end

857 858 859 860 861 862
  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
863
  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
864
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882
         {: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
883
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
884
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
885
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
886 887 888 889 890 891 892 893 894
      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
895
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
896 897 898 899 900 901 902 903 904
           %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
905
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
906
         {:ok, users} = Pleroma.List.get_following(list) do
href's avatar
href committed
907 908 909
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
eal's avatar
eal committed
910 911 912 913
    end
  end

  def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
eal's avatar
eal committed
914
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
915 916 917 918 919 920 921 922 923 924
         {: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
925
    with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
926 927 928 929 930
      params =
        params
        |> Map.put("type", "Create")
        |> Map.put("blocking_user", user)

931 932 933 934 935 936
      # 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).
      following_to =
        following
        |> Enum.filter(fn x -> x in user.following end)

eal's avatar
eal committed
937
      activities =
938
        ActivityPub.fetch_activities_bounded(following_to, following, params)
eal's avatar
eal committed
939 940 941
        |> Enum.reverse()

      conn
href's avatar
href committed
942 943
      |> put_view(StatusView)
      |> render("index.json", %{activities: activities, for: user, as: :activity})
eal's avatar
eal committed
944 945 946 947 948 949 950 951
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Error."})
    end
  end

lain's avatar
lain committed
952
  def index(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
953 954 955
    token =
      conn
      |> get_session(:oauth_token)
lain's avatar
lain committed
956 957

    if user && token do
958
      mastodon_emoji = mastodonized_emoji()
kaniini's avatar
kaniini committed
959

href's avatar
href committed
960 961
      limit = Pleroma.Config.get([:instance, :limit])

kaniini's avatar
kaniini committed
962 963
      accounts =
        Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
lain's avatar
lain committed
964 965 966 967 968 969 970 971 972 973 974 975 976 977 978

      initial_state =
        %{
          meta: %{
            streaming_api_base_url:
              String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
            access_token: token,
            locale: "en",
            domain: Pleroma.Web.Endpoint.host(),
            admin: "1",
            me: "#{user.id}",
            unfollow_modal: false,
            boost_modal: false,
            delete_modal: true,
            auto_play_gif: false,
Haelwenn's avatar
Haelwenn committed
979
            display_sensitive_media: false,
980
            reduce_motion: false,
href's avatar
href committed
981
            max_toot_chars: limit
lain's avatar
lain committed
982
          },
983
          rights: %{
984 985
            delete_others_notice: !!user.info.is_moderator,
            admin: !!user.info.is_admin
986
          },
lain's avatar
lain committed