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