mastodon_api_controller.ex 43.5 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
cleanup  
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
cleanup  
minibikini committed
32

Roger Braun's avatar
Roger Braun committed
33
  import Ecto.Query
Thog's avatar
Thog committed
34
  require Logger
35

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

39 40
  action_fallback(:errors)

41
  def create_app(conn, params) do
42 43 44
    with cs <- App.register_changeset(%App{}, params),
         false <- cs.changes[:client_name] == @local_mastodon_name,
         {:ok, app} <- Repo.insert(cs) do
45
      res = %{
46
        id: app.id |> to_string,
47
        name: app.client_name,
48
        client_id: app.client_id,
49
        client_secret: app.client_secret,
50
        redirect_uri: app.redirect_uris,
51
        website: app.website
52 53 54 55 56 57
      }

      json(conn, res)
    end
  end

lain's avatar
lain committed
58 59 60 61 62 63 64 65 66 67 68
  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
69
      end
lain's avatar
lain committed
70 71 72 73
    else
      map
    end
  end
74

lain's avatar
lain committed
75 76
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
77

lain's avatar
lain committed
78 79 80
    user_params =
      %{}
      |> add_if_present(params, "display_name", :name)
Maxim Filippov's avatar
Maxim Filippov committed
81
      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
lain's avatar
lain committed
82 83 84 85
      |> 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
86
        else
lain's avatar
lain committed
87
          _ -> :error
lain's avatar
lain committed
88
        end
lain's avatar
lain committed
89
      end)
lain's avatar
lain committed
90

lain's avatar
lain committed
91 92 93 94 95 96 97
    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
98
        else
lain's avatar
lain committed
99
          _ -> :error
lain's avatar
lain committed
100
        end
lain's avatar
lain committed
101
      end)
102

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

lain's avatar
lain committed
105 106
    with changeset <- User.update_changeset(user, user_params),
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
107 108 109 110
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
111

112
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
113 114 115 116 117 118 119 120
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
121
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
122
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
123 124 125
    json(conn, account)
  end

126
  def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
127 128
    with %User{} = user <- Repo.get(User, id),
         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
129
      account = AccountView.render("account.json", %{user: user, for: for_user})
Roger Braun's avatar
Roger Braun committed
130 131
      json(conn, account)
    else
lain's avatar
lain committed
132 133 134 135
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
Roger Braun's avatar
Roger Braun committed
136 137 138
    end
  end

139
  @mastodon_api_level "2.5.0"
lain's avatar
lain committed
140

lain's avatar
lain committed
141
  def masto_instance(conn, _params) do
Haelwenn's avatar
Haelwenn committed
142
    instance = Config.get(:instance)
href's avatar
href committed
143

lain's avatar
lain committed
144
    response = %{
lain's avatar
lain committed
145
      uri: Web.base_url(),
href's avatar
href committed
146 147
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
href's avatar
href committed
148
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
href's avatar
href committed
149
      email: Keyword.get(instance, :email),
lain's avatar
lain committed
150
      urls: %{
151
        streaming_api: Pleroma.Web.Endpoint.websocket_url()
lain's avatar
lain committed
152
      },
lain's avatar
lain committed
153 154
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
href's avatar
href committed
155
      max_toot_chars: Keyword.get(instance, :limit)
156 157
    }

lain's avatar
lain committed
158
    json(conn, response)
159
  end
lain's avatar
lain committed
160

161
  def peers(conn, _params) do
lain's avatar
lain committed
162
    json(conn, Stats.get_peers())
163 164
  end

165
  defp mastodonized_emoji do
href's avatar
href committed
166
    Pleroma.Emoji.get_all()
167
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
168 169
      url = to_string(URI.merge(Web.base_url(), relative_url))

170 171 172
      %{
        "shortcode" => shortcode,
        "static_url" => url,
173
        "visible_in_picker" => true,
174 175 176
        "url" => url
      }
    end)
177 178 179 180
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
181
    json(conn, mastodon_emoji)
182 183
  end

184
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
185 186
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
187

188 189 190
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
191 192 193 194

      {next_url, prev_url} =
        if param do
          {
195 196 197 198 199 200 201 202 203 204 205 206
            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
207 208 209
          }
        else
          {
210 211 212 213 214 215 216 217 218 219
            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
220 221 222
          }
        end

223 224 225 226 227 228 229
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
230
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
231 232 233 234 235
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
lain's avatar
lain committed
236

lain's avatar
lain committed
237
    activities =
Haelwenn's avatar
Haelwenn committed
238 239
      [user.ap_id | user.following]
      |> ActivityPub.fetch_activities(params)
240
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
241
      |> Enum.reverse()
242 243

    conn
lain's avatar
lain committed
244
    |> add_link_headers(:home_timeline, activities)
href's avatar
href committed
245 246
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
247 248 249
  end

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

Haelwenn's avatar
Haelwenn committed
252
    activities =
