mastodon_api_controller.ex 54.2 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
42
  alias Pleroma.Web.TwitterAPI.TwitterAPI
minibikini's avatar
cleanup    
minibikini committed
43

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

Thog's avatar
Thog committed
47
  require Logger
48

49
50
51
52
53
54
55
56
57
  plug(
    Pleroma.Plugs.RateLimitPlug,
    %{
      max_requests: Config.get([:app_account_creation, :max_requests]),
      interval: Config.get([:app_account_creation, :interval])
    }
    when action in [:account_register]
  )

58
  @httpoison Application.get_env(:pleroma, :httpoison)
59
  @local_mastodon_name "Mastodon-Local"
60

61
62
  action_fallback(:errors)

63
  def create_app(conn, params) do
64
    scopes = Scopes.fetch_scopes(params, ["read"])
65
66
67
68
69

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

    with cs <- App.register_changeset(%App{}, app_attrs),
72
73
         false <- cs.changes[:client_name] == @local_mastodon_name,
         {:ok, app} <- Repo.insert(cs) do
74
75
76
      conn
      |> put_view(AppView)
      |> render("show.json", %{app: app})
77
78
79
    end
  end

lain's avatar
lain committed
80
81
82
83
84
85
86
87
88
89
90
  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
91
      end
lain's avatar
lain committed
92
93
94
95
    else
      map
    end
  end
96

lain's avatar
lain committed
97
98
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
99

lain's avatar
lain committed
100
101
102
    user_params =
      %{}
      |> add_if_present(params, "display_name", :name)
103
      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
lain's avatar
lain committed
104
105
106
107
      |> 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
108
        else
lain's avatar
lain committed
109
          _ -> :error
lain's avatar
lain committed
110
        end
lain's avatar
lain committed
111
      end)
lain's avatar
lain committed
112

113
114
115
116
117
118
    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
119
    info_params =
120
121
122
123
124
125
      [: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)
126
      |> add_if_present(params, "default_scope", :default_scope)
lain's avatar
lain committed
127
128
129
130
      |> 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
131
        else
lain's avatar
lain committed
132
          _ -> :error
lain's avatar
lain committed
133
        end
lain's avatar
lain committed
134
      end)
135
      |> Map.put(:emoji, user_info_emojis)
136

137
    info_cng = User.Info.profile_update(user.info, info_params)
138

lain's avatar
lain committed
139
140
    with changeset <- User.update_changeset(user, user_params),
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
141
142
143
144
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
145

146
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
147
148
149
150
151
152
153
154
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
155
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
156
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
157
158
159
    json(conn, account)
  end

160
161
162
163
  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)
164
      |> render("short.json", %{app: app})
165
166
167
    end
  end

168
169
  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),
170
         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
171
      account = AccountView.render("account.json", %{user: user, for: for_user})
Roger Braun's avatar
Roger Braun committed
172
173
      json(conn, account)
    else
lain's avatar
lain committed
174
175
176
177
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
Roger Braun's avatar
Roger Braun committed
178
179
180
    end
  end

feld's avatar
feld committed
181
  @mastodon_api_level "2.7.2"
lain's avatar
lain committed
182

lain's avatar
lain committed
183
  def masto_instance(conn, _params) do
Haelwenn's avatar
Haelwenn committed
184
    instance = Config.get(:instance)
href's avatar
href committed
185

lain's avatar
lain committed
186
    response = %{
lain's avatar
lain committed
187
      uri: Web.base_url(),
href's avatar
href committed
188
189
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
href's avatar
href committed
190
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
href's avatar
href committed
191
      email: Keyword.get(instance, :email),
lain's avatar
lain committed
192
      urls: %{
193
        streaming_api: Pleroma.Web.Endpoint.websocket_url()
lain's avatar
lain committed
194
      },
lain's avatar
lain committed
195
196
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
197
198
199
      languages: ["en"],
      registrations: Pleroma.Config.get([:instance, :registrations_open]),
      # Extra (not present in Mastodon):
href's avatar
href committed
200
      max_toot_chars: Keyword.get(instance, :limit)
201
202
    }

lain's avatar
lain committed
203
    json(conn, response)
204
  end
lain's avatar
lain committed
205

206
  def peers(conn, _params) do
lain's avatar
lain committed
207
    json(conn, Stats.get_peers())
208
209
  end

