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