526581363eaae8bab4aa6f9accc166320b9fd6ff
[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_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 # Note: vanilla is fully unsupported
1460 @supported_flavours ["glitch"]
1461
1462 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1463 when flavour in @supported_flavours do
1464 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1465
1466 with changeset <- Ecto.Changeset.change(user),
1467 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1468 {:ok, user} <- User.update_and_set_cache(changeset),
1469 flavour <- user.info.flavour do
1470 json(conn, flavour)
1471 else
1472 e ->
1473 conn
1474 |> put_resp_content_type("application/json")
1475 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1476 end
1477 end
1478
1479 def set_flavour(conn, _params) do
1480 conn
1481 |> put_status(400)
1482 |> json(%{error: "Unsupported flavour"})
1483 end
1484
1485 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1486 json(conn, get_user_flavour(user))
1487 end
1488
1489 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1490 flavour
1491 end
1492
1493 defp get_user_flavour(_) do
1494 "glitch"
1495 end
1496
1497 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1498 redirect(conn, to: local_mastodon_root_path(conn))
1499 end
1500
1501 @doc "Local Mastodon FE login init action"
1502 def login(conn, %{"code" => auth_token}) do
1503 with {:ok, app} <- get_or_make_app(),
1504 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1505 {:ok, token} <- Token.exchange_token(app, auth) do
1506 conn
1507 |> put_session(:oauth_token, token.token)
1508 |> redirect(to: local_mastodon_root_path(conn))
1509 end
1510 end
1511
1512 @doc "Local Mastodon FE callback action"
1513 def login(conn, _) do
1514 with {:ok, app} <- get_or_make_app() do
1515 path =
1516 o_auth_path(
1517 conn,
1518 :authorize,
1519 response_type: "code",
1520 client_id: app.client_id,
1521 redirect_uri: ".",
1522 scope: Enum.join(app.scopes, " ")
1523 )
1524
1525 redirect(conn, to: path)
1526 end
1527 end
1528
1529 defp local_mastodon_root_path(conn) do
1530 case get_session(conn, :return_to) do
1531 nil ->
1532 mastodon_api_path(conn, :index, ["getting-started"])
1533
1534 return_to ->
1535 delete_session(conn, :return_to)
1536 return_to
1537 end
1538 end
1539
1540 defp get_or_make_app do
1541 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1542 scopes = ["read", "write", "follow", "push"]
1543
1544 with %App{} = app <- Repo.get_by(App, find_attrs) do
1545 {:ok, app} =
1546 if app.scopes == scopes do
1547 {:ok, app}
1548 else
1549 app
1550 |> Ecto.Changeset.change(%{scopes: scopes})
1551 |> Repo.update()
1552 end
1553
1554 {:ok, app}
1555 else
1556 _e ->
1557 cs =
1558 App.register_changeset(
1559 %App{},
1560 Map.put(find_attrs, :scopes, scopes)
1561 )
1562
1563 Repo.insert(cs)
1564 end
1565 end
1566
1567 def logout(conn, _) do
1568 conn
1569 |> clear_session
1570 |> redirect(to: "/")
1571 end
1572
1573 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1574 Logger.debug("Unimplemented, returning unmodified relationship")
1575
1576 with %User{} = target <- User.get_cached_by_id(id) do
1577 conn
1578 |> put_view(AccountView)
1579 |> render("relationship.json", %{user: user, target: target})
1580 end
1581 end
1582
1583 def empty_array(conn, _) do
1584 Logger.debug("Unimplemented, returning an empty array")
1585 json(conn, [])
1586 end
1587
1588 def empty_object(conn, _) do
1589 Logger.debug("Unimplemented, returning an empty object")
1590 json(conn, %{})
1591 end
1592
1593 def get_filters(%{assigns: %{user: user}} = conn, _) do
1594 filters = Filter.get_filters(user)
1595 res = FilterView.render("filters.json", filters: filters)
1596 json(conn, res)
1597 end
1598
1599 def create_filter(
1600 %{assigns: %{user: user}} = conn,
1601 %{"phrase" => phrase, "context" => context} = params
1602 ) do
1603 query = %Filter{
1604 user_id: user.id,
1605 phrase: phrase,
1606 context: context,
1607 hide: Map.get(params, "irreversible", false),
1608 whole_word: Map.get(params, "boolean", true)
1609 # expires_at
1610 }
1611
1612 {:ok, response} = Filter.create(query)
1613 res = FilterView.render("filter.json", filter: response)
1614 json(conn, res)
1615 end
1616
1617 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1618 filter = Filter.get(filter_id, user)
1619 res = FilterView.render("filter.json", filter: filter)
1620 json(conn, res)
1621 end
1622
1623 def update_filter(
1624 %{assigns: %{user: user}} = conn,
1625 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1626 ) do
1627 query = %Filter{
1628 user_id: user.id,
1629 filter_id: filter_id,
1630 phrase: phrase,
1631 context: context,
1632 hide: Map.get(params, "irreversible", nil),
1633 whole_word: Map.get(params, "boolean", true)
1634 # expires_at
1635 }
1636
1637 {:ok, response} = Filter.update(query)
1638 res = FilterView.render("filter.json", filter: response)
1639 json(conn, res)
1640 end
1641
1642 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1643 query = %Filter{
1644 user_id: user.id,
1645 filter_id: filter_id
1646 }
1647
1648 {:ok, _} = Filter.delete(query)
1649 json(conn, %{})
1650 end
1651
1652 # fallback action
1653 #
1654 def errors(conn, {:error, %Changeset{} = changeset}) do
1655 error_message =
1656 changeset
1657 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1658 |> Enum.map_join(", ", fn {_k, v} -> v end)
1659
1660 conn
1661 |> put_status(422)
1662 |> json(%{error: error_message})
1663 end
1664
1665 def errors(conn, {:error, :not_found}) do
1666 conn
1667 |> put_status(404)
1668 |> json(%{error: "Record not found"})
1669 end
1670
1671 def errors(conn, _) do
1672 conn
1673 |> put_status(500)
1674 |> json("Something went wrong")
1675 end
1676
1677 def suggestions(%{assigns: %{user: user}} = conn, _) do
1678 suggestions = Config.get(:suggestions)
1679
1680 if Keyword.get(suggestions, :enabled, false) do
1681 api = Keyword.get(suggestions, :third_party_engine, "")
1682 timeout = Keyword.get(suggestions, :timeout, 5000)
1683 limit = Keyword.get(suggestions, :limit, 23)
1684
1685 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1686
1687 user = user.nickname
1688
1689 url =
1690 api
1691 |> String.replace("{{host}}", host)
1692 |> String.replace("{{user}}", user)
1693
1694 with {:ok, %{status: 200, body: body}} <-
1695 HTTP.get(
1696 url,
1697 [],
1698 adapter: [
1699 recv_timeout: timeout,
1700 pool: :default
1701 ]
1702 ),
1703 {:ok, data} <- Jason.decode(body) do
1704 data =
1705 data
1706 |> Enum.slice(0, limit)
1707 |> Enum.map(fn x ->
1708 Map.put(
1709 x,
1710 "id",
1711 case User.get_or_fetch(x["acct"]) do
1712 {:ok, %User{id: id}} -> id
1713 _ -> 0
1714 end
1715 )
1716 end)
1717 |> Enum.map(fn x ->
1718 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1719 end)
1720 |> Enum.map(fn x ->
1721 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1722 end)
1723
1724 conn
1725 |> json(data)
1726 else
1727 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1728 end
1729 else
1730 json(conn, [])
1731 end
1732 end
1733
1734 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1735 with %Activity{} = activity <- Activity.get_by_id(status_id),
1736 true <- Visibility.visible_for_user?(activity, user) do
1737 data =
1738 StatusView.render(
1739 "card.json",
1740 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1741 )
1742
1743 json(conn, data)
1744 else
1745 _e ->
1746 %{}
1747 end
1748 end
1749
1750 def reports(%{assigns: %{user: user}} = conn, params) do
1751 case CommonAPI.report(user, params) do
1752 {:ok, activity} ->
1753 conn
1754 |> put_view(ReportView)
1755 |> try_render("report.json", %{activity: activity})
1756
1757 {:error, err} ->
1758 conn
1759 |> put_status(:bad_request)
1760 |> json(%{error: err})
1761 end
1762 end
1763
1764 def account_register(
1765 %{assigns: %{app: app}} = conn,
1766 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1767 ) do
1768 params =
1769 params
1770 |> Map.take([
1771 "email",
1772 "captcha_solution",
1773 "captcha_token",
1774 "captcha_answer_data",
1775 "token",
1776 "password"
1777 ])
1778 |> Map.put("nickname", nickname)
1779 |> Map.put("fullname", params["fullname"] || nickname)
1780 |> Map.put("bio", params["bio"] || "")
1781 |> Map.put("confirm", params["password"])
1782
1783 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1784 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1785 json(conn, %{
1786 token_type: "Bearer",
1787 access_token: token.token,
1788 scope: app.scopes,
1789 created_at: Token.Utils.format_created_at(token)
1790 })
1791 else
1792 {:error, errors} ->
1793 conn
1794 |> put_status(400)
1795 |> json(Jason.encode!(errors))
1796 end
1797 end
1798
1799 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1800 conn
1801 |> put_status(400)
1802 |> json(%{error: "Missing parameters"})
1803 end
1804
1805 def account_register(conn, _) do
1806 conn
1807 |> put_status(403)
1808 |> json(%{error: "Invalid credentials"})
1809 end
1810
1811 def conversations(%{assigns: %{user: user}} = conn, params) do
1812 participations = Participation.for_user_with_last_activity_id(user, params)
1813
1814 conversations =
1815 Enum.map(participations, fn participation ->
1816 ConversationView.render("participation.json", %{participation: participation, user: user})
1817 end)
1818
1819 conn
1820 |> add_link_headers(:conversations, participations)
1821 |> json(conversations)
1822 end
1823
1824 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1825 with %Participation{} = participation <-
1826 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1827 {:ok, participation} <- Participation.mark_as_read(participation) do
1828 participation_view =
1829 ConversationView.render("participation.json", %{participation: participation, user: user})
1830
1831 conn
1832 |> json(participation_view)
1833 end
1834 end
1835
1836 def try_render(conn, target, params)
1837 when is_binary(target) do
1838 res = render(conn, target, params)
1839
1840 if res == nil do
1841 conn
1842 |> put_status(501)
1843 |> json(%{error: "Can't display this activity"})
1844 else
1845 res
1846 end
1847 end
1848
1849 def try_render(conn, _, _) do
1850 conn
1851 |> put_status(501)
1852 |> json(%{error: "Can't display this activity"})
1853 end
1854
1855 defp present?(nil), do: false
1856 defp present?(false), do: false
1857 defp present?(_), do: true
1858 end