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