mastodon_api_controller.ex 39.4 KB
Newer Older
1 2 3 4
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# 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 259
  def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
    with %User{} = user <- Repo.get(User, params["id"]) do
      # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
eal's avatar
eal committed
260 261 262 263
      activities =
        if params["pinned"] == "true" do
          []
        else
264
          ActivityPub.fetch_user_activities(user, reading_user, params)
eal's avatar
eal committed
265
        end
lain's avatar
lain committed
266

267 268
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
href's avatar
href committed
269 270
      |> put_view(StatusView)
      |> render("index.json", %{
271 272 273 274
        activities: activities,
        for: reading_user,
        as: :activity
      })
lain's avatar
lain committed
275 276 277
    end
  end

278
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
279
    query =
280 281 282 283
      ActivityPub.fetch_activities_query(
        [user.ap_id],
        Map.merge(params, %{"type" => "Create", visibility: "direct"})
      )
284

285 286 287
    activities = Repo.all(query)

    conn
288
    |> add_link_headers(:dm_timeline, activities)
href's avatar
href committed
289 290
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
291 292
  end

lain's avatar
lain committed
293
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
294 295
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
href's avatar
href committed
296 297 298
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user})
lain's avatar
lain committed
299 300 301
    end
  end

lain's avatar
lain committed
302 303
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
304 305 306 307 308 309 310 311 312 313
         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
314
      result = %{
lain's avatar
lain committed
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
        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
331 332 333 334 335 336
      }

      json(conn, result)
    end
  end

337 338 339 340 341 342 343 344 345
  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
346
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
347 348 349 350
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
      |> Map.put("no_attachment_links", true)
lain's avatar
lain committed
351

lain's avatar
lain committed
352 353 354 355 356 357 358
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

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

href's avatar
href committed
361 362 363
    conn
    |> put_view(StatusView)
    |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
364
  end
lain's avatar
lain committed
365 366 367 368 369 370 371 372 373 374 375

  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
376 377

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
378
    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
href's avatar
href committed
379 380 381
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
382 383
    end
  end
lain's avatar
lain committed
384

normandy's avatar
normandy committed
385
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
386
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
387
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
href's avatar
href committed
388 389 390
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
normandy's avatar
normandy committed
391 392 393
    end
  end

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

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
404
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
lain's avatar
lain committed
405
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
href's avatar
href committed
406 407 408
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
409 410
    end
  end
411

412 413
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
414 415 416 417 418 419

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

lain's avatar
lain committed
421 422 423
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
424 425
  end

426 427 428 429 430 431 432
  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
433
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
    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
449
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
450 451 452
    end
  end

Roger Braun's avatar
Roger Braun committed
453 454
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
455
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
456
    targets = Repo.all(q)
href's avatar
href committed
457 458 459 460

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

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

466
  def update_media(%{assigns: %{user: user}} = conn, data) do
467
    with %Object{} = object <- Repo.get(Object, data["id"]),
468
         true <- Object.authorize_mutation(object, user),
469 470 471 472
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

473 474 475 476
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
477

478
      attachment_data = Map.put(new_data, "id", object.id)
href's avatar
href committed
479 480 481 482

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

486 487 488 489 490 491 492
  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
493 494 495 496

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

500
  def favourited_by(conn, %{"id" => id}) do
Thog's avatar
Thog committed
501
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
502
      q = from(u in User, where: u.ap_id in ^likes)
503
      users = Repo.all(q)
href's avatar
href committed
504 505 506 507

      conn
      |> put_view(AccountView)
      |> render(AccountView, "accounts.json", %{users: users, as: :user})
508 509 510 511 512 513 514
    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
515
      q = from(u in User, where: u.ap_id in ^announces)
516
      users = Repo.all(q)
href's avatar
href committed
517 518 519 520

      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
521 522 523 524 525
    else
      _ -> json(conn, [])
    end
  end

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

lain's avatar
lain committed
529 530 531
    params =
      params
      |> Map.put("type", "Create")
532
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
533
      |> Map.put("blocking_user", user)
feld's avatar
feld committed
534
      |> Map.put("tag", String.downcase(params["tag"]))
Roger Braun's avatar
Roger Braun committed
535

lain's avatar
lain committed
536 537 538
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
539 540

    conn
541
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
href's avatar
href committed
542 543
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
544 545
  end

546
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
547 548
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
549 550 551 552 553 554 555
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

href's avatar
href committed
556 557 558
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
559 560 561
    end
  end

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

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

578 579
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
href's avatar
href committed
580 581 582
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: follow_requests, as: :user})
583 584 585
    end
  end