lain's avatar
lain committed
253 254
      params
      |> Map.put("type", ["Create", "Announce"])
255
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
256
      |> Map.put("blocking_user", user)
Haelwenn's avatar
Haelwenn committed
257
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
258
      |> Enum.reverse()
lain's avatar
lain committed
259

lain's avatar
lain committed
260
    conn
261
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
href's avatar
href committed
262 263
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
264 265
  end

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

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

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

288 289 290
    activities = Repo.all(query)

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

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

lain's avatar
lain committed
305 306
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
307 308 309 310 311 312 313 314 315 316
         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
317
      result = %{
lain's avatar
lain committed
318 319 320 321 322 323 324 325
        ancestors:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[true] || [],
            as: :activity
          )
          |> Enum.reverse(),
Haelwenn's avatar
Haelwenn committed
326
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
327 328 329 330 331 332 333 334
        descendants:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[false] || [],
            as: :activity
          )
          |> Enum.reverse()
Haelwenn's avatar
Haelwenn committed
335
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
336 337 338 339 340 341
      }

      json(conn, result)
    end
  end

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

lain's avatar
lain committed
356 357 358 359 360 361 362
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

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

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

  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
380 381

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

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

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

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

minibikini's avatar
minibikini committed
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
  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

Haelwenn's avatar
Haelwenn committed
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
  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})
456 457 458 459
    end
  end

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

462
    with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
463 464 465
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
466 467 468 469 470
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
471 472 473 474
    end
  end

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

477
    with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
478 479 480
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
Haelwenn's avatar
Haelwenn committed
481 482 483
    end
  end

484 485
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
486 487

    result =
Haelwenn's avatar
Haelwenn committed
488 489
      notifications
      |> Enum.map(fn x -> render_notification(user, x) end)
lain's avatar
lain committed
490
      |> Enum.filter(& &1)
491

lain's avatar
lain committed
492 493 494
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
495 496
  end

497 498 499 500 501 502 503
  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
504
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
    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
520
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
521 522 523
    end
  end

Roger Braun's avatar
Roger Braun committed
524 525
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
526
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
527
    targets = Repo.all(q)
href's avatar
href committed
528 529 530 531

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

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

537
  def update_media(%{assigns: %{user: user}} = conn, data) do
538
    with %Object{} = object <- Repo.get(Object, data["id"]),
539
         true <- Object.authorize_mutation(object, user),
540 541 542 543
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

544 545 546 547
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
548

549
      attachment_data = Map.put(new_data, "id", object.id)
href's avatar
href committed
550 551 552 553

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

557 558
  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
    with {:ok, object} <-
