mastodon_api_controller.ex 27.9 KB
Newer Older
1 2
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
  use Pleroma.Web, :controller
3
  alias Pleroma.{Repo, Activity, User, Notification, Stats}
Roger Braun's avatar
Roger Braun committed
4
  alias Pleroma.Web
eal's avatar
eal committed
5
  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
6
  alias Pleroma.Web.ActivityPub.ActivityPub
7
  alias Pleroma.Web.{CommonAPI, OStatus}
Roger Braun's avatar
Roger Braun committed
8 9
  alias Pleroma.Web.OAuth.{Authorization, Token, App}
  alias Comeonin.Pbkdf2
10
  import Ecto.Query
11
  require Logger
12 13

  def create_app(conn, params) do
lain's avatar
lain committed
14 15
    with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
         {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
16 17 18 19 20 21 22 23 24 25
      res = %{
        id: app.id,
        client_id: app.client_id,
        client_secret: app.client_secret
      }

      json(conn, res)
    end
  end

26
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
27
    original_user = user
28

lain's avatar
lain committed
29 30 31 32 33 34
    params =
      if bio = params["note"] do
        Map.put(params, "bio", bio)
      else
        params
      end
35

lain's avatar
lain committed
36 37 38
    params =
      if name = params["display_name"] do
        Map.put(params, "name", name)
39
      else
lain's avatar
lain committed
40
        params
41 42
      end

lain's avatar
lain committed
43 44 45 46 47 48 49 50 51 52 53
    user =
      if avatar = params["avatar"] do
        with %Plug.Upload{} <- avatar,
             {:ok, object} <- ActivityPub.upload(avatar),
             change = Ecto.Changeset.change(user, %{avatar: object.data}),
             {:ok, user} = User.update_and_set_cache(change) do
          user
        else
          _e -> user
        end
      else
54
        user
lain's avatar
lain committed
55 56 57 58 59 60 61 62 63 64 65 66 67
      end

    user =
      if banner = params["header"] do
        with %Plug.Upload{} <- banner,
             {:ok, object} <- ActivityPub.upload(banner),
             new_info <- Map.put(user.info, "banner", object.data),
             change <- User.info_changeset(user, %{info: new_info}),
             {:ok, user} <- User.update_and_set_cache(change) do
          user
        else
          _e -> user
        end
68
      else
lain's avatar
lain committed
69
        user
70 71 72
      end

    with changeset <- User.update_changeset(user, params),
lain's avatar
lain committed
73 74 75 76
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
77 78

      json(conn, AccountView.render("account.json", %{user: user}))
79 80 81 82 83 84 85 86
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

87
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
Roger Braun's avatar
Roger Braun committed
88 89 90 91
    account = AccountView.render("account.json", %{user: user})
    json(conn, account)
  end

92 93 94 95 96
  def user(conn, %{"id" => id}) do
    with %User{} = user <- Repo.get(User, id) do
      account = AccountView.render("account.json", %{user: user})
      json(conn, account)
    else
lain's avatar
lain committed
97 98 99 100
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
101 102 103
    end
  end

104
  @instance Application.get_env(:pleroma, :instance)
105
  @mastodon_api_level "2.3.3"
106

Roger Braun's avatar
Roger Braun committed
107 108
  def masto_instance(conn, _params) do
    response = %{
lain's avatar
lain committed
109
      uri: Web.base_url(),
110
      title: Keyword.get(@instance, :name),
Roger Braun's avatar
Roger Braun committed
111
      description: "A Pleroma instance, an alternative fediverse server",
112
      version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
113 114
      email: Keyword.get(@instance, :email),
      urls: %{
lain's avatar
lain committed
115
        streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
116
      },
lain's avatar
lain committed
117 118
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
119
      max_toot_chars: Keyword.get(@instance, :limit)
120 121
    }

Roger Braun's avatar
Roger Braun committed
122
    json(conn, response)
123
  end
124

125
  def peers(conn, _params) do
lain's avatar
lain committed
126
    json(conn, Stats.get_peers())
127 128
  end

129 130
  defp mastodonized_emoji do
    Pleroma.Formatter.get_custom_emoji()
131
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
132 133
      url = to_string(URI.merge(Web.base_url(), relative_url))

134 135 136 137 138 139
      %{
        "shortcode" => shortcode,
        "static_url" => url,
        "url" => url
      }
    end)
