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