mastodon_api_controller.ex 52.7 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

5
6
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
  use Pleroma.Web, :controller
7
  alias Ecto.Changeset
Haelwenn's avatar
Haelwenn committed
8
  alias Pleroma.Activity
9
  alias Pleroma.Bookmark
Haelwenn's avatar
Haelwenn committed
10
  alias Pleroma.Config
lain's avatar
lain committed
11
  alias Pleroma.Conversation.Participation
Haelwenn's avatar
Haelwenn committed
12
  alias Pleroma.Filter
13
  alias Pleroma.Formatter
Haelwenn's avatar
Haelwenn committed
14
15
  alias Pleroma.Notification
  alias Pleroma.Object
rinpatch's avatar
rinpatch committed
16
  alias Pleroma.Object.Fetcher
17
  alias Pleroma.Pagination
Haelwenn's avatar
Haelwenn committed
18
  alias Pleroma.Repo
Eugenij's avatar
Eugenij committed
19
  alias Pleroma.ScheduledActivity
Haelwenn's avatar
Haelwenn committed
20
21
  alias Pleroma.Stats
  alias Pleroma.User
lain's avatar
lain committed
22
  alias Pleroma.Web
23
24
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Visibility
lain's avatar
lain committed
25
  alias Pleroma.Web.CommonAPI
Haelwenn's avatar
Haelwenn committed
26
  alias Pleroma.Web.MastodonAPI.AccountView
27
  alias Pleroma.Web.MastodonAPI.AppView
28
  alias Pleroma.Web.MastodonAPI.ConversationView
Haelwenn's avatar
Haelwenn committed
29
30
  alias Pleroma.Web.MastodonAPI.FilterView
  alias Pleroma.Web.MastodonAPI.ListView
31
  alias Pleroma.Web.MastodonAPI.MastodonAPI
Haelwenn's avatar
Haelwenn committed
32
  alias Pleroma.Web.MastodonAPI.MastodonView
33
  alias Pleroma.Web.MastodonAPI.NotificationView
minibikini's avatar
Reports    
minibikini committed
34
  alias Pleroma.Web.MastodonAPI.ReportView
Eugenij's avatar
Eugenij committed
35
  alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36
37
  alias Pleroma.Web.MastodonAPI.StatusView
  alias Pleroma.Web.MediaProxy
Haelwenn's avatar
Haelwenn committed
38
39
  alias Pleroma.Web.OAuth.App
  alias Pleroma.Web.OAuth.Authorization
40
  alias Pleroma.Web.OAuth.Scopes
Haelwenn's avatar
Haelwenn committed
41
  alias Pleroma.Web.OAuth.Token
minibikini's avatar
cleanup    
minibikini committed
42

43
  alias Pleroma.Web.ControllerHelper
Roger Braun's avatar
Roger Braun committed
44
  import Ecto.Query
45

Thog's avatar
Thog committed
46
  require Logger
47

48
  @httpoison Application.get_env(:pleroma, :httpoison)
49
  @local_mastodon_name "Mastodon-Local"
50

51
52
  action_fallback(:errors)

53
  def create_app(conn, params) do
54
    scopes = Scopes.fetch_scopes(params, ["read"])
55
56
57
58
59

    app_attrs =
      params
      |> Map.drop(["scope", "scopes"])
      |> Map.put("scopes", scopes)
60
61

    with cs <- App.register_changeset(%App{}, app_attrs),
62
63
         false <- cs.changes[:client_name] == @local_mastodon_name,
         {:ok, app} <- Repo.insert(cs) do
64
65
66
      conn
      |> put_view(AppView)
      |> render("show.json", %{app: app})
67
68
69
    end
  end

lain's avatar
lain committed
70
71
72
73
74
75
76
77
78
79
80
  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
81
      end
lain's avatar
lain committed
82
83
84
85
    else
      map
    end
  end
86

lain's avatar
lain committed
87
88
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
89

lain's avatar
lain committed
90
91
92
    user_params =
      %{}
      |> add_if_present(params, "display_name", :name)