140 141 142 143
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
144
    json(conn, mastodon_emoji)
145 146
  end

147
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
148 149
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
150

151 152 153
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
154 155 156 157

      {next_url, prev_url} =
        if param do
          {
158 159 160 161 162 163 164 165 166 167 168 169
            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
170 171 172
          }
        else
          {
173 174 175 176 177 178 179 180 181 182
            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
183 184 185
          }
        end

186 187 188 189 190 191 192
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

193
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
194 195 196 197 198
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
199

lain's avatar
lain committed
200 201 202
    activities =
      ActivityPub.fetch_activities([user.ap_id | user.following], params)
      |> Enum.reverse()
203 204

    conn
205
    |> add_link_headers(:home_timeline, activities)
Roger Braun's avatar
Roger Braun committed
206
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
207 208 209
  end

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

lain's avatar
lain committed
212 213 214
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
215
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
216
      |> Map.put("blocking_user", user)
217

lain's avatar
lain committed
218 219 220
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
221

222
    conn
223
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
224
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
225 226
  end

227 228 229
  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
230 231 232 233
      activities =
        if params["pinned"] == "true" do
          []
        else
234
          ActivityPub.fetch_user_activities(user, reading_user, params)
eal's avatar
eal committed
235
        end
236

237 238 239
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
      |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
240 241 242
    end
  end

243
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
244 245 246
    query =
      ActivityPub.fetch_activities_query([user.ap_id], %{"type" => "Create", visibility: "direct"})

247 248 249
    activities = Repo.all(query)

    conn
250
    |> add_link_headers(:dm_timeline, activities)
251 252 253
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

254
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
255 256
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
lain's avatar
lain committed
257
      render(conn, StatusView, "status.json", %{activity: activity, for: user})
258 259 260
    end
  end

261 262
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
263 264 265 266 267 268 269 270 271 272
         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
273
      result = %{
lain's avatar
lain committed
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
        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()
290 291 292 293 294 295
      }

      json(conn, result)
    end
  end

296
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
297 298 299 300
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
      |> Map.put("no_attachment_links", true)
301

302 303 304 305 306 307 308
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

    {:ok, activity} =
309
      Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
310

lain's avatar
lain committed
311
    render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
312
  end
Roger Braun's avatar
Roger Braun committed
313 314 315 316 317 318 319 320 321 322 323

  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
324 325

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
326
    with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do
lain's avatar
lain committed
327
      render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
328 329
    end
  end
330

331
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
332
    with {:ok, _, _, %{data: %{"id" => id}}} = CommonAPI.unrepeat(ap_id_or_id, user),
333 334
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
335 336 337
    end
  end

338
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
Roger Braun's avatar
Roger Braun committed
339 340
    with {:ok, _fav, %{data: %{"id" => id}}} = CommonAPI.favorite(ap_id_or_id, user),
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
lain's avatar
lain committed
341
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
342 343 344 345
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
346
    with {:ok, _, _, %{data: %{"id" => id}}} = CommonAPI.unfavorite(ap_id_or_id, user),
347
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
lain's avatar
lain committed
348
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
349 350
    end
  end
351

352 353
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
354 355 356 357 358 359

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

361 362 363
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
364 365
  end

366 367 368 369 370 371 372
  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
373
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
    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
389
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
390 391 392
    end
  end

393 394
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
395
    q = from(u in User, where: u.id in ^id)
396
    targets = Repo.all(q)