210
  defp mastodonized_emoji do
href's avatar
href committed
211
    Pleroma.Emoji.get_all()
212
    |> Enum.map(fn {shortcode, relative_url, tags} ->
lain's avatar
lain committed
213
214
      url = to_string(URI.merge(Web.base_url(), relative_url))

215
216
217
      %{
        "shortcode" => shortcode,
        "static_url" => url,
218
        "visible_in_picker" => true,
219
        "url" => url,
220
        "tags" => tags
221
222
      }
    end)
223
224
225
226
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
227
    json(conn, mastodon_emoji)
228
229
  end

230
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
231
232
    params =
      conn.params
233
      |> Map.drop(["since_id", "max_id", "min_id"])
234
235
      |> Map.merge(params)

236
    last = List.last(activities)
lain's avatar
lain committed
237

238
    if last do
239
240
241
242
243
244
245
      max_id = last.id

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

246
247
248
249
250
251
252
253
254
255
      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
256
257
258
259

      {next_url, prev_url} =
        if param do
          {
260
261
262
263
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              param,
264
              Map.merge(params, %{max_id: max_id})
265
266
267
268
269
            ),
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              param,
270
              Map.merge(params, %{min_id: min_id})
271
            )
lain's avatar
lain committed
272
273
274
          }
        else
          {
275
276
277
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
278
              Map.merge(params, %{max_id: max_id})
279
280
281
282
            ),
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
283
              Map.merge(params, %{min_id: min_id})
284
            )
lain's avatar
lain committed
285
286
287
          }
        end

288
289
290
291
292
293
294
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
295
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
296
297
298
299
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
300
      |> Map.put("muting_user", user)
lain's avatar
lain committed
301
      |> Map.put("user", user)
lain's avatar
lain committed
302

lain's avatar
lain committed
303
    activities =
Haelwenn's avatar
Haelwenn committed
304
305
      [user.ap_id | user.following]
      |> ActivityPub.fetch_activities(params)
lain's avatar
lain committed
306
      |> Enum.reverse()
307
308

    conn
lain's avatar
lain committed
309
    |> add_link_headers(:home_timeline, activities)
href's avatar
href committed
310
311
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
312
313
314
  end

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

Haelwenn's avatar
Haelwenn committed
317
    activities =
lain's avatar
lain committed
318
319
      params
      |> Map.put("type", ["Create", "Announce"])
320
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
321
      |> Map.put("blocking_user", user)
322
      |> Map.put("muting_user", user)
Haelwenn's avatar
Haelwenn committed
323
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
324
      |> Enum.reverse()
lain's avatar
lain committed
325

lain's avatar
lain committed
326
    conn
327
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
href's avatar
href committed
328
329
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
330
331
  end

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

336
337
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
href's avatar
href committed
338
339
      |> put_view(StatusView)
      |> render("index.json", %{
340
341
342
343
        activities: activities,
        for: reading_user,
        as: :activity
      })
lain's avatar
lain committed
344
345
346
    end
  end

347
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
Eugenij's avatar
Eugenij committed
348
349
350
351
352
353
    params =
      params
      |> Map.put("type", "Create")
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
      |> Map.put(:visibility, "direct")
354

Eugenij's avatar
Eugenij committed
355
    activities =
Maksim's avatar
Maksim committed
356
357
      [user.ap_id]
      |> ActivityPub.fetch_activities_query(params)
358
      |> Pagination.fetch_paginated(params)
359
360

    conn
361
    |> add_link_headers(:dm_timeline, activities)
href's avatar
href committed
362
363
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
364
365
  end

lain's avatar
lain committed
366
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
367
    with %Activity{} = activity <- Activity.get_by_id_with_object(id),
lain's avatar
lain committed
368
         true <- Visibility.visible_for_user?(activity, user) do
href's avatar
href committed
369
370
371
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user})
lain's avatar
lain committed
372
373
374
    end
  end

lain's avatar
lain committed
375
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
376
    with %Activity{} = activity <- Activity.get_by_id(id),
lain's avatar
lain committed
377
378
379
380
381
382
383
384
385
386
         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
387
      result = %{
lain's avatar
lain committed
388
389
390
391
392
393
394
395
        ancestors:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[true] || [],
            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
403
404
        descendants:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[false] || [],
            as: :activity
          )
          |> Enum.reverse()
