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