93
      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
lain's avatar
lain committed
94
95
96
97
      |> 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
98
        else
lain's avatar
lain committed
99
          _ -> :error
lain's avatar
lain committed
100
        end
lain's avatar
lain committed
101
      end)
lain's avatar
lain committed
102

103
104
105
106
107
108
    emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")

    user_info_emojis =
      ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
      |> Enum.dedup()

lain's avatar
lain committed
109
    info_params =
110
111
112
113
114
115
      [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
      |> Enum.reduce(%{}, fn key, acc ->
        add_if_present(acc, params, to_string(key), key, fn value ->
          {:ok, ControllerHelper.truthy_param?(value)}
        end)
      end)
116
      |> add_if_present(params, "default_scope", :default_scope)
lain's avatar
lain committed
117
118
119
120
      |> 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
121
        else
lain's avatar
lain committed
122
          _ -> :error
lain's avatar
lain committed
123
        end
lain's avatar
lain committed
124
      end)
125
      |> Map.put(:emoji, user_info_emojis)
126

127
    info_cng = User.Info.profile_update(user.info, info_params)
128

lain's avatar
lain committed
129
130
    with changeset <- User.update_changeset(user, user_params),
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
131
132
133
134
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
135

136
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
137
138
139
140
141
142
143
144
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
145
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
146
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
147
148
149
    json(conn, account)
  end

150
151
152
153
  def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
    with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
      conn
      |> put_view(AppView)
154
      |> render("short.json", %{app: app})
155
156
157
    end
  end

158
159
  def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
160
         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
161
      account = AccountView.render("account.json", %{user: user, for: for_user})
Roger Braun's avatar
Roger Braun committed
162
163
      json(conn, account)
    else
lain's avatar
lain committed
164
165
166
167
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
Roger Braun's avatar
Roger Braun committed
168
169
170
    end
  end

lain's avatar
lain committed
171
  @mastodon_api_level "2.6.5"
lain's avatar
lain committed
172

lain's avatar
lain committed
173
  def masto_instance(conn, _params) do
Haelwenn's avatar
Haelwenn committed
174
    instance = Config.get(:instance)
href's avatar
href committed
175

lain's avatar
lain committed
176
    response = %{
lain's avatar
lain committed
177
      uri: Web.base_url(),
href's avatar
href committed
178
179
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
href's avatar
href committed
180
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
href's avatar
href committed
181
      email: Keyword.get(instance, :email),
lain's avatar
lain committed
182
      urls: %{
183
        streaming_api: Pleroma.Web.Endpoint.websocket_url()
lain's avatar
lain committed
184
      },
lain's avatar
lain committed
185
186
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
187
188
189
      languages: ["en"],
      registrations: Pleroma.Config.get([:instance, :registrations_open]),
      # Extra (not present in Mastodon):
href's avatar
href committed
190
      max_toot_chars: Keyword.get(instance, :limit)
191
192
    }

lain's avatar
lain committed
193
    json(conn, response)
194
  end
lain's avatar
lain committed
195

196
  def peers(conn, _params) do
lain's avatar
lain committed
197
    json(conn, Stats.get_peers())
198
199
  end

200
  defp mastodonized_emoji do
href's avatar
href committed
201
    Pleroma.Emoji.get_all()
202
    |> Enum.map(fn {shortcode, relative_url, tags} ->
lain's avatar
lain committed
203
204
      url = to_string(URI.merge(Web.base_url(), relative_url))

205
206
207
      %{
        "shortcode" => shortcode,
        "static_url" => url,
208
        "visible_in_picker" => true,
209
        "url" => url,
210
        "tags" => tags
211
212
      }
    end)
213
214
215
216
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
217
    json(conn, mastodon_emoji)
218
219
  end

220
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
221
222
    params =
      conn.params
223
      |> Map.drop(["since_id", "max_id", "min_id"])
224
225
      |> Map.merge(params)

226
    last = List.last(activities)
lain's avatar
lain committed
227

228
    if last do
