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