lain's avatar
lain committed
397
    render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
398 399
  end

400
  def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
Roger Braun's avatar
Roger Braun committed
401
    with {:ok, object} <- ActivityPub.upload(file) do
lain's avatar
lain committed
402 403 404
      data =
        object.data
        |> Map.put("id", object.id)
Roger Braun's avatar
Roger Braun committed
405

lain's avatar
lain committed
406
      render(conn, StatusView, "attachment.json", %{attachment: data})
Roger Braun's avatar
Roger Braun committed
407 408 409
    end
  end

410
  def favourited_by(conn, %{"id" => id}) do
411
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
412
      q = from(u in User, where: u.ap_id in ^likes)
413
      users = Repo.all(q)
lain's avatar
lain committed
414
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
415 416 417 418 419 420 421
    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
422
      q = from(u in User, where: u.ap_id in ^announces)
423
      users = Repo.all(q)
lain's avatar
lain committed
424
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
425 426 427 428 429
    else
      _ -> json(conn, [])
    end
  end

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

lain's avatar
lain committed
433 434 435
    params =
      params
      |> Map.put("type", "Create")
436
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
437
      |> Map.put("blocking_user", user)
Roger Braun's avatar
Roger Braun committed
438

lain's avatar
lain committed
439 440 441
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
442 443

    conn
444
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
Roger Braun's avatar
Roger Braun committed
445 446 447
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

448 449 450 451
  # TODO: Pagination
  def followers(conn, %{"id" => id}) do
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
lain's avatar
lain committed
452
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
453 454 455 456 457 458
    end
  end

  def following(conn, %{"id" => id}) do
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
lain's avatar
lain committed
459
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
460 461 462
    end
  end

eal's avatar
eal committed
463 464
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
465
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
466
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
lain's avatar
lain committed
467
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
468
    else
469
      {:error, message} ->
eal's avatar
eal committed
470 471
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
472
        |> send_resp(403, Jason.encode!(%{"error" => message}))
473 474 475
    end
  end

eal's avatar
eal committed
476
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
477
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
478
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
479
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
lain's avatar
lain committed
480
      render(conn, AccountView, "account.json", %{user: followed})
eal's avatar
eal committed
481
    else
482
      {:error, message} ->
eal's avatar
eal committed
483 484
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
485
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
486 487 488
    end
  end

489 490
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
491 492
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
lain's avatar
lain committed
493
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
494 495 496
    end
  end

Roger Braun's avatar
Roger Braun committed
497 498
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
499 500
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
lain's avatar
lain committed
501
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
Roger Braun's avatar
Roger Braun committed
502
    else
503
      {:error, message} ->
Roger Braun's avatar
Roger Braun committed
504 505
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
506
        |> send_resp(403, Jason.encode!(%{"error" => message}))
Roger Braun's avatar
Roger Braun committed
507 508 509 510 511
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
512 513
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
lain's avatar
lain committed
514
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
Roger Braun's avatar
Roger Braun committed
515
    else
516
      {:error, message} ->
Roger Braun's avatar
Roger Braun committed
517 518
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
519
        |> send_resp(403, Jason.encode!(%{"error" => message}))
Roger Braun's avatar
Roger Braun committed
520 521 522
    end
  end

523 524 525
  # TODO: Use proper query
  def blocks(%{assigns: %{user: user}} = conn, _) do
    with blocked_users <- user.info["blocks"] || [],
lain's avatar
lain committed
526
         accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
527 528 529 530 531
      res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
      json(conn, res)
    end
  end

532
  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
533
    accounts = User.search(query, params["resolve"] == "true")
Roger Braun's avatar
Roger Braun committed
534

lain's avatar
lain committed
535 536 537 538
    fetched =
      if Regex.match?(~r/https?:/, query) do
        with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
          activities
539 540 541 542
          |> Enum.filter(fn
            %{data: %{"type" => "Create"}} -> true
            _ -> false
          end)