229
230
231
232
233
234
235
      max_id = last.id

      limit =
        params
        |> Map.get("limit", "20")
        |> String.to_integer()

236
237
238
239
240
241
242
243
244
245
      min_id =
        if length(activities) <= limit do
          activities
          |> List.first()
          |> Map.get(:id)
        else
          activities
          |> Enum.at(limit * -1)
          |> Map.get(:id)
        end
lain's avatar
lain committed
246
247
248
249

      {next_url, prev_url} =
        if param do
          {
250
251
252
253
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              param,
254
              Map.merge(params, %{max_id: max_id})
255
256
257
258
259
            ),
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              param,
260
              Map.merge(params, %{min_id: min_id})
261
            )
lain's avatar
lain committed
262
263
264
          }
        else
          {
265
266
267
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
268
              Map.merge(params, %{max_id: max_id})
269
270
271
272
            ),
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
273
              Map.merge(params, %{min_id: min_id})
274
            )
lain's avatar
lain committed
275
276
277
          }
        end

278
279
280
281
282
283
284
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
285
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
286
287
288
289
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
290
      |> Map.put("muting_user", user)
lain's avatar
lain committed
291
      |> Map.put("user", user)
lain's avatar
lain committed
292

lain's avatar
lain committed
293
    activities =
Haelwenn's avatar
Haelwenn committed
294
295
      [user.ap_id | user.following]
      |> ActivityPub.fetch_activities(params)
296
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
297
      |> Enum.reverse()
298
299

    conn
lain's avatar
lain committed
300
    |> add_link_headers(:home_timeline, activities)
href's avatar
href committed
301
302
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
303
304
305
  end

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

Haelwenn's avatar
Haelwenn committed
308
    activities =
lain's avatar
lain committed
309
310
      params
      |> Map.put("type", ["Create", "Announce"])
311
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
312
      |> Map.put("blocking_user", user)
313
      |> Map.put("muting_user", user)
Haelwenn's avatar
Haelwenn committed
314
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
315
      |> Enum.reverse()
lain's avatar
lain committed
316

lain's avatar
lain committed
317
    conn
318
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
href's avatar
href committed
319
320
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
321
322
  end

323
  def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
rinpatch's avatar
rinpatch committed
324
    with %User{} = user <- User.get_cached_by_id(params["id"]) do
minibikini's avatar
minibikini committed
325
      activities = ActivityPub.fetch_user_activities(user, reading_user, params)
lain's avatar
lain committed
326

327
328
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
href's avatar
href committed
329
330
      |> put_view(StatusView)
      |> render("index.json", %{
331
332
333
334
        activities: activities,
        for: reading_user,
        as: :activity
      })
lain's avatar
lain committed
335
336
337
    end
  end

338
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
Eugenij's avatar
Eugenij committed
339
340
341
342
343
344
    params =
      params
      |> Map.put("type", "Create")
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
      |> Map.put(:visibility, "direct")
345

Eugenij's avatar
Eugenij committed
346
    activities =
Maksim's avatar
Maksim committed
347
348
      [user.ap_id]
      |> ActivityPub.fetch_activities_query(params)
349
      |> Pagination.fetch_paginated(params)
350
351

    conn
352
    |> add_link_headers(:dm_timeline, activities)
href's avatar
href committed
353
354
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
355
356
  end

lain's avatar
lain committed
357
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
358
    with %Activity{} = activity <- Activity.get_by_id_with_object(id),
lain's avatar
lain committed
359
         true <- Visibility.visible_for_user?(activity, user) do
href's avatar
href committed
360
361
362
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user})
lain's avatar
lain committed
363
364
365
    end
  end

lain's avatar
lain committed
366
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
367
    with %Activity{} = activity <- Activity.get_by_id(id),
lain's avatar
lain committed
368
369
370
371
372
373
374
375
376
377
         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
378
      result = %{
lain's avatar
lain committed
379
380
381
382
383
384
385
386
        ancestors:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[true] || [],
            as: :activity
          )
          |> Enum.reverse(),