kaniini's avatar
kaniini committed
559 560
           ActivityPub.upload(
             file,
561 562 563 564
             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
565 566 567 568

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
569 570 571
    end
  end

572
  def favourited_by(conn, %{"id" => id}) do
Thog's avatar
Thog committed
573
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
574
      q = from(u in User, where: u.ap_id in ^likes)
575
      users = Repo.all(q)
href's avatar
href committed
576 577 578 579

      conn
      |> put_view(AccountView)
      |> render(AccountView, "accounts.json", %{users: users, as: :user})
580 581 582 583 584 585 586
    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
587
      q = from(u in User, where: u.ap_id in ^announces)
588
      users = Repo.all(q)
href's avatar
href committed
589 590 591 592

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

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

Haelwenn's avatar
Haelwenn committed
601
    tags =
602 603
      [params["tag"], params["any"]]
      |> List.flatten()
Haelwenn's avatar
Haelwenn committed
604 605 606 607
      |> Enum.uniq()
      |> Enum.filter(& &1)
      |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
608 609 610 611 612
    tag_all =
      params["all"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
613 614 615 616 617
    tag_reject =
      params["none"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
618
    activities =
lain's avatar
lain committed
619 620
      params
      |> Map.put("type", "Create")
621
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
622
      |> Map.put("blocking_user", user)
Haelwenn's avatar
Haelwenn committed
623
      |> Map.put("tag", tags)
Haelwenn's avatar
Haelwenn committed
624
      |> Map.put("tag_all", tag_all)
Haelwenn's avatar
Haelwenn committed
625
      |> Map.put("tag_reject", tag_reject)
Haelwenn's avatar
Haelwenn committed
626
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
627
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
628 629

    conn
630
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
href's avatar
href committed
631 632
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
633 634
  end

635
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
636 637
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
638 639 640
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
641
          user.info.hide_followers -> []
642 643 644
          true -> followers
        end

href's avatar
href committed
645 646 647
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
648 649 650
    end
  end

651
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
652 653
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
654 655 656
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
657
          user.info.hide_follows -> []
658 659 660
          true -> followers
        end

href's avatar
href committed
661 662 663
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
664 665 666
    end
  end

667 668
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
href's avatar
href committed
669 670 671
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: follow_requests, as: :user})
672 673 674
    end
  end

kaniini's avatar
kaniini committed
675 676
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
677
         {:ok, follower} <- User.maybe_follow(follower, followed),
kaniini's avatar
kaniini committed
678
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
kaniini's avatar
kaniini committed
679
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
kaniini's avatar
kaniini committed
680 681
         {:ok, _activity} <-
           ActivityPub.accept(%{
kaniini's avatar
kaniini committed
682
             to: [follower.ap_id],
kaniini's avatar
kaniini committed
683 684 685 686
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
href's avatar
href committed
687 688 689
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
690 691 692 693 694 695 696 697
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

kaniini's avatar
kaniini committed
698 699 700 701 702 703 704 705 706 707 708
  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
709 710 711
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
712 713 714 715 716 717 718
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
719

eal's avatar
eal committed
720 721
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
722
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
723
         {:ok, _activity} <- ActivityPub.follow(follower, followed),
724
         {:ok, follower, followed} <-
href's avatar
href committed
725
           User.wait_and_refresh(
Haelwenn's avatar
Haelwenn committed
726
             Config.get([:activitypub, :follow_handshake_timeout]),
href's avatar
href committed
727 728 729
             follower,
             followed
           ) do
href's avatar
href committed
730 731 732
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
733
    else
Thog's avatar
Thog committed
734
      {:error, message} ->
eal's avatar
eal committed
735 736
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
737
        |> send_resp(403, Jason.encode!(%{"error" => message}))
738 739 740
    end
  end

eal's avatar
eal committed
741
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
742
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
743
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
Thog's avatar
Thog committed
744
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
href's avatar
href committed
745 746 747
      conn
      |> put_view(AccountView)
      |> render("account.json", %{user: followed, for: follower})
eal's avatar
eal committed
748
    else
Thog's avatar
Thog committed
749
      {:error, message} ->
eal's avatar
eal committed
750 751
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
752
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
753 754 755
    end
  end

756 757
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
758 759
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
href's avatar
href committed
760 761 762
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
763 764 765
    end
  end

lain's avatar
lain committed
766 767
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
768 769
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
href's avatar
href committed
770 771 772
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
773
    else
Thog's avatar
Thog committed
774
      {:error, message} ->
lain's avatar
lain committed
775 776
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
777
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
778 779 780 781 782
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
783 784
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
href's avatar
href committed
785 786 787
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
788
    else
Thog's avatar
Thog committed
789
      {:error, message} ->
lain's avatar
lain committed
790 791
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
792
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
793 794 795
    end
  end

lain's avatar
lain committed
796
  def blocks(%{assigns: %{user: user}} = conn, _) do
797 798
    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
799 800 801 802
      json(conn, res)
    end
  end

eal's avatar
eal committed
803
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
lain's avatar
lain committed
804
    json(conn, info.domain_blocks || [])
eal's avatar
eal committed
805 806 807 808 809 810 811 812 813 814 815 816
  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

817
  def status_search(user, query) do
818 819
    fetched =
      if Regex.match?(~r/https?:/, query) do
820
        with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
821
             %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
822 823
             true <- ActivityPub.visible_for_user?(activity, user) do
          [activity]
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843
        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]
      )

844 845 846 847
    Repo.all(q) ++ fetched
  end

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

850
    statuses = status_search(user, query)
851 852 853 854

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

    tags =
Haelwenn's avatar
Haelwenn committed
855 856
      query
      |> String.split()
857 858 859 860 861 862 863 864 865 866 867 868 869 870 871
      |> 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

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

875
    statuses = status_search(user, query)
lain's avatar
lain committed
876 877

    tags =
Haelwenn's avatar
Haelwenn committed
878 879
      query
      |> String.split()
lain's avatar
lain committed
880 881 882
      |> 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
883 884 885

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
886 887
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
888
      "hashtags" => tags
lain's avatar
lain committed
889 890 891 892 893
    }

    json(conn, res)
  end

lain's avatar
lain committed
894
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
895
    accounts = User.search(query, params["resolve"] == "true", user)
896 897 898 899 900 901

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

    json(conn, res)
  end

902
  def favourites(%{assigns: %{user: user}} = conn, params) do
Haelwenn's avatar
Haelwenn committed
903
    activities =
904
      params
lain's avatar
lain committed
905 906 907
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
Haelwenn's avatar
Haelwenn committed
908
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
909
      |> Enum.reverse()
910 911

    conn
912
    |> add_link_headers(:favourites, activities)
href's avatar
href committed
913 914
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
915 916
  end

Haelwenn's avatar
Haelwenn committed
917 918 919 920 921 922 923 924 925 926 927 928 929
  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
930 931 932 933 934 935 936
  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
937
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
938 939 940
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
941 942 943 944
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Record not found"})
eal's avatar
eal committed
945 946 947
    end
  end

948 949 950 951 952 953
  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
954
  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
955
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
956 957 958 959 960 961 962 963 964