lain's avatar
lain committed
543 544 545 546 547 548 549 550 551
        else
          _e -> []
        end
      end || []

    q =
      from(
        a in Activity,
        where: fragment("?->>'type' = 'Create'", a.data),
552
        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
lain's avatar
lain committed
553 554 555 556 557 558
        where:
          fragment(
            "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
            a.data,
            ^query
          ),
lain's avatar
lain committed
559
        limit: 20,
lain's avatar
lain committed
560
        order_by: [desc: :id]
lain's avatar
lain committed
561
      )
562 563

    statuses = Repo.all(q) ++ fetched
564 565 566 567 568 569

    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)
Roger Braun's avatar
Roger Braun committed
570 571 572

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
573 574
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
575
      "hashtags" => tags
Roger Braun's avatar
Roger Braun committed
576 577 578 579 580
    }

    json(conn, res)
  end

581 582
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")
583 584 585 586 587 588

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

    json(conn, res)
  end

589
  def favourites(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
590 591 592 593 594
    params =
      %{}
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
595

lain's avatar
lain committed
596 597 598
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
599 600 601 602 603

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

eal's avatar
eal committed
604 605 606 607 608 609 610
  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
611
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
612 613 614 615 616 617 618 619
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e -> json(conn, "error")
    end
  end

  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
620
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
         {: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
639
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
640
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
641
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
642 643 644 645 646 647 648 649 650
      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
651
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
652 653 654 655 656 657 658 659 660
           %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
661
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
662 663 664 665 666 667
         {:ok, users} = Pleroma.List.get_following(list) do
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
    end
  end

  def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
eal's avatar
eal committed
668
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
669 670 671 672 673 674 675 676 677 678
         {: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
eal's avatar
eal committed
679
    with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
      params =
        params
        |> Map.put("type", "Create")
        |> Map.put("blocking_user", user)

      # adding title is a hack to not make empty lists function like a public timeline
      activities =
        ActivityPub.fetch_activities([title | following], params)
        |> Enum.reverse()

      conn
      |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Error."})
    end
  end

Roger Braun's avatar
Roger Braun committed
700
  def index(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
701 702 703
    token =
      conn
      |> get_session(:oauth_token)
Roger Braun's avatar
Roger Braun committed
704 705

    if user && token do
706
      mastodon_emoji = mastodonized_emoji()
Roger Braun's avatar
Roger Braun committed
707
      accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
lain's avatar
lain committed
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723

      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,
            reduce_motion: false
Roger Braun's avatar
Roger Braun committed
724
          },
lain's avatar
lain committed
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
          compose: %{
            me: "#{user.id}",
            default_privacy: "public",
            default_sensitive: false
          },
          media_attachments: %{
            accept_content_types: [
              ".jpg",
              ".jpeg",
              ".png",
              ".gif",
              ".webm",
              ".mp4",
              ".m4v",
              "image\/jpeg",
              "image\/png",
              "image\/gif",
              "video\/webm",
              "video\/mp4"
            ]
          },
lain's avatar
lain committed
746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775
          settings:
            Map.get(user.info, "settings") ||
              %{
                onboarded: true,
                home: %{
                  shows: %{
                    reblog: true,
                    reply: true
                  }
                },
                notifications: %{
                  alerts: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  },
                  shows: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  },
                  sounds: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  }
                }
lain's avatar
lain committed
776 777 778 779 780 781 782 783
              },
          push_subscription: nil,
          accounts: accounts,
          custom_emojis: mastodon_emoji,
          char_limit: Keyword.get(@instance, :limit)
        }
        |> Jason.encode!()

Roger Braun's avatar
Roger Braun committed
784 785 786 787 788 789 790 791 792
      conn
      |> put_layout(false)
      |> render(MastodonView, "index.html", %{initial_state: initial_state})
    else
      conn
      |> redirect(to: "/web/login")
    end
  end