Haelwenn's avatar
Haelwenn committed
387
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
388
389
390
391
392
393
394
395
        descendants:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[false] || [],
            as: :activity
          )
          |> Enum.reverse()
Haelwenn's avatar
Haelwenn committed
396
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
397
398
399
400
401
402
      }

      json(conn, result)
    end
  end

Eugenij's avatar
Eugenij committed
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
  def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
    with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
      conn
      |> add_link_headers(:scheduled_statuses, scheduled_activities)
      |> put_view(ScheduledActivityView)
      |> render("index.json", %{scheduled_activities: scheduled_activities})
    end
  end

  def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
    with %ScheduledActivity{} = scheduled_activity <-
           ScheduledActivity.get(user, scheduled_activity_id) do
      conn
      |> put_view(ScheduledActivityView)
      |> render("show.json", %{scheduled_activity: scheduled_activity})
    else
      _ -> {:error, :not_found}
    end
  end

  def update_scheduled_status(
        %{assigns: %{user: user}} = conn,
        %{"id" => scheduled_activity_id} = params
      ) do
427
428
429
    with %ScheduledActivity{} = scheduled_activity <-
           ScheduledActivity.get(user, scheduled_activity_id),
         {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
Eugenij's avatar
Eugenij committed
430
431
432
      conn
      |> put_view(ScheduledActivityView)
      |> render("show.json", %{scheduled_activity: scheduled_activity})
433
434
435
    else
      nil -> {:error, :not_found}
      error -> error
Eugenij's avatar
Eugenij committed
436
437
438
439
    end
  end

  def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
440
441
442
    with %ScheduledActivity{} = scheduled_activity <-
           ScheduledActivity.get(user, scheduled_activity_id),
         {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
Eugenij's avatar
Eugenij committed
443
      conn
444
445
446
447
448
      |> put_view(ScheduledActivityView)
      |> render("show.json", %{scheduled_activity: scheduled_activity})
    else
      nil -> {:error, :not_found}
      error -> error
Eugenij's avatar
Eugenij committed
449
450
451
    end
  end

452
453
454
455
456
457
458
459
460
  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
461
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
462
463
464
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
lain's avatar
lain committed
465

lain's avatar
lain committed
466
467
468
469
470
471
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

472
    scheduled_at = params["scheduled_at"]
lain's avatar
lain committed
473

474
    if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
475
476
477
478
479
480
      with {:ok, scheduled_activity} <-
             ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
        conn
        |> put_view(ScheduledActivityView)
        |> render("show.json", %{scheduled_activity: scheduled_activity})
      end
481
482
483
484
485
486
487
488
489
490
491
492
    else
      params = Map.drop(params, ["scheduled_at"])

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

      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
lain's avatar
lain committed
493
  end
lain's avatar
lain committed
494
495
496
497
498
499
500
501
502
503
504

  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
505
506

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
507
508
    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
         %Activity{} = announce <- Activity.normalize(announce.data) do
href's avatar
href committed
509
510
511
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
512
513
    end
  end
lain's avatar
lain committed
514

normandy's avatar
normandy committed
515
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
517
         %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
href's avatar
href committed
518
519
520
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
normandy's avatar
normandy committed
521
522
523
    end
  end

lain's avatar
lain committed
524
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
526
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
527
528
529
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
530
531
532
533
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
534
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
535
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
536
537
538
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
539
540
    end
  end
541

minibikini's avatar
minibikini committed
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
  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
563
  def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
564
    with %Activity{} = activity <- Activity.get_by_id_with_object(id),
minibikini's avatar
minibikini committed
565
         %User{} = user <- User.get_cached_by_nickname(user.nickname),
lain's avatar
lain committed
566
         true <- Visibility.visible_for_user?(activity, user),
567
         {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
Haelwenn's avatar
Haelwenn committed
568
569
570
571
572
573
574
      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
rinpatch's avatar
rinpatch committed
575
    with %Activity{} = activity <- Activity.get_by_id_with_object(id),
minibikini's avatar
minibikini committed
576
         %User{} = user <- User.get_cached_by_nickname(user.nickname),
lain's avatar
lain committed
577
         true <- Visibility.visible_for_user?(activity, user),
578
         {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
Haelwenn's avatar
Haelwenn committed
579
580
581
582
583
584
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
  end

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

588
    with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
589
590
591
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
592
593
594
595
596
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
597
598
599
600
    end
  end

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

603
    with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
604
605
606
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
Haelwenn's avatar
Haelwenn committed
607
608
609
    end
  end

610
  def notifications(%{assigns: %{user: user}} = conn, params) do
611
    notifications = MastodonAPI.get_notifications(user, params)
612

lain's avatar
lain committed
613
614
    conn
    |> add_link_headers(:notifications, notifications)
615
616
    |> put_view(NotificationView)
    |> render("index.json", %{notifications: notifications, for: user})
617
618
  end

619
620
  def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    with {:ok, notification} <- Notification.get(user, id) do
621
622
623
      conn
      |> put_view(NotificationView)
      |> render("show.json", %{notification: notification, for: user})
624
625
626
627
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
628
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
    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
644
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
645
646
647
    end
  end

648
649
650
651
652
  def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
    Notification.destroy_multiple(user, ids)
    json(conn, %{})
  end

Roger Braun's avatar
Roger Braun committed
653
654
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
655
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
656
    targets = Repo.all(q)
href's avatar
href committed
657
658
659
660

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

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

666
  def update_media(%{assigns: %{user: user}} = conn, data) do
667
    with %Object{} = object <- Repo.get(Object, data["id"]),
668
         true <- Object.authorize_mutation(object, user),
669
670
671
672
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

673
674
675
676
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
677

678
      attachment_data = Map.put(new_data, "id", object.id)
href's avatar
href committed
679
680
681
682

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

686
687
  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
    with {:ok, object} <-
kaniini's avatar
kaniini committed
688
689
           ActivityPub.upload(
             file,
690
691
692
693
             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
694
695
696
697

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
698
699
700
    end
  end

701
  def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
702
703
    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
         %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
lain's avatar
lain committed
704
      q = from(u in User, where: u.ap_id in ^likes)
705
      users = Repo.all(q)
href's avatar
href committed
706
707
708

      conn
      |> put_view(AccountView)
709
      |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
710
711
712
713
714
    else
      _ -> json(conn, [])
    end
  end

715
  def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
716
717
    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
         %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
lain's avatar
lain committed
718
      q = from(u in User, where: u.ap_id in ^announces)
719
      users = Repo.all(q)
href's avatar
href committed
720
721
722

      conn
      |> put_view(AccountView)
723
      |> render("accounts.json", %{for: user, users: users, as: :user})
724
725
726
727
728
    else
      _ -> json(conn, [])
    end
  end

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

Haelwenn's avatar
Haelwenn committed
732
    tags =
733
734
      [params["tag"], params["any"]]
      |> List.flatten()
Haelwenn's avatar
Haelwenn committed
735
736
737
738
      |> Enum.uniq()
      |> Enum.filter(& &1)
      |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
739
740
741
742
743
    tag_all =
      params["all"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
744
745
746
747
748
    tag_reject =
      params["none"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
749
    activities =
lain's avatar
lain committed
750
751
      params
      |> Map.put("type", "Create")
752
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
753
      |> Map.put("blocking_user", user)
754
      |> Map.put("muting_user", user)
Haelwenn's avatar
Haelwenn committed
755
      |> Map.put("tag", tags)
Haelwenn's avatar
Haelwenn committed
756
      |> Map.put("tag_all", tag_all)
Haelwenn's avatar
Haelwenn committed
757
      |> Map.put("tag_reject", tag_reject)
Haelwenn's avatar
Haelwenn committed
758
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
759
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
760
761

    conn
762
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
href's avatar
href committed
763
764
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
765
766
  end

767
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
minibikini's avatar
minibikini committed
768
    with %User{} = user <- User.get_cached_by_id(id),
769
         followers <- MastodonAPI.get_followers(user, params) do
770
771
772
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
773
          user.info.hide_followers -> []
774
775
776
          true -> followers
        end

href's avatar
href committed
777
      conn
778
      |> add_link_headers(:followers, followers, user)
href's avatar
href committed
779
      |> put_view(AccountView)
780
      |> render("accounts.json", %{for: for_user, users: followers, as: :user})
781
782
783
    end
  end

784
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
minibikini's avatar
minibikini committed
785
    with %User{} = user <- User.get_cached_by_id(id),
786
         followers <- MastodonAPI.get_friends(user, params) do
787
788
789
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
790
          user.info.hide_follows -> []
791
792
793
          true -> followers
        end

href's avatar
href committed
794
      conn
795
      |> add_link_headers(:following, followers, user)
href's avatar
href committed
796
      |> put_view(AccountView)
797
      |> render("accounts.json", %{for: for_user, users: followers, as: :user})
798
799
800
    end
  end

801
802
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
href's avatar
href committed
803
804
      conn
      |> put_view(AccountView)
805
      |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
806
807
808
    end
  end

kaniini's avatar
kaniini committed
809
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
minibikini's avatar
minibikini committed
810
    with %User{} = follower <- User.get_cached_by_id(id),
811
         {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
href's avatar
href committed
812
813
814
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
815
816
817
818
819
820
821
822
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

kaniini's avatar
kaniini committed
823
  def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
minibikini's avatar
minibikini committed
824
    with %User{} = follower <- User.get_cached_by_id(id),
825
         {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
href's avatar
href committed
826
827
828
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
829
830
831
832
833
834
835
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
836

eal's avatar
eal committed
837
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
Eugenij's avatar
Eugenij committed
838
    with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
839
         {_, true} <- {:followed, follower.id != followed.id},
840
         {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
href's avatar
href committed
841
842
843
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
844
    else
845
846
847
      {:followed, _} ->
        {:error, :not_found}

Thog's avatar
Thog committed
848
      {:error, message} ->
eal's avatar
eal committed
849
850
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
851
        |> send_resp(403, Jason.encode!(%{"error" => message}))
852
853
854
    end
  end

eal's avatar
eal committed
855
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
Eugenij's avatar
Eugenij committed
856
    with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
857
         {_, true} <- {:followed, follower.id != followed.id},
858
         {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
href's avatar
href committed
859
860
861
      conn
      |> put_view(AccountView)
      |> render("account.json", %{user: followed, for: follower})
eal's avatar
eal committed
862
    else
863
864
865
      {:followed, _} ->
        {:error, :not_found}

Thog's avatar
Thog committed
866
      {:error, message} ->
eal's avatar
eal committed
867
868
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
869
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
870
871
872
    end
  end

873
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
Eugenij's avatar
Eugenij committed
874
    with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
875
         {_, true} <- {:followed, follower.id != followed.id},
876
         {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
href's avatar
href committed
877
878
879
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
880
881
882
883
884
885
    else
      {:followed, _} ->
        {:error, :not_found}

      error ->
        error
886
887
888
    end
  end

889
  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
minibikini's avatar
minibikini committed
890
    with %User{} = muted <- User.get_cached_by_id(id),
891
         {:ok, muter} <- User.mute(muter, muted) do
892
893
894
895
896
897
898
899
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: muter, target: muted})
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
900
901
902
903
    end
  end

  def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
minibikini's avatar
minibikini committed
904
    with %User{} = muted <- User.get_cached_by_id(id),
905
         {:ok, muter} <- User.unmute(muter, muted) do
906
907
908
909
910
911
912
913
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: muter, target: muted})
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
914
915
916
    end
  end

vaartis's avatar
vaartis committed
917
  def mutes(%{assigns: %{user: user}} = conn, _) do
918
919
    with muted_accounts <- User.muted_users(user) do
      res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
vaartis's avatar
vaartis committed
920
921
922
923
      json(conn, res)
    end
  end

lain's avatar
lain committed
924
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
minibikini's avatar
minibikini committed
925
    with %User{} = blocked <- User.get_cached_by_id(id),
926
927
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do