76648b9f7e126470074eab1b7bc465fc6aa51981
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7 alias Ecto.Changeset
8 alias Pleroma.Activity
9 alias Pleroma.Bookmark
10 alias Pleroma.Config
11 alias Pleroma.Conversation.Participation
12 alias Pleroma.Filter
13 alias Pleroma.Formatter
14 alias Pleroma.HTTP
15 alias Pleroma.Notification
16 alias Pleroma.Object
17 alias Pleroma.Pagination
18 alias Pleroma.Plugs.RateLimiter
19 alias Pleroma.Repo
20 alias Pleroma.ScheduledActivity
21 alias Pleroma.Stats
22 alias Pleroma.User
23 alias Pleroma.Web
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.ConversationView
30 alias Pleroma.Web.MastodonAPI.FilterView
31 alias Pleroma.Web.MastodonAPI.ListView
32 alias Pleroma.Web.MastodonAPI.MastodonAPI
33 alias Pleroma.Web.MastodonAPI.MastodonView
34 alias Pleroma.Web.MastodonAPI.NotificationView
35 alias Pleroma.Web.MastodonAPI.ReportView
36 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
37 alias Pleroma.Web.MastodonAPI.StatusView
38 alias Pleroma.Web.MediaProxy
39 alias Pleroma.Web.OAuth.App
40 alias Pleroma.Web.OAuth.Authorization
41 alias Pleroma.Web.OAuth.Scopes
42 alias Pleroma.Web.OAuth.Token
43 alias Pleroma.Web.TwitterAPI.TwitterAPI
44
45 alias Pleroma.Web.ControllerHelper
46 import Ecto.Query
47
48 require Logger
49
50 @rate_limited_status_crud_actions ~w(post_status delete_status)a
51 @rate_limited_status_reactions ~w(reblog_status unreblog_status fav_status unfav_status)a
52 @rate_limited_status_actions @rate_limited_status_crud_actions ++ @rate_limited_status_reactions
53
54 plug(
55 RateLimiter,
56 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
57 when action in ~w(reblog_status unreblog_status)a
58 )
59
60 plug(
61 RateLimiter,
62 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
63 when action in ~w(fav_status unfav_status)a
64 )
65
66 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
67 plug(RateLimiter, :app_account_creation when action == :account_register)
68 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
69
70 @local_mastodon_name "Mastodon-Local"
71
72 action_fallback(:errors)
73
74 def create_app(conn, params) do
75 scopes = Scopes.fetch_scopes(params, ["read"])
76
77 app_attrs =
78 params
79 |> Map.drop(["scope", "scopes"])
80 |> Map.put("scopes", scopes)
81
82 with cs <- App.register_changeset(%App{}, app_attrs),
83 false <- cs.changes[:client_name] == @local_mastodon_name,
84 {:ok, app} <- Repo.insert(cs) do
85 conn
86 |> put_view(AppView)
87 |> render("show.json", %{app: app})
88 end
89 end
90
91 defp add_if_present(
92 map,
93 params,
94 params_field,
95 map_field,
96 value_function \\ fn x -> {:ok, x} end
97 ) do
98 if Map.has_key?(params, params_field) do
99 case value_function.(params[params_field]) do
100 {:ok, new_value} -> Map.put(map, map_field, new_value)
101 :error -> map
102 end
103 else
104 map
105 end
106 end
107
108 def update_credentials(%{assigns: %{user: user}} = conn, params) do
109 original_user = user
110
111 user_params =
112 %{}
113 |> add_if_present(params, "display_name", :name)
114 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
115 |> add_if_present(params, "avatar", :avatar, fn value ->
116 with %Plug.Upload{} <- value,
117 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
118 {:ok, object.data}
119 else
120 _ -> :error
121 end
122 end)
123
124 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
125
126 user_info_emojis =
127 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
128 |> Enum.dedup()
129
130 info_params =
131 [
132 :no_rich_text,
133 :locked,
134 :hide_followers,
135 :hide_follows,
136 :hide_favorites,
137 :show_role,
138 :skip_thread_containment
139 ]
140 |> Enum.reduce(%{}, fn key, acc ->
141 add_if_present(acc, params, to_string(key), key, fn value ->
142 {:ok, ControllerHelper.truthy_param?(value)}
143 end)
144 end)
145 |> add_if_present(params, "default_scope", :default_scope)
146 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
147 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
148 end)
149 |> add_if_present(params, "header", :banner, fn value ->
150 with %Plug.Upload{} <- value,
151 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
152 {:ok, object.data}
153 else
154 _ -> :error
155 end
156 end)
157 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
158 with %Plug.Upload{} <- value,
159 {:ok, object} <- ActivityPub.upload(value, type: :background) do
160 {:ok, object.data}
161 else
162 _ -> :error
163 end
164 end)
165 |> Map.put(:emoji, user_info_emojis)
166
167 info_cng = User.Info.profile_update(user.info, info_params)
168
169 with changeset <- User.update_changeset(user, user_params),
170 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
171 {:ok, user} <- User.update_and_set_cache(changeset) do
172 if original_user != user do
173 CommonAPI.update(user)
174 end
175
176 json(
177 conn,
178 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
179 )
180 else
181 _e -> render_error(conn, :forbidden, "Invalid request")
182 end
183 end
184
185 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
186 change = Changeset.change(user, %{avatar: nil})
187 {:ok, user} = User.update_and_set_cache(change)
188 CommonAPI.update(user)
189
190 json(conn, %{url: nil})
191 end
192
193 def update_avatar(%{assigns: %{user: user}} = conn, params) do
194 {:ok, object} = ActivityPub.upload(params, type: :avatar)
195 change = Changeset.change(user, %{avatar: object.data})
196 {:ok, user} = User.update_and_set_cache(change)
197 CommonAPI.update(user)
198 %{"url" => [%{"href" => href} | _]} = object.data
199
200 json(conn, %{url: href})
201 end
202
203 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
204 with new_info <- %{"banner" => %{}},
205 info_cng <- User.Info.profile_update(user.info, new_info),
206 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
207 {:ok, user} <- User.update_and_set_cache(changeset) do
208 CommonAPI.update(user)
209
210 json(conn, %{url: nil})
211 end
212 end
213
214 def update_banner(%{assigns: %{user: user}} = conn, params) do
215 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
216 new_info <- %{"banner" => object.data},
217 info_cng <- User.Info.profile_update(user.info, new_info),
218 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
219 {:ok, user} <- User.update_and_set_cache(changeset) do
220 CommonAPI.update(user)
221 %{"url" => [%{"href" => href} | _]} = object.data
222
223 json(conn, %{url: href})
224 end
225 end
226
227 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
228 with new_info <- %{"background" => %{}},
229 info_cng <- User.Info.profile_update(user.info, new_info),
230 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
231 {:ok, _user} <- User.update_and_set_cache(changeset) do
232 json(conn, %{url: nil})
233 end
234 end
235
236 def update_background(%{assigns: %{user: user}} = conn, params) do
237 with {:ok, object} <- ActivityPub.upload(params, type: :background),
238 new_info <- %{"background" => object.data},
239 info_cng <- User.Info.profile_update(user.info, new_info),
240 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
241 {:ok, _user} <- User.update_and_set_cache(changeset) do
242 %{"url" => [%{"href" => href} | _]} = object.data
243
244 json(conn, %{url: href})
245 end
246 end
247
248 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
249 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
250
251 account =
252 AccountView.render("account.json", %{
253 user: user,
254 for: user,
255 with_pleroma_settings: true,
256 with_chat_token: chat_token
257 })
258
259 json(conn, account)
260 end
261
262 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
263 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
264 conn
265 |> put_view(AppView)
266 |> render("short.json", %{app: app})
267 end
268 end
269
270 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
271 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
272 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
273 account = AccountView.render("account.json", %{user: user, for: for_user})
274 json(conn, account)
275 else
276 _e -> render_error(conn, :not_found, "Can't find user")
277 end
278 end
279
280 @mastodon_api_level "2.7.2"
281
282 def masto_instance(conn, _params) do
283 instance = Config.get(:instance)
284
285 response = %{
286 uri: Web.base_url(),
287 title: Keyword.get(instance, :name),
288 description: Keyword.get(instance, :description),
289 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
290 email: Keyword.get(instance, :email),
291 urls: %{
292 streaming_api: Pleroma.Web.Endpoint.websocket_url()
293 },
294 stats: Stats.get_stats(),
295 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
296 languages: ["en"],
297 registrations: Pleroma.Config.get([:instance, :registrations_open]),
298 # Extra (not present in Mastodon):
299 max_toot_chars: Keyword.get(instance, :limit),
300 poll_limits: Keyword.get(instance, :poll_limits)
301 }
302
303 json(conn, response)
304 end
305
306 def peers(conn, _params) do
307 json(conn, Stats.get_peers())
308 end
309
310 defp mastodonized_emoji do
311 Pleroma.Emoji.get_all()
312 |> Enum.map(fn {shortcode, relative_url, tags} ->
313 url = to_string(URI.merge(Web.base_url(), relative_url))
314
315 %{
316 "shortcode" => shortcode,
317 "static_url" => url,
318 "visible_in_picker" => true,
319 "url" => url,
320 "tags" => tags,
321 # Assuming that a comma is authorized in the category name
322 "category" => (tags -- ["Custom"]) |> Enum.join(",")
323 }
324 end)
325 end
326
327 def custom_emojis(conn, _params) do
328 mastodon_emoji = mastodonized_emoji()
329 json(conn, mastodon_emoji)
330 end
331
332 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
333 params =
334 conn.params
335 |> Map.drop(["since_id", "max_id", "min_id"])
336 |> Map.merge(params)
337
338 last = List.last(activities)
339
340 if last do
341 max_id = last.id
342
343 limit =
344 params
345 |> Map.get("limit", "20")
346 |> String.to_integer()
347
348 min_id =
349 if length(activities) <= limit do
350 activities
351 |> List.first()
352 |> Map.get(:id)
353 else
354 activities
355 |> Enum.at(limit * -1)
356 |> Map.get(:id)
357 end
358
359 {next_url, prev_url} =
360 if param do
361 {
362 mastodon_api_url(
363 Pleroma.Web.Endpoint,
364 method,
365 param,
366 Map.merge(params, %{max_id: max_id})
367 ),
368 mastodon_api_url(
369 Pleroma.Web.Endpoint,
370 method,
371 param,
372 Map.merge(params, %{min_id: min_id})
373 )
374 }
375 else
376 {
377 mastodon_api_url(
378 Pleroma.Web.Endpoint,
379 method,
380 Map.merge(params, %{max_id: max_id})
381 ),
382 mastodon_api_url(
383 Pleroma.Web.Endpoint,
384 method,
385 Map.merge(params, %{min_id: min_id})
386 )
387 }
388 end
389
390 conn
391 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
392 else
393 conn
394 end
395 end
396
397 def home_timeline(%{assigns: %{user: user}} = conn, params) do
398 params =
399 params
400 |> Map.put("type", ["Create", "Announce"])
401 |> Map.put("blocking_user", user)
402 |> Map.put("muting_user", user)
403 |> Map.put("user", user)
404
405 activities =
406 [user.ap_id | user.following]
407 |> ActivityPub.fetch_activities(params)
408 |> Enum.reverse()
409
410 conn
411 |> add_link_headers(:home_timeline, activities)
412 |> put_view(StatusView)
413 |> render("index.json", %{activities: activities, for: user, as: :activity})
414 end
415
416 def public_timeline(%{assigns: %{user: user}} = conn, params) do
417 local_only = params["local"] in [true, "True", "true", "1"]
418
419 activities =
420 params
421 |> Map.put("type", ["Create", "Announce"])
422 |> Map.put("local_only", local_only)
423 |> Map.put("blocking_user", user)
424 |> Map.put("muting_user", user)
425 |> ActivityPub.fetch_public_activities()
426 |> Enum.reverse()
427
428 conn
429 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
430 |> put_view(StatusView)
431 |> render("index.json", %{activities: activities, for: user, as: :activity})
432 end
433
434 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
435 with %User{} = user <- User.get_cached_by_id(params["id"]) do
436 params =
437 params
438 |> Map.put("tag", params["tagged"])
439
440 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
441
442 conn
443 |> add_link_headers(:user_statuses, activities, params["id"])
444 |> put_view(StatusView)
445 |> render("index.json", %{
446 activities: activities,
447 for: reading_user,
448 as: :activity
449 })
450 end
451 end
452
453 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
454 params =
455 params
456 |> Map.put("type", "Create")
457 |> Map.put("blocking_user", user)
458 |> Map.put("user", user)
459 |> Map.put(:visibility, "direct")
460
461 activities =
462 [user.ap_id]
463 |> ActivityPub.fetch_activities_query(params)
464 |> Pagination.fetch_paginated(params)
465
466 conn
467 |> add_link_headers(:dm_timeline, activities)
468 |> put_view(StatusView)
469 |> render("index.json", %{activities: activities, for: user, as: :activity})
470 end
471
472 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
473 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
474 true <- Visibility.visible_for_user?(activity, user) do
475 conn
476 |> put_view(StatusView)
477 |> try_render("status.json", %{activity: activity, for: user})
478 end
479 end
480
481 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
482 with %Activity{} = activity <- Activity.get_by_id(id),
483 activities <-
484 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
485 "blocking_user" => user,
486 "user" => user
487 }),
488 activities <-
489 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
490 activities <-
491 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
492 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
493 result = %{
494 ancestors:
495 StatusView.render(
496 "index.json",
497 for: user,
498 activities: grouped_activities[true] || [],
499 as: :activity
500 )
501 |> Enum.reverse(),
502 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
503 descendants:
504 StatusView.render(
505 "index.json",
506 for: user,
507 activities: grouped_activities[false] || [],
508 as: :activity
509 )
510 |> Enum.reverse()
511 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
512 }
513
514 json(conn, result)
515 end
516 end
517
518 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
519 with %Object{} = object <- Object.get_by_id(id),
520 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
521 true <- Visibility.visible_for_user?(activity, user) do
522 conn
523 |> put_view(StatusView)
524 |> try_render("poll.json", %{object: object, for: user})
525 else
526 nil -> render_error(conn, :not_found, "Record not found")
527 false -> render_error(conn, :not_found, "Record not found")
528 end
529 end
530
531 defp get_cached_vote_or_vote(user, object, choices) do
532 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
533
534 {_, res} =
535 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
536 case CommonAPI.vote(user, object, choices) do
537 {:error, _message} = res -> {:ignore, res}
538 res -> {:commit, res}
539 end
540 end)
541
542 res
543 end
544
545 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
546 with %Object{} = object <- Object.get_by_id(id),
547 true <- object.data["type"] == "Question",
548 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
549 true <- Visibility.visible_for_user?(activity, user),
550 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
551 conn
552 |> put_view(StatusView)
553 |> try_render("poll.json", %{object: object, for: user})
554 else
555 nil ->
556 render_error(conn, :not_found, "Record not found")
557
558 false ->
559 render_error(conn, :not_found, "Record not found")
560
561 {:error, message} ->
562 conn
563 |> put_status(:unprocessable_entity)
564 |> json(%{error: message})
565 end
566 end
567
568 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
569 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
570 conn
571 |> add_link_headers(:scheduled_statuses, scheduled_activities)
572 |> put_view(ScheduledActivityView)
573 |> render("index.json", %{scheduled_activities: scheduled_activities})
574 end
575 end
576
577 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
578 with %ScheduledActivity{} = scheduled_activity <-
579 ScheduledActivity.get(user, scheduled_activity_id) do
580 conn
581 |> put_view(ScheduledActivityView)
582 |> render("show.json", %{scheduled_activity: scheduled_activity})
583 else
584 _ -> {:error, :not_found}
585 end
586 end
587
588 def update_scheduled_status(
589 %{assigns: %{user: user}} = conn,
590 %{"id" => scheduled_activity_id} = params
591 ) do
592 with %ScheduledActivity{} = scheduled_activity <-
593 ScheduledActivity.get(user, scheduled_activity_id),
594 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
595 conn
596 |> put_view(ScheduledActivityView)
597 |> render("show.json", %{scheduled_activity: scheduled_activity})
598 else
599 nil -> {:error, :not_found}
600 error -> error
601 end
602 end
603
604 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
605 with %ScheduledActivity{} = scheduled_activity <-
606 ScheduledActivity.get(user, scheduled_activity_id),
607 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
608 conn
609 |> put_view(ScheduledActivityView)
610 |> render("show.json", %{scheduled_activity: scheduled_activity})
611 else
612 nil -> {:error, :not_found}
613 error -> error
614 end
615 end
616
617 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
618 params =
619 params
620 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
621
622 scheduled_at = params["scheduled_at"]
623
624 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
625 with {:ok, scheduled_activity} <-
626 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
627 conn
628 |> put_view(ScheduledActivityView)
629 |> render("show.json", %{scheduled_activity: scheduled_activity})
630 end
631 else
632 params = Map.drop(params, ["scheduled_at"])
633
634 case CommonAPI.post(user, params) do
635 {:error, message} ->
636 conn
637 |> put_status(:unprocessable_entity)
638 |> json(%{error: message})
639
640 {:ok, activity} ->
641 conn
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
644 end
645 end
646 end
647
648 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
649 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
650 json(conn, %{})
651 else
652 _e -> render_error(conn, :forbidden, "Can't delete this post")
653 end
654 end
655
656 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
657 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
658 %Activity{} = announce <- Activity.normalize(announce.data) do
659 conn
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
662 end
663 end
664
665 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
666 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
667 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
668 conn
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
671 end
672 end
673
674 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
675 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
676 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
677 conn
678 |> put_view(StatusView)
679 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
680 end
681 end
682
683 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
684 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
685 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
686 conn
687 |> put_view(StatusView)
688 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
689 end
690 end
691
692 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
693 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
694 conn
695 |> put_view(StatusView)
696 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
697 else
698 {:error, reason} ->
699 conn
700 |> put_status(:bad_request)
701 |> json(%{"error" => reason})
702 end
703 end
704
705 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
706 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
707 conn
708 |> put_view(StatusView)
709 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
710 end
711 end
712
713 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
714 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
715 %User{} = user <- User.get_cached_by_nickname(user.nickname),
716 true <- Visibility.visible_for_user?(activity, user),
717 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
718 conn
719 |> put_view(StatusView)
720 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
721 end
722 end
723
724 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
725 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
726 %User{} = user <- User.get_cached_by_nickname(user.nickname),
727 true <- Visibility.visible_for_user?(activity, user),
728 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
729 conn
730 |> put_view(StatusView)
731 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
732 end
733 end
734
735 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
736 activity = Activity.get_by_id(id)
737
738 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
739 conn
740 |> put_view(StatusView)
741 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
742 else
743 {:error, reason} ->
744 conn
745 |> put_resp_content_type("application/json")
746 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
747 end
748 end
749
750 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
751 activity = Activity.get_by_id(id)
752
753 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
754 conn
755 |> put_view(StatusView)
756 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
757 end
758 end
759
760 def notifications(%{assigns: %{user: user}} = conn, params) do
761 notifications = MastodonAPI.get_notifications(user, params)
762
763 conn
764 |> add_link_headers(:notifications, notifications)
765 |> put_view(NotificationView)
766 |> render("index.json", %{notifications: notifications, for: user})
767 end
768
769 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
770 with {:ok, notification} <- Notification.get(user, id) do
771 conn
772 |> put_view(NotificationView)
773 |> render("show.json", %{notification: notification, for: user})
774 else
775 {:error, reason} ->
776 conn
777 |> put_status(:forbidden)
778 |> json(%{"error" => reason})
779 end
780 end
781
782 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
783 Notification.clear(user)
784 json(conn, %{})
785 end
786
787 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
788 with {:ok, _notif} <- Notification.dismiss(user, id) do
789 json(conn, %{})
790 else
791 {:error, reason} ->
792 conn
793 |> put_status(:forbidden)
794 |> json(%{"error" => reason})
795 end
796 end
797
798 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
799 Notification.destroy_multiple(user, ids)
800 json(conn, %{})
801 end
802
803 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
804 id = List.wrap(id)
805 q = from(u in User, where: u.id in ^id)
806 targets = Repo.all(q)
807
808 conn
809 |> put_view(AccountView)
810 |> render("relationships.json", %{user: user, targets: targets})
811 end
812
813 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
814 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
815
816 def update_media(%{assigns: %{user: user}} = conn, data) do
817 with %Object{} = object <- Repo.get(Object, data["id"]),
818 true <- Object.authorize_mutation(object, user),
819 true <- is_binary(data["description"]),
820 description <- data["description"] do
821 new_data = %{object.data | "name" => description}
822
823 {:ok, _} =
824 object
825 |> Object.change(%{data: new_data})
826 |> Repo.update()
827
828 attachment_data = Map.put(new_data, "id", object.id)
829
830 conn
831 |> put_view(StatusView)
832 |> render("attachment.json", %{attachment: attachment_data})
833 end
834 end
835
836 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
837 with {:ok, object} <-
838 ActivityPub.upload(
839 file,
840 actor: User.ap_id(user),
841 description: Map.get(data, "description")
842 ) do
843 attachment_data = Map.put(object.data, "id", object.id)
844
845 conn
846 |> put_view(StatusView)
847 |> render("attachment.json", %{attachment: attachment_data})
848 end
849 end
850
851 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
852 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
853 %{} = attachment_data <- Map.put(object.data, "id", object.id),
854 %{type: type} = rendered <-
855 StatusView.render("attachment.json", %{attachment: attachment_data}) do
856 # Reject if not an image
857 if type == "image" do
858 # Sure!
859 # Save to the user's info
860 info_changeset = User.Info.mascot_update(user.info, rendered)
861
862 user_changeset =
863 user
864 |> Ecto.Changeset.change()
865 |> Ecto.Changeset.put_embed(:info, info_changeset)
866
867 {:ok, _user} = User.update_and_set_cache(user_changeset)
868
869 conn
870 |> json(rendered)
871 else
872 render_error(conn, :unsupported_media_type, "mascots can only be images")
873 end
874 end
875 end
876
877 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
878 mascot = User.get_mascot(user)
879
880 conn
881 |> json(mascot)
882 end
883
884 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
885 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
886 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
887 q = from(u in User, where: u.ap_id in ^likes)
888 users = Repo.all(q)
889
890 conn
891 |> put_view(AccountView)
892 |> render("accounts.json", %{for: user, users: users, as: :user})
893 else
894 _ -> json(conn, [])
895 end
896 end
897
898 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
899 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
900 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
901 q = from(u in User, where: u.ap_id in ^announces)
902 users = Repo.all(q)
903
904 conn
905 |> put_view(AccountView)
906 |> render("accounts.json", %{for: user, users: users, as: :user})
907 else
908 _ -> json(conn, [])
909 end
910 end
911
912 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
913 local_only = params["local"] in [true, "True", "true", "1"]
914
915 tags =
916 [params["tag"], params["any"]]
917 |> List.flatten()
918 |> Enum.uniq()
919 |> Enum.filter(& &1)
920 |> Enum.map(&String.downcase(&1))
921
922 tag_all =
923 params["all"] ||
924 []
925 |> Enum.map(&String.downcase(&1))
926
927 tag_reject =
928 params["none"] ||
929 []
930 |> Enum.map(&String.downcase(&1))
931
932 activities =
933 params
934 |> Map.put("type", "Create")
935 |> Map.put("local_only", local_only)
936 |> Map.put("blocking_user", user)
937 |> Map.put("muting_user", user)
938 |> Map.put("tag", tags)
939 |> Map.put("tag_all", tag_all)
940 |> Map.put("tag_reject", tag_reject)
941 |> ActivityPub.fetch_public_activities()
942 |> Enum.reverse()
943
944 conn
945 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
946 |> put_view(StatusView)
947 |> render("index.json", %{activities: activities, for: user, as: :activity})
948 end
949
950 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
951 with %User{} = user <- User.get_cached_by_id(id),
952 followers <- MastodonAPI.get_followers(user, params) do
953 followers =
954 cond do
955 for_user && user.id == for_user.id -> followers
956 user.info.hide_followers -> []
957 true -> followers
958 end
959
960 conn
961 |> add_link_headers(:followers, followers, user)
962 |> put_view(AccountView)
963 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
964 end
965 end
966
967 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
968 with %User{} = user <- User.get_cached_by_id(id),
969 followers <- MastodonAPI.get_friends(user, params) do
970 followers =
971 cond do
972 for_user && user.id == for_user.id -> followers
973 user.info.hide_follows -> []
974 true -> followers
975 end
976
977 conn
978 |> add_link_headers(:following, followers, user)
979 |> put_view(AccountView)
980 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
981 end
982 end
983
984 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
985 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
986 conn
987 |> put_view(AccountView)
988 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
989 end
990 end
991
992 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
993 with %User{} = follower <- User.get_cached_by_id(id),
994 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
995 conn
996 |> put_view(AccountView)
997 |> render("relationship.json", %{user: followed, target: follower})
998 else
999 {:error, message} ->
1000 conn
1001 |> put_status(:forbidden)
1002 |> json(%{error: message})
1003 end
1004 end
1005
1006 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1007 with %User{} = follower <- User.get_cached_by_id(id),
1008 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1009 conn
1010 |> put_view(AccountView)
1011 |> render("relationship.json", %{user: followed, target: follower})
1012 else
1013 {:error, message} ->
1014 conn
1015 |> put_status(:forbidden)
1016 |> json(%{error: message})
1017 end
1018 end
1019
1020 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1021 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1022 {_, true} <- {:followed, follower.id != followed.id},
1023 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1024 conn
1025 |> put_view(AccountView)
1026 |> render("relationship.json", %{user: follower, target: followed})
1027 else
1028 {:followed, _} ->
1029 {:error, :not_found}
1030
1031 {:error, message} ->
1032 conn
1033 |> put_status(:forbidden)
1034 |> json(%{error: message})
1035 end
1036 end
1037
1038 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1039 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1040 {_, true} <- {:followed, follower.id != followed.id},
1041 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1042 conn
1043 |> put_view(AccountView)
1044 |> render("account.json", %{user: followed, for: follower})
1045 else
1046 {:followed, _} ->
1047 {:error, :not_found}
1048
1049 {:error, message} ->
1050 conn
1051 |> put_status(:forbidden)
1052 |> json(%{error: message})
1053 end
1054 end
1055
1056 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1057 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1058 {_, true} <- {:followed, follower.id != followed.id},
1059 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1060 conn
1061 |> put_view(AccountView)
1062 |> render("relationship.json", %{user: follower, target: followed})
1063 else
1064 {:followed, _} ->
1065 {:error, :not_found}
1066
1067 error ->
1068 error
1069 end
1070 end
1071
1072 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1073 with %User{} = muted <- User.get_cached_by_id(id),
1074 {:ok, muter} <- User.mute(muter, muted) do
1075 conn
1076 |> put_view(AccountView)
1077 |> render("relationship.json", %{user: muter, target: muted})
1078 else
1079 {:error, message} ->
1080 conn
1081 |> put_status(:forbidden)
1082 |> json(%{error: message})
1083 end
1084 end
1085
1086 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1087 with %User{} = muted <- User.get_cached_by_id(id),
1088 {:ok, muter} <- User.unmute(muter, muted) do
1089 conn
1090 |> put_view(AccountView)
1091 |> render("relationship.json", %{user: muter, target: muted})
1092 else
1093 {:error, message} ->
1094 conn
1095 |> put_status(:forbidden)
1096 |> json(%{error: message})
1097 end
1098 end
1099
1100 def mutes(%{assigns: %{user: user}} = conn, _) do
1101 with muted_accounts <- User.muted_users(user) do
1102 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1103 json(conn, res)
1104 end
1105 end
1106
1107 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1108 with %User{} = blocked <- User.get_cached_by_id(id),
1109 {:ok, blocker} <- User.block(blocker, blocked),
1110 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1111 conn
1112 |> put_view(AccountView)
1113 |> render("relationship.json", %{user: blocker, target: blocked})
1114 else
1115 {:error, message} ->
1116 conn
1117 |> put_status(:forbidden)
1118 |> json(%{error: message})
1119 end
1120 end
1121
1122 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1123 with %User{} = blocked <- User.get_cached_by_id(id),
1124 {:ok, blocker} <- User.unblock(blocker, blocked),
1125 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1126 conn
1127 |> put_view(AccountView)
1128 |> render("relationship.json", %{user: blocker, target: blocked})
1129 else
1130 {:error, message} ->
1131 conn
1132 |> put_status(:forbidden)
1133 |> json(%{error: message})
1134 end
1135 end
1136
1137 def blocks(%{assigns: %{user: user}} = conn, _) do
1138 with blocked_accounts <- User.blocked_users(user) do
1139 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1140 json(conn, res)
1141 end
1142 end
1143
1144 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1145 json(conn, info.domain_blocks || [])
1146 end
1147
1148 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1149 User.block_domain(blocker, domain)
1150 json(conn, %{})
1151 end
1152
1153 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1154 User.unblock_domain(blocker, domain)
1155 json(conn, %{})
1156 end
1157
1158 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1159 with %User{} = subscription_target <- User.get_cached_by_id(id),
1160 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1161 conn
1162 |> put_view(AccountView)
1163 |> render("relationship.json", %{user: user, target: subscription_target})
1164 else
1165 {:error, message} ->
1166 conn
1167 |> put_status(:forbidden)
1168 |> json(%{error: message})
1169 end
1170 end
1171
1172 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1173 with %User{} = subscription_target <- User.get_cached_by_id(id),
1174 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1175 conn
1176 |> put_view(AccountView)
1177 |> render("relationship.json", %{user: user, target: subscription_target})
1178 else
1179 {:error, message} ->
1180 conn
1181 |> put_status(:forbidden)
1182 |> json(%{error: message})
1183 end
1184 end
1185
1186 def favourites(%{assigns: %{user: user}} = conn, params) do
1187 params =
1188 params
1189 |> Map.put("type", "Create")
1190 |> Map.put("favorited_by", user.ap_id)
1191 |> Map.put("blocking_user", user)
1192
1193 activities =
1194 ActivityPub.fetch_activities([], params)
1195 |> Enum.reverse()
1196
1197 conn
1198 |> add_link_headers(:favourites, activities)
1199 |> put_view(StatusView)
1200 |> render("index.json", %{activities: activities, for: user, as: :activity})
1201 end
1202
1203 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1204 with %User{} = user <- User.get_by_id(id),
1205 false <- user.info.hide_favorites do
1206 params =
1207 params
1208 |> Map.put("type", "Create")
1209 |> Map.put("favorited_by", user.ap_id)
1210 |> Map.put("blocking_user", for_user)
1211
1212 recipients =
1213 if for_user do
1214 ["https://www.w3.org/ns/activitystreams#Public"] ++
1215 [for_user.ap_id | for_user.following]
1216 else
1217 ["https://www.w3.org/ns/activitystreams#Public"]
1218 end
1219
1220 activities =
1221 recipients
1222 |> ActivityPub.fetch_activities(params)
1223 |> Enum.reverse()
1224
1225 conn
1226 |> add_link_headers(:favourites, activities)
1227 |> put_view(StatusView)
1228 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1229 else
1230 nil -> {:error, :not_found}
1231 true -> render_error(conn, :forbidden, "Can't get favorites")
1232 end
1233 end
1234
1235 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1236 user = User.get_cached_by_id(user.id)
1237
1238 bookmarks =
1239 Bookmark.for_user_query(user.id)
1240 |> Pagination.fetch_paginated(params)
1241
1242 activities =
1243 bookmarks
1244 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1245
1246 conn
1247 |> add_link_headers(:bookmarks, bookmarks)
1248 |> put_view(StatusView)
1249 |> render("index.json", %{activities: activities, for: user, as: :activity})
1250 end
1251
1252 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1253 lists = Pleroma.List.for_user(user, opts)
1254 res = ListView.render("lists.json", lists: lists)
1255 json(conn, res)
1256 end
1257
1258 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1259 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1260 res = ListView.render("list.json", list: list)
1261 json(conn, res)
1262 else
1263 _e -> render_error(conn, :not_found, "Record not found")
1264 end
1265 end
1266
1267 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1268 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1269 res = ListView.render("lists.json", lists: lists)
1270 json(conn, res)
1271 end
1272
1273 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1274 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1275 {:ok, _list} <- Pleroma.List.delete(list) do
1276 json(conn, %{})
1277 else
1278 _e ->
1279 json(conn, dgettext("errors", "error"))
1280 end
1281 end
1282
1283 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1284 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1285 res = ListView.render("list.json", list: list)
1286 json(conn, res)
1287 end
1288 end
1289
1290 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1291 accounts
1292 |> Enum.each(fn account_id ->
1293 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1294 %User{} = followed <- User.get_cached_by_id(account_id) do
1295 Pleroma.List.follow(list, followed)
1296 end
1297 end)
1298
1299 json(conn, %{})
1300 end
1301
1302 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1303 accounts
1304 |> Enum.each(fn account_id ->
1305 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1306 %User{} = followed <- User.get_cached_by_id(account_id) do
1307 Pleroma.List.unfollow(list, followed)
1308 end
1309 end)
1310
1311 json(conn, %{})
1312 end
1313
1314 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1315 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1316 {:ok, users} = Pleroma.List.get_following(list) do
1317 conn
1318 |> put_view(AccountView)
1319 |> render("accounts.json", %{for: user, users: users, as: :user})
1320 end
1321 end
1322
1323 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1324 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1325 {:ok, list} <- Pleroma.List.rename(list, title) do
1326 res = ListView.render("list.json", list: list)
1327 json(conn, res)
1328 else
1329 _e ->
1330 json(conn, dgettext("errors", "error"))
1331 end
1332 end
1333
1334 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1335 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1336 params =
1337 params
1338 |> Map.put("type", "Create")
1339 |> Map.put("blocking_user", user)
1340 |> Map.put("muting_user", user)
1341
1342 # we must filter the following list for the user to avoid leaking statuses the user
1343 # does not actually have permission to see (for more info, peruse security issue #270).
1344 activities =
1345 following
1346 |> Enum.filter(fn x -> x in user.following end)
1347 |> ActivityPub.fetch_activities_bounded(following, params)
1348 |> Enum.reverse()
1349
1350 conn
1351 |> put_view(StatusView)
1352 |> render("index.json", %{activities: activities, for: user, as: :activity})
1353 else
1354 _e -> render_error(conn, :forbidden, "Error.")
1355 end
1356 end
1357
1358 def index(%{assigns: %{user: user}} = conn, _params) do
1359 token = get_session(conn, :oauth_token)
1360
1361 if user && token do
1362 mastodon_emoji = mastodonized_emoji()
1363
1364 limit = Config.get([:instance, :limit])
1365
1366 accounts =
1367 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1368
1369 initial_state =
1370 %{
1371 meta: %{
1372 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1373 access_token: token,
1374 locale: "en",
1375 domain: Pleroma.Web.Endpoint.host(),
1376 admin: "1",
1377 me: "#{user.id}",
1378 unfollow_modal: false,
1379 boost_modal: false,
1380 delete_modal: true,
1381 auto_play_gif: false,
1382 display_sensitive_media: false,
1383 reduce_motion: false,
1384 max_toot_chars: limit,
1385 mascot: User.get_mascot(user)["url"]
1386 },
1387 poll_limits: Config.get([:instance, :poll_limits]),
1388 rights: %{
1389 delete_others_notice: present?(user.info.is_moderator),
1390 admin: present?(user.info.is_admin)
1391 },
1392 compose: %{
1393 me: "#{user.id}",
1394 default_privacy: user.info.default_scope,
1395 default_sensitive: false,
1396 allow_content_types: Config.get([:instance, :allowed_post_formats])
1397 },
1398 media_attachments: %{
1399 accept_content_types: [
1400 ".jpg",
1401 ".jpeg",
1402 ".png",
1403 ".gif",
1404 ".webm",
1405 ".mp4",
1406 ".m4v",
1407 "image\/jpeg",
1408 "image\/png",
1409 "image\/gif",
1410 "video\/webm",
1411 "video\/mp4"
1412 ]
1413 },
1414 settings:
1415 user.info.settings ||
1416 %{
1417 onboarded: true,
1418 home: %{
1419 shows: %{
1420 reblog: true,
1421 reply: true
1422 }
1423 },
1424 notifications: %{
1425 alerts: %{
1426 follow: true,
1427 favourite: true,
1428 reblog: true,
1429 mention: true
1430 },
1431 shows: %{
1432 follow: true,
1433 favourite: true,
1434 reblog: true,
1435 mention: true
1436 },
1437 sounds: %{
1438 follow: true,
1439 favourite: true,
1440 reblog: true,
1441 mention: true
1442 }
1443 }
1444 },
1445 push_subscription: nil,
1446 accounts: accounts,
1447 custom_emojis: mastodon_emoji,
1448 char_limit: limit
1449 }
1450 |> Jason.encode!()
1451
1452 conn
1453 |> put_layout(false)
1454 |> put_view(MastodonView)
1455 |> render("index.html", %{initial_state: initial_state})
1456 else
1457 conn
1458 |> put_session(:return_to, conn.request_path)
1459 |> redirect(to: "/web/login")
1460 end
1461 end
1462
1463 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1464 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1465
1466 with changeset <- Ecto.Changeset.change(user),
1467 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1468 {:ok, _user} <- User.update_and_set_cache(changeset) do
1469 json(conn, %{})
1470 else
1471 e ->
1472 conn
1473 |> put_status(:internal_server_error)
1474 |> json(%{error: inspect(e)})
1475 end
1476 end
1477
1478 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1479 redirect(conn, to: local_mastodon_root_path(conn))
1480 end
1481
1482 @doc "Local Mastodon FE login init action"
1483 def login(conn, %{"code" => auth_token}) do
1484 with {:ok, app} <- get_or_make_app(),
1485 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1486 {:ok, token} <- Token.exchange_token(app, auth) do
1487 conn
1488 |> put_session(:oauth_token, token.token)
1489 |> redirect(to: local_mastodon_root_path(conn))
1490 end
1491 end
1492
1493 @doc "Local Mastodon FE callback action"
1494 def login(conn, _) do
1495 with {:ok, app} <- get_or_make_app() do
1496 path =
1497 o_auth_path(
1498 conn,
1499 :authorize,
1500 response_type: "code",
1501 client_id: app.client_id,
1502 redirect_uri: ".",
1503 scope: Enum.join(app.scopes, " ")
1504 )
1505
1506 redirect(conn, to: path)
1507 end
1508 end
1509
1510 defp local_mastodon_root_path(conn) do
1511 case get_session(conn, :return_to) do
1512 nil ->
1513 mastodon_api_path(conn, :index, ["getting-started"])
1514
1515 return_to ->
1516 delete_session(conn, :return_to)
1517 return_to
1518 end
1519 end
1520
1521 defp get_or_make_app do
1522 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1523 scopes = ["read", "write", "follow", "push"]
1524
1525 with %App{} = app <- Repo.get_by(App, find_attrs) do
1526 {:ok, app} =
1527 if app.scopes == scopes do
1528 {:ok, app}
1529 else
1530 app
1531 |> Ecto.Changeset.change(%{scopes: scopes})
1532 |> Repo.update()
1533 end
1534
1535 {:ok, app}
1536 else
1537 _e ->
1538 cs =
1539 App.register_changeset(
1540 %App{},
1541 Map.put(find_attrs, :scopes, scopes)
1542 )
1543
1544 Repo.insert(cs)
1545 end
1546 end
1547
1548 def logout(conn, _) do
1549 conn
1550 |> clear_session
1551 |> redirect(to: "/")
1552 end
1553
1554 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1555 Logger.debug("Unimplemented, returning unmodified relationship")
1556
1557 with %User{} = target <- User.get_cached_by_id(id) do
1558 conn
1559 |> put_view(AccountView)
1560 |> render("relationship.json", %{user: user, target: target})
1561 end
1562 end
1563
1564 def empty_array(conn, _) do
1565 Logger.debug("Unimplemented, returning an empty array")
1566 json(conn, [])
1567 end
1568
1569 def empty_object(conn, _) do
1570 Logger.debug("Unimplemented, returning an empty object")
1571 json(conn, %{})
1572 end
1573
1574 def get_filters(%{assigns: %{user: user}} = conn, _) do
1575 filters = Filter.get_filters(user)
1576 res = FilterView.render("filters.json", filters: filters)
1577 json(conn, res)
1578 end
1579
1580 def create_filter(
1581 %{assigns: %{user: user}} = conn,
1582 %{"phrase" => phrase, "context" => context} = params
1583 ) do
1584 query = %Filter{
1585 user_id: user.id,
1586 phrase: phrase,
1587 context: context,
1588 hide: Map.get(params, "irreversible", false),
1589 whole_word: Map.get(params, "boolean", true)
1590 # expires_at
1591 }
1592
1593 {:ok, response} = Filter.create(query)
1594 res = FilterView.render("filter.json", filter: response)
1595 json(conn, res)
1596 end
1597
1598 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1599 filter = Filter.get(filter_id, user)
1600 res = FilterView.render("filter.json", filter: filter)
1601 json(conn, res)
1602 end
1603
1604 def update_filter(
1605 %{assigns: %{user: user}} = conn,
1606 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1607 ) do
1608 query = %Filter{
1609 user_id: user.id,
1610 filter_id: filter_id,
1611 phrase: phrase,
1612 context: context,
1613 hide: Map.get(params, "irreversible", nil),
1614 whole_word: Map.get(params, "boolean", true)
1615 # expires_at
1616 }
1617
1618 {:ok, response} = Filter.update(query)
1619 res = FilterView.render("filter.json", filter: response)
1620 json(conn, res)
1621 end
1622
1623 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1624 query = %Filter{
1625 user_id: user.id,
1626 filter_id: filter_id
1627 }
1628
1629 {:ok, _} = Filter.delete(query)
1630 json(conn, %{})
1631 end
1632
1633 # fallback action
1634 #
1635 def errors(conn, {:error, %Changeset{} = changeset}) do
1636 error_message =
1637 changeset
1638 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1639 |> Enum.map_join(", ", fn {_k, v} -> v end)
1640
1641 conn
1642 |> put_status(:unprocessable_entity)
1643 |> json(%{error: error_message})
1644 end
1645
1646 def errors(conn, {:error, :not_found}) do
1647 render_error(conn, :not_found, "Record not found")
1648 end
1649
1650 def errors(conn, _) do
1651 conn
1652 |> put_status(:internal_server_error)
1653 |> json(dgettext("errors", "Something went wrong"))
1654 end
1655
1656 def suggestions(%{assigns: %{user: user}} = conn, _) do
1657 suggestions = Config.get(:suggestions)
1658
1659 if Keyword.get(suggestions, :enabled, false) do
1660 api = Keyword.get(suggestions, :third_party_engine, "")
1661 timeout = Keyword.get(suggestions, :timeout, 5000)
1662 limit = Keyword.get(suggestions, :limit, 23)
1663
1664 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1665
1666 user = user.nickname
1667
1668 url =
1669 api
1670 |> String.replace("{{host}}", host)
1671 |> String.replace("{{user}}", user)
1672
1673 with {:ok, %{status: 200, body: body}} <-
1674 HTTP.get(
1675 url,
1676 [],
1677 adapter: [
1678 recv_timeout: timeout,
1679 pool: :default
1680 ]
1681 ),
1682 {:ok, data} <- Jason.decode(body) do
1683 data =
1684 data
1685 |> Enum.slice(0, limit)
1686 |> Enum.map(fn x ->
1687 Map.put(
1688 x,
1689 "id",
1690 case User.get_or_fetch(x["acct"]) do
1691 {:ok, %User{id: id}} -> id
1692 _ -> 0
1693 end
1694 )
1695 end)
1696 |> Enum.map(fn x ->
1697 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1698 end)
1699 |> Enum.map(fn x ->
1700 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1701 end)
1702
1703 conn
1704 |> json(data)
1705 else
1706 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1707 end
1708 else
1709 json(conn, [])
1710 end
1711 end
1712
1713 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1714 with %Activity{} = activity <- Activity.get_by_id(status_id),
1715 true <- Visibility.visible_for_user?(activity, user) do
1716 data =
1717 StatusView.render(
1718 "card.json",
1719 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1720 )
1721
1722 json(conn, data)
1723 else
1724 _e ->
1725 %{}
1726 end
1727 end
1728
1729 def reports(%{assigns: %{user: user}} = conn, params) do
1730 case CommonAPI.report(user, params) do
1731 {:ok, activity} ->
1732 conn
1733 |> put_view(ReportView)
1734 |> try_render("report.json", %{activity: activity})
1735
1736 {:error, err} ->
1737 conn
1738 |> put_status(:bad_request)
1739 |> json(%{error: err})
1740 end
1741 end
1742
1743 def account_register(
1744 %{assigns: %{app: app}} = conn,
1745 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1746 ) do
1747 params =
1748 params
1749 |> Map.take([
1750 "email",
1751 "captcha_solution",
1752 "captcha_token",
1753 "captcha_answer_data",
1754 "token",
1755 "password"
1756 ])
1757 |> Map.put("nickname", nickname)
1758 |> Map.put("fullname", params["fullname"] || nickname)
1759 |> Map.put("bio", params["bio"] || "")
1760 |> Map.put("confirm", params["password"])
1761
1762 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1763 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1764 json(conn, %{
1765 token_type: "Bearer",
1766 access_token: token.token,
1767 scope: app.scopes,
1768 created_at: Token.Utils.format_created_at(token)
1769 })
1770 else
1771 {:error, errors} ->
1772 conn
1773 |> put_status(:bad_request)
1774 |> json(errors)
1775 end
1776 end
1777
1778 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1779 render_error(conn, :bad_request, "Missing parameters")
1780 end
1781
1782 def account_register(conn, _) do
1783 render_error(conn, :forbidden, "Invalid credentials")
1784 end
1785
1786 def conversations(%{assigns: %{user: user}} = conn, params) do
1787 participations = Participation.for_user_with_last_activity_id(user, params)
1788
1789 conversations =
1790 Enum.map(participations, fn participation ->
1791 ConversationView.render("participation.json", %{participation: participation, user: user})
1792 end)
1793
1794 conn
1795 |> add_link_headers(:conversations, participations)
1796 |> json(conversations)
1797 end
1798
1799 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1800 with %Participation{} = participation <-
1801 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1802 {:ok, participation} <- Participation.mark_as_read(participation) do
1803 participation_view =
1804 ConversationView.render("participation.json", %{participation: participation, user: user})
1805
1806 conn
1807 |> json(participation_view)
1808 end
1809 end
1810
1811 def try_render(conn, target, params)
1812 when is_binary(target) do
1813 case render(conn, target, params) do
1814 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1815 res -> res
1816 end
1817 end
1818
1819 def try_render(conn, _, _) do
1820 render_error(conn, :not_implemented, "Can't display this activity")
1821 end
1822
1823 defp present?(nil), do: false
1824 defp present?(false), do: false
1825 defp present?(_), do: true
1826 end