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