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