kaniini's avatar
kaniini committed
586 587
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
588
         {:ok, follower} <- User.maybe_follow(follower, followed),
kaniini's avatar
kaniini committed
589
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
kaniini's avatar
kaniini committed
590
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
kaniini's avatar
kaniini committed
591 592
         {:ok, _activity} <-
           ActivityPub.accept(%{
kaniini's avatar
kaniini committed
593
             to: [follower.ap_id],
kaniini's avatar
kaniini committed
594 595 596 597
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
href's avatar
href committed
598 599 600
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
601 602 603 604 605 606 607 608
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

kaniini's avatar
kaniini committed
609 610 611 612 613 614 615 616 617 618 619
  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
620 621 622
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
623 624 625 626 627 628 629
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
630

eal's avatar
eal committed
631 632
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
633
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
634
         {:ok, _activity} <- ActivityPub.follow(follower, followed),
635
         {:ok, follower, followed} <-
href's avatar
href committed
636 637 638 639 640
           User.wait_and_refresh(
             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
             follower,
             followed
           ) do
href's avatar
href committed
641 642 643
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
644
    else
Thog's avatar
Thog committed
645
      {:error, message} ->
eal's avatar
eal committed
646 647
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
648
        |> send_resp(403, Jason.encode!(%{"error" => message}))
649 650 651
    end
  end

eal's avatar
eal committed
652
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
653
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
654
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
Thog's avatar
Thog committed
655
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
href's avatar
href committed
656 657 658
      conn
      |> put_view(AccountView)
      |> render("account.json", %{user: followed, for: follower})
eal's avatar
eal committed
659
    else
Thog's avatar
Thog committed
660
      {:error, message} ->
eal's avatar
eal committed
661 662
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
663
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
664 665 666
    end
  end

667 668
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
669 670
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
href's avatar
href committed
671 672 673
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
674 675 676
    end
  end

lain's avatar
lain committed
677 678
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
679 680
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
href's avatar
href committed
681 682 683
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
684
    else
Thog's avatar
Thog committed
685
      {:error, message} ->
lain's avatar
lain committed
686 687
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
688
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
689 690 691 692 693
    end
  end

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

lain's avatar
lain committed
707
  def blocks(%{assigns: %{user: user}} = conn, _) do
708 709
    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
710 711 712 713
      json(conn, res)
    end
  end

eal's avatar
eal committed
714
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
lain's avatar
lain committed
715
    json(conn, info.domain_blocks || [])
eal's avatar
eal committed
716 717 718 719 720 721 722 723 724 725 726 727
  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

728
  def status_search(query) do
729 730
    fetched =
      if Regex.match?(~r/https?:/, query) do
lain's avatar
lain committed
731 732
        with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do
          [Activity.get_create_activity_by_object_ap_id(object.data["id"])]
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752
        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]
      )

753 754 755 756 757 758 759
    Repo.all(q) ++ fetched
  end

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

    statuses = status_search(query)
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779

    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

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

783
    statuses = status_search(query)
lain's avatar
lain committed
784 785 786 787 788 789

    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
790 791 792

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
793 794
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
795
      "hashtags" => tags
lain's avatar
lain committed
796 797 798 799 800
    }

    json(conn, res)
  end

lain's avatar
lain committed
801 802
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")
803 804 805 806 807 808

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

    json(conn, res)
  end

Thog's avatar
Thog committed
809
  def favourites(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
810 811 812 813 814
    params =
      %{}
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
815

lain's avatar
lain committed
816 817 818
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
819 820

    conn
href's avatar
href committed
821 822
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
823 824
  end

eal's avatar
eal committed
825 826 827 828 829 830 831
  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
832
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
833 834 835 836 837 838 839
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e -> json(conn, "error")
    end
  end

840 841 842 843 844 845
  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
846
  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
847
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
         {: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
866
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
867
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
868
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
869 870 871 872 873 874 875 876 877
      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
878
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
879 880 881 882 883 884 885 886 887
           %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
888
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
889
         {:ok, users} = Pleroma.List.get_following(list) do
href's avatar
href committed
890 891 892
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
eal's avatar
eal committed
893 894 895 896
    end
  end

  def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
eal's avatar
eal committed
897
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
898 899 900 901 902 903 904 905 906 907
         {: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
908
    with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
909 910 911 912 913
      params =
        params
        |> Map.put("type", "Create")
        |> Map.put("blocking_user", user)

914 915 916 917 918 919
      # 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
920
      activities =
921
        ActivityPub.fetch_activities_bounded(following_to, following, params)
eal's avatar
eal committed
922 923 924
        |> Enum.reverse()

      conn