Haelwenn's avatar
Haelwenn committed
405
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
406
407
408
409
410
411
      }

      json(conn, result)
    end
  end

Eugenij's avatar
Eugenij committed
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
  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
436
437
438
    with %ScheduledActivity{} = scheduled_activity <-
           ScheduledActivity.get(user, scheduled_activity_id),
         {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
Eugenij's avatar
Eugenij committed
439
440
441
      conn
      |> put_view(ScheduledActivityView)
      |> render("show.json", %{scheduled_activity: scheduled_activity})
442
443
444
    else
      nil -> {:error, :not_found}
      error -> error
Eugenij's avatar
Eugenij committed
445
446
447
448
    end
  end

  def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
449
450
451
    with %ScheduledActivity{} = scheduled_activity <-
           ScheduledActivity.get(user, scheduled_activity_id),
         {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
Eugenij's avatar
Eugenij committed
452
      conn
453
454
455
456
457
      |> put_view(ScheduledActivityView)
      |> render("show.json", %{scheduled_activity: scheduled_activity})
    else
      nil -> {:error, :not_found}
      error -> error
Eugenij's avatar
Eugenij committed
458
459
460
    end
  end

461
462
463
464
465
466
467
468
469
  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
470
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
471
472
473
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
lain's avatar
lain committed
474

lain's avatar
lain committed
475
476
477
478
479
480
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

481
    scheduled_at = params["scheduled_at"]
lain's avatar
lain committed
482

483
    if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
484
485
486
487
488
489
      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
490
491
492
493
494
495
496
497
498
499
500
501
    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
502
  end
lain's avatar
lain committed
503
504
505
506
507
508
509
510
511
512
513

  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
514
515

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

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

lain's avatar
lain committed
533
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
534
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(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
541
542
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
543
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
544
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
545
546
547
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
548
549
    end
  end
550

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

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

597
    with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
598
599
600
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
601
602
603
604
605
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
606
607
608
609
    end
  end

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

612
    with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
613
614
615
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
Haelwenn's avatar
Haelwenn committed
616
617
618
    end
  end

619
  def notifications(%{assigns: %{user: user}} = conn, params) do
620
    notifications = MastodonAPI.get_notifications(user, params)
621

lain's avatar
lain committed
622
623
    conn
    |> add_link_headers(:notifications, notifications)
624
625
    |> put_view(NotificationView)
    |> render("index.json", %{notifications: notifications, for: user})
626
627
  end

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

657
658
659
660
661
  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
662
663
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
664
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
665
    targets = Repo.all(q)
href's avatar
href committed
666
667
668
669

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

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

675
  def update_media(%{assigns: %{user: user}} = conn, data) do
676
    with %Object{} = object <- Repo.get(Object, data["id"]),
677
         true <- Object.authorize_mutation(object, user),
678
679
680
681
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

682
683
684
685
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
686

687
      attachment_data = Map.put(new_data, "id", object.id)
href's avatar
href committed
688
689
690
691

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

695
696
  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
    with {:ok, object} <-
kaniini's avatar
kaniini committed
697
698
           ActivityPub.upload(
             file,
699
700
701
702
             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
703
704
705
706

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
707
708
709
    end
  end

710
  def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
711
712
    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
         %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
lain's avatar
lain committed
713
      q = from(u in User, where: u.ap_id in ^likes)
714
      users = Repo.all(q)
href's avatar
href committed
715
716
717

      conn
      |> put_view(AccountView)
718
      |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
719
720
721
722
723
    else
      _ -> json(conn, [])
    end
  end

724
  def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
725
726
    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
         %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
lain's avatar
lain committed
727
      q = from(u in User, where: u.ap_id in ^announces)
728
      users = Repo.all(q)
href's avatar
href committed
729
730
731

      conn
      |> put_view(AccountView)
732
      |> render("accounts.json", %{for: user, users: users, as: :user})
733
734
735
736
737
    else
      _ -> json(conn, [])
    end
  end

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

Haelwenn's avatar
Haelwenn committed
741
    tags =
742
743
      [params["tag"], params["any"]]
      |> List.flatten()
Haelwenn's avatar
Haelwenn committed
744
745
746
747
      |> Enum.uniq()
      |> Enum.filter(& &1)
      |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
748
749
750
751
752
    tag_all =
      params["all"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
753
754
755
756
757
    tag_reject =
      params["none"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
758
    activities =
lain's avatar
lain committed
759
760
      params
      |> Map.put("type", "Create")
761
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
762
      |> Map.put("blocking_user", user)
763
      |> Map.put("muting_user", user)
Haelwenn's avatar
Haelwenn committed
764
      |> Map.put("tag", tags)
Haelwenn's avatar
Haelwenn committed
765
      |> Map.put("tag_all", tag_all)
Haelwenn's avatar
Haelwenn committed
766
      |> Map.put("tag_reject", tag_reject)
Haelwenn's avatar
Haelwenn committed
767
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
768
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
769
770

    conn
771
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
href's avatar
href committed
772
773
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
774
775
  end

776
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
minibikini's avatar
minibikini committed
777
    with %User{} = user <- User.get_cached_by_id(id),
778
         followers <- MastodonAPI.get_followers(user, params) do
779
780
781
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
782
          user.info.hide_followers -> []
783
784
785
          true -> followers
        end

href's avatar
href committed
786
      conn
787
      |> add_link_headers(:followers, followers, user)
href's avatar
href committed
788
      |> put_view(AccountView)
789
      |> render("accounts.json", %{for: for_user, users: followers, as: :user})
790
791
792
    end
  end

793
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
minibikini's avatar
minibikini committed
794
    with %User{} = user <- User.get_cached_by_id(id),
795
         followers <- MastodonAPI.get_friends(user, params) do
796
797
798
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
799
          user.info.hide_follows -> []
800
801
802
          true -> followers
        end

href's avatar
href committed
803
      conn
804
      |> add_link_headers(:following, followers, user)
href's avatar
href committed
805
      |> put_view(AccountView)
806
      |> render("accounts.json", %{for: for_user, users: followers, as: :user})
807
808
809
    end
  end

810
811
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
href's avatar
href committed
812
813
      conn
      |> put_view(AccountView)
814
      |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
815
816
817
    end
  end

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

kaniini's avatar
kaniini committed
832
  def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
minibikini's avatar
minibikini committed
833
    with %User{} = follower <- User.get_cached_by_id(id),
834
         {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
href's avatar
href committed
835
836
837
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
838
839
840
841
842
843
844
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
845

eal's avatar
eal committed
846
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
Eugenij's avatar
Eugenij committed
847
    with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
848
         {_, true} <- {:followed, follower.id != followed.id},
849
         {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
href's avatar
href committed
850
851
852
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
853
    else
854
855
856
      {:followed, _} ->
        {:error, :not_found}

Thog's avatar
Thog committed
857
      {:error, message} ->
eal's avatar
eal committed
858
859
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
860
        |> send_resp(403, Jason.encode!(%{"error" => message}))
861
862
863
    end
  end

eal's avatar
eal committed
864
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
Eugenij's avatar
Eugenij committed
865
    with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
866
         {_, true} <- {:followed, follower.id != followed.id},
867
         {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
href's avatar
href committed
868
869
870
      conn
      |> put_view(AccountView)
      |> render("account.json", %{user: followed, for: follower})
eal's avatar
eal committed
871
    else
872
873
874
      {:followed, _} ->
        {:error, :not_found}

Thog's avatar
Thog committed
875
      {:error, message} ->
eal's avatar
eal committed
876
877
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
878
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
879
880
881
    end
  end

882
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
Eugenij's avatar
Eugenij committed
883
    with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
884
         {_, true} <- {:followed, follower.id != followed.id},
885
         {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
href's avatar
href committed
886
887
888
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
889
890
891
892
893
894
    else
      {:followed, _} ->
        {:error, :not_found}

      error ->
        error
895
896
897
    end
  end

898
  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
minibikini's avatar
minibikini committed
899
    with %User{} = muted <- User.get_cached_by_id(id),
900
         {:ok, muter} <- User.mute(muter, muted) do
901
902
903
904
905
906
907
908
      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}))
909
910
911
912
    end
  end

  def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
minibikini's avatar
minibikini committed
913
    with %User{} = muted <- User.get_cached_by_id(id),
914
         {:ok, muter} <- User.unmute(muter, muted) do
915
916
917
918
919
920
921
922
      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}))
923
924
925
    end
  end

vaartis's avatar
vaartis committed
926
  def mutes(%{assigns: %{user: user}} = conn, _) do
927
928
    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
929
930
931
932
      json(conn, res)
    end