793 794 795 796 797 798
  def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
    with new_info <- Map.put(user.info, "settings", settings),
         change <- User.info_changeset(user, %{info: new_info}),
         {:ok, _user} <- User.update_and_set_cache(change) do
      conn
      |> json(%{})
lain's avatar
lain committed
799 800
    else
      e ->
801 802 803 804 805
        conn
        |> json(%{error: inspect(e)})
    end
  end

806
  def login(conn, _) do
Roger Braun's avatar
Roger Braun committed
807
    conn
808
    |> render(MastodonView, "login.html", %{error: false})
Roger Braun's avatar
Roger Braun committed
809 810 811 812 813 814 815
  end

  defp get_or_make_app() do
    with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
      {:ok, app}
    else
      _e ->
lain's avatar
lain committed
816 817 818 819 820 821 822
        cs =
          App.register_changeset(%App{}, %{
            client_name: "Mastodon-Local",
            redirect_uris: ".",
            scopes: "read,write,follow"
          })

Roger Braun's avatar
Roger Braun committed
823 824 825 826
        Repo.insert(cs)
    end
  end

lain's avatar
lain committed
827
  def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
828
    with %User{} = user <- User.get_by_nickname_or_email(name),
Roger Braun's avatar
Roger Braun committed
829 830 831 832 833 834
         true <- Pbkdf2.checkpw(password, user.password_hash),
         {:ok, app} <- get_or_make_app(),
         {:ok, auth} <- Authorization.create_authorization(app, user),
         {:ok, token} <- Token.exchange_token(app, auth) do
      conn
      |> put_session(:oauth_token, token.token)
835
      |> redirect(to: "/web/getting-started")
836 837 838 839
    else
      _e ->
        conn
        |> render(MastodonView, "login.html", %{error: "Wrong username or password"})
Roger Braun's avatar
Roger Braun committed
840 841 842
    end
  end

Roger Braun's avatar
Roger Braun committed
843 844 845 846 847 848
  def logout(conn, _) do
    conn
    |> clear_session
    |> redirect(to: "/")
  end

849 850
  def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    Logger.debug("Unimplemented, returning unmodified relationship")
lain's avatar
lain committed
851

852
    with %User{} = target <- Repo.get(User, id) do
lain's avatar
lain committed
853
      render(conn, AccountView, "relationship.json", %{user: user, target: target})
854 855 856
    end
  end

857 858 859 860
  def empty_array(conn, _) do
    Logger.debug("Unimplemented, returning an empty array")
    json(conn, [])
  end
861

862 863 864 865 866
  def empty_object(conn, _) do
    Logger.debug("Unimplemented, returning an empty object")
    json(conn, %{})
  end

867
  def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
868
    actor = User.get_cached_by_ap_id(activity.data["actor"])
lain's avatar
lain committed
869 870 871 872 873

    created_at =
      NaiveDateTime.to_iso8601(created_at)
      |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)

874 875
    case activity.data["type"] do
      "Create" ->
lain's avatar
lain committed
876 877 878 879 880 881 882 883
        %{
          id: id,
          type: "mention",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor}),
          status: StatusView.render("status.json", %{activity: activity, for: user})
        }

884 885
      "Like" ->
        liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
lain's avatar
lain committed
886 887 888 889 890 891 892 893 894

        %{
          id: id,
          type: "favourite",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor}),
          status: StatusView.render("status.json", %{activity: liked_activity, for: user})
        }

895 896
      "Announce" ->
        announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
lain's avatar
lain committed
897 898 899 900 901 902 903 904 905

        %{
          id: id,
          type: "reblog",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor}),
          status: StatusView.render("status.json", %{activity: announced_activity, for: user})
        }

906
      "Follow" ->
lain's avatar
lain committed
907 908 909 910 911 912 913 914 915
        %{
          id: id,
          type: "follow",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor})
        }

      _ ->
        nil
916 917
    end
  end
918
end