mastodon_api_controller.ex 33.3 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
William Pitcock's avatar
William Pitcock committed
7
  alias Pleroma.Web.ActivityPub.Utils
8
  alias Pleroma.Web.{CommonAPI, OStatus}
Roger Braun's avatar
Roger Braun committed
9 10
  alias Pleroma.Web.OAuth.{Authorization, Token, App}
  alias Comeonin.Pbkdf2
11
  import Ecto.Query
12
  require Logger
13

14 15
  @httpoison Application.get_env(:pleroma, :httpoison)

16 17
  action_fallback(:errors)

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

      json(conn, res)
    end
  end

31
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
32
    original_user = user
33

lain's avatar
lain committed
34 35 36 37 38 39
    params =
      if bio = params["note"] do
        Map.put(params, "bio", bio)
      else
        params
      end
40

lain's avatar
lain committed
41 42 43
    params =
      if name = params["display_name"] do
        Map.put(params, "name", name)
44
      else
lain's avatar
lain committed
45
        params
46 47
      end

lain's avatar
lain committed
48 49 50 51 52 53 54 55 56 57 58
    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
59
        user
lain's avatar
lain committed
60 61 62 63 64 65 66 67 68 69 70 71 72
      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
73
      else
lain's avatar
lain committed
74
        user
75 76
      end

77 78 79 80 81 82 83 84 85 86 87 88 89 90
    user =
      if locked = params["locked"] do
        with locked <- locked == "true",
             new_info <- Map.put(user.info, "locked", locked),
             change <- User.info_changeset(user, %{info: new_info}),
             {:ok, user} <- User.update_and_set_cache(change) do
          user
        else
          _e -> user
        end
      else
        user
      end

91
    with changeset <- User.update_changeset(user, params),
lain's avatar
lain committed
92 93 94 95
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
96 97

      json(conn, AccountView.render("account.json", %{user: user}))
98 99 100 101 102 103 104 105
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

106
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
Roger Braun's avatar
Roger Braun committed
107 108 109 110
    account = AccountView.render("account.json", %{user: user})
    json(conn, account)
  end

111 112 113 114 115
  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
116 117 118 119
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
120 121 122
    end
  end

123
  @instance Application.get_env(:pleroma, :instance)
124
  @mastodon_api_level "2.3.3"
125

Roger Braun's avatar
Roger Braun committed
126 127
  def masto_instance(conn, _params) do
    response = %{
lain's avatar
lain committed
128
      uri: Web.base_url(),
129
      title: Keyword.get(@instance, :name),
Roger Braun's avatar
Roger Braun committed
130
      description: "A Pleroma instance, an alternative fediverse server",
131
      version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
132 133
      email: Keyword.get(@instance, :email),
      urls: %{
lain's avatar
lain committed
134
        streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
135
      },
lain's avatar
lain committed
136 137
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
138
      max_toot_chars: Keyword.get(@instance, :limit)
139 140
    }

Roger Braun's avatar
Roger Braun committed
141
    json(conn, response)
142
  end
143

144
  def peers(conn, _params) do
lain's avatar
lain committed
145
    json(conn, Stats.get_peers())
146 147
  end

148 149
  defp mastodonized_emoji do
    Pleroma.Formatter.get_custom_emoji()
150
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
151 152
      url = to_string(URI.merge(Web.base_url(), relative_url))

153 154 155
      %{
        "shortcode" => shortcode,
        "static_url" => url,
156
        "visible_in_picker" => true,
157 158 159
        "url" => url
      }
    end)
160 161 162 163
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
164
    json(conn, mastodon_emoji)
165 166
  end

167
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
168 169
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
170

171 172 173
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
174 175 176 177

      {next_url, prev_url} =
        if param do
          {
178 179 180 181 182 183 184 185 186 187 188 189
            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
190 191 192
          }
        else
          {
193 194 195 196 197 198 199 200 201 202
            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
203 204 205
          }
        end

206 207 208 209 210 211 212
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

213
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
214 215 216 217 218
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
219

lain's avatar
lain committed
220 221 222
    activities =
      ActivityPub.fetch_activities([user.ap_id | user.following], params)
      |> Enum.reverse()
223 224

    conn
225
    |> add_link_headers(:home_timeline, activities)
Roger Braun's avatar
Roger Braun committed
226
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
227 228 229
  end

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

lain's avatar
lain committed
232 233 234
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
235
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
236
      |> Map.put("blocking_user", user)
237

lain's avatar
lain committed
238 239 240
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
241

242
    conn
243
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
244
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
245 246
  end

247 248 249
  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
250 251 252 253
      activities =
        if params["pinned"] == "true" do
          []
        else
254
          ActivityPub.fetch_user_activities(user, reading_user, params)
