Add API endpoints for a custom user mascot
[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.Notification
15 alias Pleroma.Object
16 alias Pleroma.Object.Fetcher
17 alias Pleroma.Pagination
18 alias Pleroma.Repo
19 alias Pleroma.ScheduledActivity
20 alias Pleroma.Stats
21 alias Pleroma.User
22 alias Pleroma.Web
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
43
44 alias Pleroma.Web.ControllerHelper
45 import Ecto.Query
46
47 require Logger
48
49 plug(
50 Pleroma.Plugs.RateLimitPlug,
51 %{
52 max_requests: Config.get([:app_account_creation, :max_requests]),
53 interval: Config.get([:app_account_creation, :interval])
54 }
55 when action in [:account_register]
56 )
57
58 @httpoison Application.get_env(:pleroma, :httpoison)
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_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 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
733 end
734 end
735 end
736
737 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
738 %{info: %{mascot: mascot}} = user
739
740 conn
741 |> json(mascot)
742 end
743
744 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
745 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
746 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
747 q = from(u in User, where: u.ap_id in ^likes)
748 users = Repo.all(q)
749
750 conn
751 |> put_view(AccountView)
752 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
753 else
754 _ -> json(conn, [])
755 end
756 end
757
758 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
759 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
760 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
761 q = from(u in User, where: u.ap_id in ^announces)
762 users = Repo.all(q)
763
764 conn
765 |> put_view(AccountView)
766 |> render("accounts.json", %{for: user, users: users, as: :user})
767 else
768 _ -> json(conn, [])
769 end
770 end
771
772 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
773 local_only = params["local"] in [true, "True", "true", "1"]
774
775 tags =
776 [params["tag"], params["any"]]
777 |> List.flatten()
778 |> Enum.uniq()
779 |> Enum.filter(& &1)
780 |> Enum.map(&String.downcase(&1))
781
782 tag_all =
783 params["all"] ||
784 []
785 |> Enum.map(&String.downcase(&1))
786
787 tag_reject =
788 params["none"] ||
789 []
790 |> Enum.map(&String.downcase(&1))
791
792 activities =
793 params
794 |> Map.put("type", "Create")
795 |> Map.put("local_only", local_only)
796 |> Map.put("blocking_user", user)
797 |> Map.put("muting_user", user)
798 |> Map.put("tag", tags)
799 |> Map.put("tag_all", tag_all)
800 |> Map.put("tag_reject", tag_reject)
801 |> ActivityPub.fetch_public_activities()
802 |> Enum.reverse()
803
804 conn
805 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
806 |> put_view(StatusView)
807 |> render("index.json", %{activities: activities, for: user, as: :activity})
808 end
809
810 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
811 with %User{} = user <- User.get_cached_by_id(id),
812 followers <- MastodonAPI.get_followers(user, params) do
813 followers =
814 cond do
815 for_user && user.id == for_user.id -> followers
816 user.info.hide_followers -> []
817 true -> followers
818 end
819
820 conn
821 |> add_link_headers(:followers, followers, user)
822 |> put_view(AccountView)
823 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
824 end
825 end
826
827 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
828 with %User{} = user <- User.get_cached_by_id(id),
829 followers <- MastodonAPI.get_friends(user, params) do
830 followers =
831 cond do
832 for_user && user.id == for_user.id -> followers
833 user.info.hide_follows -> []
834 true -> followers
835 end
836
837 conn
838 |> add_link_headers(:following, followers, user)
839 |> put_view(AccountView)
840 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
841 end
842 end
843
844 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
845 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
846 conn
847 |> put_view(AccountView)
848 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
849 end
850 end
851
852 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
853 with %User{} = follower <- User.get_cached_by_id(id),
854 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
855 conn
856 |> put_view(AccountView)
857 |> render("relationship.json", %{user: followed, target: follower})
858 else
859 {:error, message} ->
860 conn
861 |> put_resp_content_type("application/json")
862 |> send_resp(403, Jason.encode!(%{"error" => message}))
863 end
864 end
865
866 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
867 with %User{} = follower <- User.get_cached_by_id(id),
868 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
869 conn
870 |> put_view(AccountView)
871 |> render("relationship.json", %{user: followed, target: follower})
872 else
873 {:error, message} ->
874 conn
875 |> put_resp_content_type("application/json")
876 |> send_resp(403, Jason.encode!(%{"error" => message}))
877 end
878 end
879
880 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
881 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
882 {_, true} <- {:followed, follower.id != followed.id},
883 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
884 conn
885 |> put_view(AccountView)
886 |> render("relationship.json", %{user: follower, target: followed})
887 else
888 {:followed, _} ->
889 {:error, :not_found}
890
891 {:error, message} ->
892 conn
893 |> put_resp_content_type("application/json")
894 |> send_resp(403, Jason.encode!(%{"error" => message}))
895 end
896 end
897
898 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
899 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
900 {_, true} <- {:followed, follower.id != followed.id},
901 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
902 conn
903 |> put_view(AccountView)
904 |> render("account.json", %{user: followed, for: follower})
905 else
906 {:followed, _} ->
907 {:error, :not_found}
908
909 {:error, message} ->
910 conn
911 |> put_resp_content_type("application/json")
912 |> send_resp(403, Jason.encode!(%{"error" => message}))
913 end
914 end
915
916 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
917 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
918 {_, true} <- {:followed, follower.id != followed.id},
919 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
920 conn
921 |> put_view(AccountView)
922 |> render("relationship.json", %{user: follower, target: followed})
923 else
924 {:followed, _} ->
925 {:error, :not_found}
926
927 error ->
928 error
929 end
930 end
931
932 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
933 with %User{} = muted <- User.get_cached_by_id(id),
934 {:ok, muter} <- User.mute(muter, muted) do
935 conn
936 |> put_view(AccountView)
937 |> render("relationship.json", %{user: muter, target: muted})
938 else
939 {:error, message} ->
940 conn
941 |> put_resp_content_type("application/json")
942 |> send_resp(403, Jason.encode!(%{"error" => message}))
943 end
944 end
945
946 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
947 with %User{} = muted <- User.get_cached_by_id(id),
948 {:ok, muter} <- User.unmute(muter, muted) do
949 conn
950 |> put_view(AccountView)
951 |> render("relationship.json", %{user: muter, target: muted})
952 else
953 {:error, message} ->
954 conn
955 |> put_resp_content_type("application/json")
956 |> send_resp(403, Jason.encode!(%{"error" => message}))
957 end
958 end
959
960 def mutes(%{assigns: %{user: user}} = conn, _) do
961 with muted_accounts <- User.muted_users(user) do
962 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
963 json(conn, res)
964 end
965 end
966
967 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
968 with %User{} = blocked <- User.get_cached_by_id(id),
969 {:ok, blocker} <- User.block(blocker, blocked),
970 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
971 conn
972 |> put_view(AccountView)
973 |> render("relationship.json", %{user: blocker, target: blocked})
974 else
975 {:error, message} ->
976 conn
977 |> put_resp_content_type("application/json")
978 |> send_resp(403, Jason.encode!(%{"error" => message}))
979 end
980 end
981
982 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
983 with %User{} = blocked <- User.get_cached_by_id(id),
984 {:ok, blocker} <- User.unblock(blocker, blocked),
985 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
986 conn
987 |> put_view(AccountView)
988 |> render("relationship.json", %{user: blocker, target: blocked})
989 else
990 {:error, message} ->
991 conn
992 |> put_resp_content_type("application/json")
993 |> send_resp(403, Jason.encode!(%{"error" => message}))
994 end
995 end
996
997 def blocks(%{assigns: %{user: user}} = conn, _) do
998 with blocked_accounts <- User.blocked_users(user) do
999 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1000 json(conn, res)
1001 end
1002 end
1003
1004 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1005 json(conn, info.domain_blocks || [])
1006 end
1007
1008 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1009 User.block_domain(blocker, domain)
1010 json(conn, %{})
1011 end
1012
1013 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1014 User.unblock_domain(blocker, domain)
1015 json(conn, %{})
1016 end
1017
1018 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1019 with %User{} = subscription_target <- User.get_cached_by_id(id),
1020 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1021 conn
1022 |> put_view(AccountView)
1023 |> render("relationship.json", %{user: user, target: subscription_target})
1024 else
1025 {:error, message} ->
1026 conn
1027 |> put_resp_content_type("application/json")
1028 |> send_resp(403, Jason.encode!(%{"error" => message}))
1029 end
1030 end
1031
1032 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1033 with %User{} = subscription_target <- User.get_cached_by_id(id),
1034 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1035 conn
1036 |> put_view(AccountView)
1037 |> render("relationship.json", %{user: user, target: subscription_target})
1038 else
1039 {:error, message} ->
1040 conn
1041 |> put_resp_content_type("application/json")
1042 |> send_resp(403, Jason.encode!(%{"error" => message}))
1043 end
1044 end
1045
1046 def status_search_query_with_gin(q, query) do
1047 from([a, o] in q,
1048 where:
1049 fragment(
1050 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1051 o.data,
1052 ^query
1053 ),
1054 order_by: [desc: :id]
1055 )
1056 end
1057
1058 def status_search_query_with_rum(q, query) do
1059 from([a, o] in q,
1060 where:
1061 fragment(
1062 "? @@ plainto_tsquery('english', ?)",
1063 o.fts_content,
1064 ^query
1065 ),
1066 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1067 )
1068 end
1069
1070 def status_search(user, query) do
1071 fetched =
1072 if Regex.match?(~r/https?:/, query) do
1073 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1074 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1075 true <- Visibility.visible_for_user?(activity, user) do
1076 [activity]
1077 else
1078 _e -> []
1079 end
1080 end || []
1081
1082 q =
1083 from([a, o] in Activity.with_preloaded_object(Activity),
1084 where: fragment("?->>'type' = 'Create'", a.data),
1085 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1086 limit: 20
1087 )
1088
1089 q =
1090 if Pleroma.Config.get([:database, :rum_enabled]) do
1091 status_search_query_with_rum(q, query)
1092 else
1093 status_search_query_with_gin(q, query)
1094 end
1095
1096 Repo.all(q) ++ fetched
1097 end
1098
1099 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1100 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1101
1102 statuses = status_search(user, query)
1103
1104 tags_path = Web.base_url() <> "/tag/"
1105
1106 tags =
1107 query
1108 |> String.split()
1109 |> Enum.uniq()
1110 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1111 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1112 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1113
1114 res = %{
1115 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1116 "statuses" =>
1117 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1118 "hashtags" => tags
1119 }
1120
1121 json(conn, res)
1122 end
1123
1124 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1125 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1126
1127 statuses = status_search(user, query)
1128
1129 tags =
1130 query
1131 |> String.split()
1132 |> Enum.uniq()
1133 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1134 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1135
1136 res = %{
1137 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1138 "statuses" =>
1139 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1140 "hashtags" => tags
1141 }
1142
1143 json(conn, res)
1144 end
1145
1146 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1147 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1148
1149 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1150
1151 json(conn, res)
1152 end
1153
1154 def favourites(%{assigns: %{user: user}} = conn, params) do
1155 params =
1156 params
1157 |> Map.put("type", "Create")
1158 |> Map.put("favorited_by", user.ap_id)
1159 |> Map.put("blocking_user", user)
1160
1161 activities =
1162 ActivityPub.fetch_activities([], params)
1163 |> Enum.reverse()
1164
1165 conn
1166 |> add_link_headers(:favourites, activities)
1167 |> put_view(StatusView)
1168 |> render("index.json", %{activities: activities, for: user, as: :activity})
1169 end
1170
1171 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1172 with %User{} = user <- User.get_by_id(id),
1173 false <- user.info.hide_favorites do
1174 params =
1175 params
1176 |> Map.put("type", "Create")
1177 |> Map.put("favorited_by", user.ap_id)
1178 |> Map.put("blocking_user", for_user)
1179
1180 recipients =
1181 if for_user do
1182 ["https://www.w3.org/ns/activitystreams#Public"] ++
1183 [for_user.ap_id | for_user.following]
1184 else
1185 ["https://www.w3.org/ns/activitystreams#Public"]
1186 end
1187
1188 activities =
1189 recipients
1190 |> ActivityPub.fetch_activities(params)
1191 |> Enum.reverse()
1192
1193 conn
1194 |> add_link_headers(:favourites, activities)
1195 |> put_view(StatusView)
1196 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1197 else
1198 nil ->
1199 {:error, :not_found}
1200
1201 true ->
1202 conn
1203 |> put_status(403)
1204 |> json(%{error: "Can't get favorites"})
1205 end
1206 end
1207
1208 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1209 user = User.get_cached_by_id(user.id)
1210
1211 bookmarks =
1212 Bookmark.for_user_query(user.id)
1213 |> Pagination.fetch_paginated(params)
1214
1215 activities =
1216 bookmarks
1217 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1218
1219 conn
1220 |> add_link_headers(:bookmarks, bookmarks)
1221 |> put_view(StatusView)
1222 |> render("index.json", %{activities: activities, for: user, as: :activity})
1223 end
1224
1225 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1226 lists = Pleroma.List.for_user(user, opts)
1227 res = ListView.render("lists.json", lists: lists)
1228 json(conn, res)
1229 end
1230
1231 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1232 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1233 res = ListView.render("list.json", list: list)
1234 json(conn, res)
1235 else
1236 _e ->
1237 conn
1238 |> put_status(404)
1239 |> json(%{error: "Record not found"})
1240 end
1241 end
1242
1243 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1244 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1245 res = ListView.render("lists.json", lists: lists)
1246 json(conn, res)
1247 end
1248
1249 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1250 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1251 {:ok, _list} <- Pleroma.List.delete(list) do
1252 json(conn, %{})
1253 else
1254 _e ->
1255 json(conn, "error")
1256 end
1257 end
1258
1259 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1260 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1261 res = ListView.render("list.json", list: list)
1262 json(conn, res)
1263 end
1264 end
1265
1266 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1267 accounts
1268 |> Enum.each(fn account_id ->
1269 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1270 %User{} = followed <- User.get_cached_by_id(account_id) do
1271 Pleroma.List.follow(list, followed)
1272 end
1273 end)
1274
1275 json(conn, %{})
1276 end
1277
1278 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1279 accounts
1280 |> Enum.each(fn account_id ->
1281 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1282 %User{} = followed <- User.get_cached_by_id(account_id) do
1283 Pleroma.List.unfollow(list, followed)
1284 end
1285 end)
1286
1287 json(conn, %{})
1288 end
1289
1290 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1291 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1292 {:ok, users} = Pleroma.List.get_following(list) do
1293 conn
1294 |> put_view(AccountView)
1295 |> render("accounts.json", %{for: user, users: users, as: :user})
1296 end
1297 end
1298
1299 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1300 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1301 {:ok, list} <- Pleroma.List.rename(list, title) do
1302 res = ListView.render("list.json", list: list)
1303 json(conn, res)
1304 else
1305 _e ->
1306 json(conn, "error")
1307 end
1308 end
1309
1310 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1311 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1312 params =
1313 params
1314 |> Map.put("type", "Create")
1315 |> Map.put("blocking_user", user)
1316 |> Map.put("muting_user", user)
1317
1318 # we must filter the following list for the user to avoid leaking statuses the user
1319 # does not actually have permission to see (for more info, peruse security issue #270).
1320 activities =
1321 following
1322 |> Enum.filter(fn x -> x in user.following end)
1323 |> ActivityPub.fetch_activities_bounded(following, params)
1324 |> Enum.reverse()
1325
1326 conn
1327 |> put_view(StatusView)
1328 |> render("index.json", %{activities: activities, for: user, as: :activity})
1329 else
1330 _e ->
1331 conn
1332 |> put_status(403)
1333 |> json(%{error: "Error."})
1334 end
1335 end
1336
1337 def index(%{assigns: %{user: user}} = conn, _params) do
1338 token = get_session(conn, :oauth_token)
1339
1340 if user && token do
1341 mastodon_emoji = mastodonized_emoji()
1342
1343 limit = Config.get([:instance, :limit])
1344
1345 accounts =
1346 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1347
1348 flavour = get_user_flavour(user)
1349
1350 initial_state =
1351 %{
1352 meta: %{
1353 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1354 access_token: token,
1355 locale: "en",
1356 domain: Pleroma.Web.Endpoint.host(),
1357 admin: "1",
1358 me: "#{user.id}",
1359 unfollow_modal: false,
1360 boost_modal: false,
1361 delete_modal: true,
1362 auto_play_gif: false,
1363 display_sensitive_media: false,
1364 reduce_motion: false,
1365 max_toot_chars: limit,
1366 mascot: Map.get(user.info.mascot, "url", "/images/pleroma-fox-tan-smol.png")
1367 },
1368 rights: %{
1369 delete_others_notice: present?(user.info.is_moderator),
1370 admin: present?(user.info.is_admin)
1371 },
1372 compose: %{
1373 me: "#{user.id}",
1374 default_privacy: user.info.default_scope,
1375 default_sensitive: false,
1376 allow_content_types: Config.get([:instance, :allowed_post_formats])
1377 },
1378 media_attachments: %{
1379 accept_content_types: [
1380 ".jpg",
1381 ".jpeg",
1382 ".png",
1383 ".gif",
1384 ".webm",
1385 ".mp4",
1386 ".m4v",
1387 "image\/jpeg",
1388 "image\/png",
1389 "image\/gif",
1390 "video\/webm",
1391 "video\/mp4"
1392 ]
1393 },
1394 settings:
1395 user.info.settings ||
1396 %{
1397 onboarded: true,
1398 home: %{
1399 shows: %{
1400 reblog: true,
1401 reply: true
1402 }
1403 },
1404 notifications: %{
1405 alerts: %{
1406 follow: true,
1407 favourite: true,
1408 reblog: true,
1409 mention: true
1410 },
1411 shows: %{
1412 follow: true,
1413 favourite: true,
1414 reblog: true,
1415 mention: true
1416 },
1417 sounds: %{
1418 follow: true,
1419 favourite: true,
1420 reblog: true,
1421 mention: true
1422 }
1423 }
1424 },
1425 push_subscription: nil,
1426 accounts: accounts,
1427 custom_emojis: mastodon_emoji,
1428 char_limit: limit
1429 }
1430 |> Jason.encode!()
1431
1432 conn
1433 |> put_layout(false)
1434 |> put_view(MastodonView)
1435 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1436 else
1437 conn
1438 |> put_session(:return_to, conn.request_path)
1439 |> redirect(to: "/web/login")
1440 end
1441 end
1442
1443 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1444 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1445
1446 with changeset <- Ecto.Changeset.change(user),
1447 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1448 {:ok, _user} <- User.update_and_set_cache(changeset) do
1449 json(conn, %{})
1450 else
1451 e ->
1452 conn
1453 |> put_resp_content_type("application/json")
1454 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1455 end
1456 end
1457
1458 @supported_flavours ["glitch", "vanilla"]
1459
1460 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1461 when flavour in @supported_flavours do
1462 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1463
1464 with changeset <- Ecto.Changeset.change(user),
1465 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1466 {:ok, user} <- User.update_and_set_cache(changeset),
1467 flavour <- user.info.flavour do
1468 json(conn, flavour)
1469 else
1470 e ->
1471 conn
1472 |> put_resp_content_type("application/json")
1473 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1474 end
1475 end
1476
1477 def set_flavour(conn, _params) do
1478 conn
1479 |> put_status(400)
1480 |> json(%{error: "Unsupported flavour"})
1481 end
1482
1483 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1484 json(conn, get_user_flavour(user))
1485 end
1486
1487 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1488 flavour
1489 end
1490
1491 defp get_user_flavour(_) do
1492 "glitch"
1493 end
1494
1495 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1496 redirect(conn, to: local_mastodon_root_path(conn))
1497 end
1498
1499 @doc "Local Mastodon FE login init action"
1500 def login(conn, %{"code" => auth_token}) do
1501 with {:ok, app} <- get_or_make_app(),
1502 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1503 {:ok, token} <- Token.exchange_token(app, auth) do
1504 conn
1505 |> put_session(:oauth_token, token.token)
1506 |> redirect(to: local_mastodon_root_path(conn))
1507 end
1508 end
1509
1510 @doc "Local Mastodon FE callback action"
1511 def login(conn, _) do
1512 with {:ok, app} <- get_or_make_app() do
1513 path =
1514 o_auth_path(
1515 conn,
1516 :authorize,
1517 response_type: "code",
1518 client_id: app.client_id,
1519 redirect_uri: ".",
1520 scope: Enum.join(app.scopes, " ")
1521 )
1522
1523 redirect(conn, to: path)
1524 end
1525 end
1526
1527 defp local_mastodon_root_path(conn) do
1528 case get_session(conn, :return_to) do
1529 nil ->
1530 mastodon_api_path(conn, :index, ["getting-started"])
1531
1532 return_to ->
1533 delete_session(conn, :return_to)
1534 return_to
1535 end
1536 end
1537
1538 defp get_or_make_app do
1539 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1540 scopes = ["read", "write", "follow", "push"]
1541
1542 with %App{} = app <- Repo.get_by(App, find_attrs) do
1543 {:ok, app} =
1544 if app.scopes == scopes do
1545 {:ok, app}
1546 else
1547 app
1548 |> Ecto.Changeset.change(%{scopes: scopes})
1549 |> Repo.update()
1550 end
1551
1552 {:ok, app}
1553 else
1554 _e ->
1555 cs =
1556 App.register_changeset(
1557 %App{},
1558 Map.put(find_attrs, :scopes, scopes)
1559 )
1560
1561 Repo.insert(cs)
1562 end
1563 end
1564
1565 def logout(conn, _) do
1566 conn
1567 |> clear_session
1568 |> redirect(to: "/")
1569 end
1570
1571 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1572 Logger.debug("Unimplemented, returning unmodified relationship")
1573
1574 with %User{} = target <- User.get_cached_by_id(id) do
1575 conn
1576 |> put_view(AccountView)
1577 |> render("relationship.json", %{user: user, target: target})
1578 end
1579 end
1580
1581 def empty_array(conn, _) do
1582 Logger.debug("Unimplemented, returning an empty array")
1583 json(conn, [])
1584 end
1585
1586 def empty_object(conn, _) do
1587 Logger.debug("Unimplemented, returning an empty object")
1588 json(conn, %{})
1589 end
1590
1591 def get_filters(%{assigns: %{user: user}} = conn, _) do
1592 filters = Filter.get_filters(user)
1593 res = FilterView.render("filters.json", filters: filters)
1594 json(conn, res)
1595 end
1596
1597 def create_filter(
1598 %{assigns: %{user: user}} = conn,
1599 %{"phrase" => phrase, "context" => context} = params
1600 ) do
1601 query = %Filter{
1602 user_id: user.id,
1603 phrase: phrase,
1604 context: context,
1605 hide: Map.get(params, "irreversible", false),
1606 whole_word: Map.get(params, "boolean", true)
1607 # expires_at
1608 }
1609
1610 {:ok, response} = Filter.create(query)
1611 res = FilterView.render("filter.json", filter: response)
1612 json(conn, res)
1613 end
1614
1615 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1616 filter = Filter.get(filter_id, user)
1617 res = FilterView.render("filter.json", filter: filter)
1618 json(conn, res)
1619 end
1620
1621 def update_filter(
1622 %{assigns: %{user: user}} = conn,
1623 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1624 ) do
1625 query = %Filter{
1626 user_id: user.id,
1627 filter_id: filter_id,
1628 phrase: phrase,
1629 context: context,
1630 hide: Map.get(params, "irreversible", nil),
1631 whole_word: Map.get(params, "boolean", true)
1632 # expires_at
1633 }
1634
1635 {:ok, response} = Filter.update(query)
1636 res = FilterView.render("filter.json", filter: response)
1637 json(conn, res)
1638 end
1639
1640 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1641 query = %Filter{
1642 user_id: user.id,
1643 filter_id: filter_id
1644 }
1645
1646 {:ok, _} = Filter.delete(query)
1647 json(conn, %{})
1648 end
1649
1650 # fallback action
1651 #
1652 def errors(conn, {:error, %Changeset{} = changeset}) do
1653 error_message =
1654 changeset
1655 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1656 |> Enum.map_join(", ", fn {_k, v} -> v end)
1657
1658 conn
1659 |> put_status(422)
1660 |> json(%{error: error_message})
1661 end
1662
1663 def errors(conn, {:error, :not_found}) do
1664 conn
1665 |> put_status(404)
1666 |> json(%{error: "Record not found"})
1667 end
1668
1669 def errors(conn, _) do
1670 conn
1671 |> put_status(500)
1672 |> json("Something went wrong")
1673 end
1674
1675 def suggestions(%{assigns: %{user: user}} = conn, _) do
1676 suggestions = Config.get(:suggestions)
1677
1678 if Keyword.get(suggestions, :enabled, false) do
1679 api = Keyword.get(suggestions, :third_party_engine, "")
1680 timeout = Keyword.get(suggestions, :timeout, 5000)
1681 limit = Keyword.get(suggestions, :limit, 23)
1682
1683 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1684
1685 user = user.nickname
1686
1687 url =
1688 api
1689 |> String.replace("{{host}}", host)
1690 |> String.replace("{{user}}", user)
1691
1692 with {:ok, %{status: 200, body: body}} <-
1693 @httpoison.get(
1694 url,
1695 [],
1696 adapter: [
1697 recv_timeout: timeout,
1698 pool: :default
1699 ]
1700 ),
1701 {:ok, data} <- Jason.decode(body) do
1702 data =
1703 data
1704 |> Enum.slice(0, limit)
1705 |> Enum.map(fn x ->
1706 Map.put(
1707 x,
1708 "id",
1709 case User.get_or_fetch(x["acct"]) do
1710 {:ok, %User{id: id}} -> id
1711 _ -> 0
1712 end
1713 )
1714 end)
1715 |> Enum.map(fn x ->
1716 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1717 end)
1718 |> Enum.map(fn x ->
1719 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1720 end)
1721
1722 conn
1723 |> json(data)
1724 else
1725 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1726 end
1727 else
1728 json(conn, [])
1729 end
1730 end
1731
1732 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1733 with %Activity{} = activity <- Activity.get_by_id(status_id),
1734 true <- Visibility.visible_for_user?(activity, user) do
1735 data =
1736 StatusView.render(
1737 "card.json",
1738 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1739 )
1740
1741 json(conn, data)
1742 else
1743 _e ->
1744 %{}
1745 end
1746 end
1747
1748 def reports(%{assigns: %{user: user}} = conn, params) do
1749 case CommonAPI.report(user, params) do
1750 {:ok, activity} ->
1751 conn
1752 |> put_view(ReportView)
1753 |> try_render("report.json", %{activity: activity})
1754
1755 {:error, err} ->
1756 conn
1757 |> put_status(:bad_request)
1758 |> json(%{error: err})
1759 end
1760 end
1761
1762 def account_register(
1763 %{assigns: %{app: app}} = conn,
1764 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1765 ) do
1766 params =
1767 params
1768 |> Map.take([
1769 "email",
1770 "captcha_solution",
1771 "captcha_token",
1772 "captcha_answer_data",
1773 "token",
1774 "password"
1775 ])
1776 |> Map.put("nickname", nickname)
1777 |> Map.put("fullname", params["fullname"] || nickname)
1778 |> Map.put("bio", params["bio"] || "")
1779 |> Map.put("confirm", params["password"])
1780
1781 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1782 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1783 json(conn, %{
1784 token_type: "Bearer",
1785 access_token: token.token,
1786 scope: app.scopes,
1787 created_at: Token.Utils.format_created_at(token)
1788 })
1789 else
1790 {:error, errors} ->
1791 conn
1792 |> put_status(400)
1793 |> json(Jason.encode!(errors))
1794 end
1795 end
1796
1797 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1798 conn
1799 |> put_status(400)
1800 |> json(%{error: "Missing parameters"})
1801 end
1802
1803 def account_register(conn, _) do
1804 conn
1805 |> put_status(403)
1806 |> json(%{error: "Invalid credentials"})
1807 end
1808
1809 def conversations(%{assigns: %{user: user}} = conn, params) do
1810 participations = Participation.for_user_with_last_activity_id(user, params)
1811
1812 conversations =
1813 Enum.map(participations, fn participation ->
1814 ConversationView.render("participation.json", %{participation: participation, user: user})
1815 end)
1816
1817 conn
1818 |> add_link_headers(:conversations, participations)
1819 |> json(conversations)
1820 end
1821
1822 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1823 with %Participation{} = participation <-
1824 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1825 {:ok, participation} <- Participation.mark_as_read(participation) do
1826 participation_view =
1827 ConversationView.render("participation.json", %{participation: participation, user: user})
1828
1829 conn
1830 |> json(participation_view)
1831 end
1832 end
1833
1834 def try_render(conn, target, params)
1835 when is_binary(target) do
1836 res = render(conn, target, params)
1837
1838 if res == nil do
1839 conn
1840 |> put_status(501)
1841 |> json(%{error: "Can't display this activity"})
1842 else
1843 res
1844 end
1845 end
1846
1847 def try_render(conn, _, _) do
1848 conn
1849 |> put_status(501)
1850 |> json(%{error: "Can't display this activity"})
1851 end
1852
1853 defp present?(nil), do: false
1854 defp present?(false), do: false
1855 defp present?(_), do: true
1856 end