eal's avatar
eal committed
255
        end
256

257 258
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
259 260 261 262 263
      |> render(StatusView, "index.json", %{
        activities: activities,
        for: reading_user,
        as: :activity
      })
264 265 266
    end
  end

267
  def dm_timeline(%{assigns: %{user: user}} = conn, _params) do
268 269 270
    query =
      ActivityPub.fetch_activities_query([user.ap_id], %{"type" => "Create", visibility: "direct"})

271 272 273
    activities = Repo.all(query)

    conn
274
    |> add_link_headers(:dm_timeline, activities)
275 276 277
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

278
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
279 280
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
lain's avatar
lain committed
281
      render(conn, StatusView, "status.json", %{activity: activity, for: user})
282 283 284
    end
  end

285 286
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
287 288 289 290 291 292 293 294 295 296
         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
297
      result = %{
lain's avatar
lain committed
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
        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()
314 315 316 317 318 319
      }

      json(conn, result)
    end
  end

320 321 322 323 324 325 326 327 328
  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

329
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
330 331 332 333
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
      |> Map.put("no_attachment_links", true)
334

335 336 337 338 339 340 341
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

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

lain's avatar
lain committed
344
    render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
345
  end
Roger Braun's avatar
Roger Braun committed
346 347 348 349 350 351 352 353 354 355 356

  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
357 358

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
359
    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
lain's avatar
lain committed
360
      render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
361 362
    end
  end
363

364
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
365
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
366 367
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
368 369 370
    end
  end

371
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
372
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
Roger Braun's avatar
Roger Braun committed
373
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
lain's avatar
lain committed
374
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
375 376 377 378
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
379
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
380
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
lain's avatar
lain committed
381
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
382 383
    end
  end
384

385 386
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
387 388 389 390 391 392

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

394 395 396
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
397 398
  end

399 400 401 402 403 404 405
  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
406
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
    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
422
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
423 424 425
    end
  end

426 427
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
428
    q = from(u in User, where: u.id in ^id)
429
    targets = Repo.all(q)
lain's avatar
lain committed
430
    render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
431 432
  end

433
  def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
Roger Braun's avatar
Roger Braun committed
434
    with {:ok, object} <- ActivityPub.upload(file) do
lain's avatar
lain committed
435 436 437
      data =
        object.data
        |> Map.put("id", object.id)
Roger Braun's avatar
Roger Braun committed
438

lain's avatar
lain committed
439
      render(conn, StatusView, "attachment.json", %{attachment: data})
Roger Braun's avatar
Roger Braun committed
440 441 442
    end
  end

443
  def favourited_by(conn, %{"id" => id}) do
444
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
445
      q = from(u in User, where: u.ap_id in ^likes)
446
      users = Repo.all(q)
lain's avatar
lain committed
447
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
448 449 450 451 452 453 454
    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
455
      q = from(u in User, where: u.ap_id in ^announces)
456
      users = Repo.all(q)
lain's avatar
lain committed
457
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
458 459 460 461 462
    else
      _ -> json(conn, [])
    end
  end

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

lain's avatar
lain committed
466 467 468
    params =
      params
      |> Map.put("type", "Create")
469
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
470
      |> Map.put("blocking_user", user)
Roger Braun's avatar
Roger Braun committed
471

lain's avatar
lain committed
472 473 474
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
475 476

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

481 482 483 484
  # 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
485
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
486 487 488 489 490 491
    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
492
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
493 494 495
    end
  end

496 497 498 499 500 501
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
      render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
    end
  end

William Pitcock's avatar
William Pitcock committed
502 503
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
504
         {:ok, follower} <- User.maybe_follow(follower, followed),
William Pitcock's avatar
William Pitcock committed
505
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
William Pitcock's avatar
William Pitcock committed
506
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
William Pitcock's avatar
William Pitcock committed
507 508
         {:ok, _activity} <-
           ActivityPub.accept(%{
William Pitcock's avatar
William Pitcock committed
509
             to: [follower.ap_id],
William Pitcock's avatar
William Pitcock committed
510 511 512 513 514 515 516 517 518 519 520 521 522
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
      render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

William Pitcock's avatar
William Pitcock committed
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
  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
      render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
William Pitcock's avatar
William Pitcock committed
542

eal's avatar
eal committed
543 544
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
545
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
546
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
lain's avatar
lain committed
547
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
548
    else
549
      {:error, message} ->
eal's avatar
eal committed
550 551
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
552
        |> send_resp(403, Jason.encode!(%{"error" => message}))
553 554 555
    end
  end

eal's avatar
eal committed
556
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
557
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
558
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
559
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
lain's avatar
lain committed
560
      render(conn, AccountView, "account.json", %{user: followed})
eal's avatar
eal committed
561
    else
562
      {:error, message} ->
eal's avatar
eal committed
563 564
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
565
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
566 567 568
    end
  end

569 570
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
571 572
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
lain's avatar
lain committed
573
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
574 575 576
    end
  end

Roger Braun's avatar
Roger Braun committed
577 578
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
579 580
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
lain's avatar
lain committed
581
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
Roger Braun's avatar
Roger Braun committed
582
    else
583
      {:error, message} ->
Roger Braun's avatar
Roger Braun committed
584 585
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
586
        |> send_resp(403, Jason.encode!(%{"error" => message}))
Roger Braun's avatar
Roger Braun committed
587 588 589 590 591
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
592 593
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
lain's avatar
lain committed
594
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
Roger Braun's avatar
Roger Braun committed
595
    else
596
      {:error, message} ->
Roger Braun's avatar
Roger Braun committed
597 598
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
599
        |> send_resp(403, Jason.encode!(%{"error" => message}))
Roger Braun's avatar
Roger Braun committed
600 601 602
    end
  end

603 604 605
  # TODO: Use proper query
  def blocks(%{assigns: %{user: user}} = conn, _) do
    with blocked_users <- user.info["blocks"] || [],
lain's avatar
lain committed
606
         accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
607 608 609 610 611
      res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
      json(conn, res)
    end
  end

eal's avatar
eal committed
612 613 614 615 616 617 618 619 620 621 622 623 624 625
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
    json(conn, info["domain_blocks"] || [])
  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

626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677
  def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")

    fetched =
      if Regex.match?(~r/https?:/, query) do
        with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
          activities
          |> Enum.filter(fn
            %{data: %{"type" => "Create"}} -> true
            _ -> false
          end)
        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]
      )

    statuses = Repo.all(q) ++ fetched

    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

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

lain's avatar
lain committed
681 682 683 684
    fetched =
      if Regex.match?(~r/https?:/, query) do
        with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
          activities
685 686 687 688
          |> Enum.filter(fn
            %{data: %{"type" => "Create"}} -> true
            _ -> false
          end)
lain's avatar
lain committed
689 690 691 692 693 694 695 696 697
        else
          _e -> []
        end
      end || []

    q =
      from(
        a in Activity,
        where: fragment("?->>'type' = 'Create'", a.data),
698
        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
lain's avatar
lain committed
699 700 701 702 703 704
        where:
          fragment(
            "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
            a.data,
            ^query
          ),
lain's avatar
lain committed
705
        limit: 20,
lain's avatar
lain committed
706
        order_by: [desc: :id]
lain's avatar
lain committed
707
      )
708 709

    statuses = Repo.all(q) ++ fetched
710 711 712 713 714 715

    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
716 717 718

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
719 720
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
721
      "hashtags" => tags
Roger Braun's avatar
Roger Braun committed
722 723 724 725 726
    }

    json(conn, res)
  end

727 728
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")
729 730 731 732 733 734

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

    json(conn, res)
  end

735
  def favourites(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
736 737 738 739 740
    params =
      %{}
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
741

lain's avatar
lain committed
742 743 744
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
745 746 747 748 749

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

eal's avatar
eal committed
750 751 752 753 754 755 756
  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
757
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
758 759 760 761 762 763 764 765
      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
766
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784
         {: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
785
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
786
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
787
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
788 789 790 791 792 793 794 795 796
      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
797
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
798 799 800 801 802 803 804 805 806
           %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
807
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
808 809 810 811 812 813
         {: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
814
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
815 816 817 818 819 820 821 822 823 824
         {: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
825
    with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845
      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
846
  def index(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
847 848 849
    token =
      conn
      |> get_session(:oauth_token)
Roger Braun's avatar
Roger Braun committed
850 851

    if user && token do
852
      mastodon_emoji = mastodonized_emoji()
Roger Braun's avatar
Roger Braun committed
853
      accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
lain's avatar
lain committed
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868

      initial_state =
        %{
          meta: %{
            streaming_api_base_url:
              String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
            access_token: token,
            locale: "en",
            domain: Pleroma.Web.Endpoint.host(),
            admin: "1",
            me: "#{user.id}",
            unfollow_modal: false,
            boost_modal: false,
            delete_modal: true,
            auto_play_gif: false,
Haelwenn Monnier's avatar
Haelwenn Monnier committed
869
            display_sensitive_media: false,
870 871
            reduce_motion: false,
            max_toot_chars: Keyword.get(@instance, :limit)
Roger Braun's avatar
Roger Braun committed
872
          },
873 874 875
          rights: %{
            delete_others_notice: !!user.info["is_moderator"]
          },
lain's avatar
lain committed
876 877
          compose: %{
            me: "#{user.id}",
878
            default_privacy: user.info["default_scope"] || "public",
lain's avatar
lain committed
879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896
            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
897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926
          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
927 928 929 930 931 932 933 934
              },
          push_subscription: nil,
          accounts: accounts,
          custom_emojis: mastodon_emoji,
          char_limit: Keyword.get(@instance, :limit)
        }
        |> Jason.encode!()

Roger Braun's avatar
Roger Braun committed
935 936 937 938 939 940 941 942 943
      conn
      |> put_layout(false)
      |> render(MastodonView, "index.html", %{initial_state: initial_state})
    else
      conn
      |> redirect(to: "/web/login")
    end
  end

944 945 946 947 948 949
  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
950 951
    else
      e ->
952 953 954 955 956
        conn
        |> json(%{error: inspect(e)})
    end
  end

957
  def login(conn, _) do
Roger Braun's avatar
Roger Braun committed
958
    conn
959
    |> render(MastodonView, "login.html", %{error: false})
Roger Braun's avatar
Roger Braun committed
960 961 962 963 964 965 966
  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
967 968 969 970 971 972 973
        cs =
          App.register_changeset(%App{}, %{
            client_name: "Mastodon-Local",
            redirect_uris: ".",
            scopes: "read,write,follow"
          })

Roger Braun's avatar
Roger Braun committed
974 975 976 977
        Repo.insert(cs)
    end
  end

lain's avatar
lain committed
978
  def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
979
    with %User{} = user <- User.get_by_nickname_or_email(name),
Roger Braun's avatar
Roger Braun committed
980 981 982 983 984 985
         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)
986
      |> redirect(to: "/web/getting-started")
987 988 989 990
    else
      _e ->
        conn
        |> render(MastodonView, "login.html", %{error: "Wrong username or password"})
Roger Braun's avatar
Roger Braun committed
991 992 993
    end
  end

Roger Braun's avatar
Roger Braun committed
994 995 996 997 998 999
  def logout(conn, _) do
    conn
    |> clear_session
    |> redirect(to: "/")
  end

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

1003
    with %User{} = target <- Repo.get(User, id) do
lain's avatar
lain committed
1004
      render(conn, AccountView, "relationship.json", %{user: user, target: target})
1005 1006 1007
    end
  end

1008 1009 1010 1011
  def empty_array(conn, _) do
    Logger.debug("Unimplemented, returning an empty array")
    json(conn, [])
  end
1012

1013 1014 1015 1016 1017
  def empty_object(conn, _) do
    Logger.debug("Unimplemented, returning an empty object")
    json(conn, %{})
  end

1018
  def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1019
    actor = User.get_cached_by_ap_id(activity.data["actor"])
lain's avatar
lain committed
1020 1021 1022 1023 1024

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

1025 1026
    case activity.data["type"] do
      "Create" ->
lain's avatar
lain committed
1027 1028 1029 1030 1031 1032 1033 1034
        %{
          id: id,
          type: "mention",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor}),
          status: StatusView.render("status.json", %{activity: activity, for: user})
        }

1035 1036
      "Like" ->
        liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
lain's avatar
lain committed
1037 1038 1039 1040 1041 1042 1043 1044 1045

        %{
          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})
        }

1046 1047
      "Announce" ->
        announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
lain's avatar
lain committed
1048 1049 1050 1051 1052 1053 1054 1055 1056

        %{
          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})
        }

1057
      "Follow" ->
lain's avatar
lain committed
1058 1059 1060 1061 1062 1063 1064 1065 1066
        %{
          id: id,
          type: "follow",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor})
        }

      _ ->
        nil
1067 1068
    end
  end
1069 1070 1071 1072 1073 1074

  def errors(conn, _) do
    conn
    |> put_status(500)
    |> json("Something went wrong")
  end
1075

hakabahitoyo's avatar
hakabahitoyo committed
1076 1077
  @suggestions Application.get_env(:pleroma, :suggestions)

hakabahitoyo's avatar
hakabahitoyo committed
1078
  def suggestions(%{assigns: %{user: user}} = conn, _) do
1079 1080
    host = String.replace Web.base_url(), "https://", ""
    user = user.nickname
hakabahitoyo's avatar
hakabahitoyo committed
1081
    api = Keyword.get(@suggestions, :third_party_engine, "")
1082 1083 1084 1085 1086 1087 1088 1089 1090
    url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
    with {:ok, %{status_code: 200, body: body}} <-
           @httpoison.get(url),
         {:ok, data} <- Jason.decode(body) do
      conn
      |> json(data)
    else
      e -> Logger.error("Could not decode user at fetch #{url}, #{inspect(e)}")
    end
1091
  end
1092
end