Polish IdempotencyPlug
[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 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
140 with %Plug.Upload{} <- value,
141 {:ok, object} <- ActivityPub.upload(value, type: :background) do
142 {:ok, object.data}
143 else
144 _ -> :error
145 end
146 end)
147 |> Map.put(:emoji, user_info_emojis)
148
149 info_cng = User.Info.profile_update(user.info, info_params)
150
151 with changeset <- User.update_changeset(user, user_params),
152 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
153 {:ok, user} <- User.update_and_set_cache(changeset) do
154 if original_user != user do
155 CommonAPI.update(user)
156 end
157
158 json(
159 conn,
160 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
161 )
162 else
163 _e ->
164 conn
165 |> put_status(403)
166 |> json(%{error: "Invalid request"})
167 end
168 end
169
170 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
171 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
172
173 account =
174 AccountView.render("account.json", %{
175 user: user,
176 for: user,
177 with_pleroma_settings: true,
178 with_chat_token: chat_token
179 })
180
181 json(conn, account)
182 end
183
184 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
185 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
186 conn
187 |> put_view(AppView)
188 |> render("short.json", %{app: app})
189 end
190 end
191
192 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
193 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
194 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
195 account = AccountView.render("account.json", %{user: user, for: for_user})
196 json(conn, account)
197 else
198 _e ->
199 conn
200 |> put_status(404)
201 |> json(%{error: "Can't find user"})
202 end
203 end
204
205 @mastodon_api_level "2.7.2"
206
207 def masto_instance(conn, _params) do
208 instance = Config.get(:instance)
209
210 response = %{
211 uri: Web.base_url(),
212 title: Keyword.get(instance, :name),
213 description: Keyword.get(instance, :description),
214 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
215 email: Keyword.get(instance, :email),
216 urls: %{
217 streaming_api: Pleroma.Web.Endpoint.websocket_url()
218 },
219 stats: Stats.get_stats(),
220 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
221 languages: ["en"],
222 registrations: Pleroma.Config.get([:instance, :registrations_open]),
223 # Extra (not present in Mastodon):
224 max_toot_chars: Keyword.get(instance, :limit),
225 poll_limits: Keyword.get(instance, :poll_limits)
226 }
227
228 json(conn, response)
229 end
230
231 def peers(conn, _params) do
232 json(conn, Stats.get_peers())
233 end
234
235 defp mastodonized_emoji do
236 Pleroma.Emoji.get_all()
237 |> Enum.map(fn {shortcode, relative_url, tags} ->
238 url = to_string(URI.merge(Web.base_url(), relative_url))
239
240 %{
241 "shortcode" => shortcode,
242 "static_url" => url,
243 "visible_in_picker" => true,
244 "url" => url,
245 "tags" => tags
246 }
247 end)
248 end
249
250 def custom_emojis(conn, _params) do
251 mastodon_emoji = mastodonized_emoji()
252 json(conn, mastodon_emoji)
253 end
254
255 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
256 params =
257 conn.params
258 |> Map.drop(["since_id", "max_id", "min_id"])
259 |> Map.merge(params)
260
261 last = List.last(activities)
262
263 if last do
264 max_id = last.id
265
266 limit =
267 params
268 |> Map.get("limit", "20")
269 |> String.to_integer()
270
271 min_id =
272 if length(activities) <= limit do
273 activities
274 |> List.first()
275 |> Map.get(:id)
276 else
277 activities
278 |> Enum.at(limit * -1)
279 |> Map.get(:id)
280 end
281
282 {next_url, prev_url} =
283 if param do
284 {
285 mastodon_api_url(
286 Pleroma.Web.Endpoint,
287 method,
288 param,
289 Map.merge(params, %{max_id: max_id})
290 ),
291 mastodon_api_url(
292 Pleroma.Web.Endpoint,
293 method,
294 param,
295 Map.merge(params, %{min_id: min_id})
296 )
297 }
298 else
299 {
300 mastodon_api_url(
301 Pleroma.Web.Endpoint,
302 method,
303 Map.merge(params, %{max_id: max_id})
304 ),
305 mastodon_api_url(
306 Pleroma.Web.Endpoint,
307 method,
308 Map.merge(params, %{min_id: min_id})
309 )
310 }
311 end
312
313 conn
314 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
315 else
316 conn
317 end
318 end
319
320 def home_timeline(%{assigns: %{user: user}} = conn, params) do
321 params =
322 params
323 |> Map.put("type", ["Create", "Announce"])
324 |> Map.put("blocking_user", user)
325 |> Map.put("muting_user", user)
326 |> Map.put("user", user)
327
328 activities =
329 [user.ap_id | user.following]
330 |> ActivityPub.fetch_activities(params)
331 |> Enum.reverse()
332
333 conn
334 |> add_link_headers(:home_timeline, activities)
335 |> put_view(StatusView)
336 |> render("index.json", %{activities: activities, for: user, as: :activity})
337 end
338
339 def public_timeline(%{assigns: %{user: user}} = conn, params) do
340 local_only = params["local"] in [true, "True", "true", "1"]
341
342 activities =
343 params
344 |> Map.put("type", ["Create", "Announce"])
345 |> Map.put("local_only", local_only)
346 |> Map.put("blocking_user", user)
347 |> Map.put("muting_user", user)
348 |> ActivityPub.fetch_public_activities()
349 |> Enum.reverse()
350
351 conn
352 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
353 |> put_view(StatusView)
354 |> render("index.json", %{activities: activities, for: user, as: :activity})
355 end
356
357 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
358 with %User{} = user <- User.get_cached_by_id(params["id"]) do
359 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
360
361 conn
362 |> add_link_headers(:user_statuses, activities, params["id"])
363 |> put_view(StatusView)
364 |> render("index.json", %{
365 activities: activities,
366 for: reading_user,
367 as: :activity
368 })
369 end
370 end
371
372 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
373 params =
374 params
375 |> Map.put("type", "Create")
376 |> Map.put("blocking_user", user)
377 |> Map.put("user", user)
378 |> Map.put(:visibility, "direct")
379
380 activities =
381 [user.ap_id]
382 |> ActivityPub.fetch_activities_query(params)
383 |> Pagination.fetch_paginated(params)
384
385 conn
386 |> add_link_headers(:dm_timeline, activities)
387 |> put_view(StatusView)
388 |> render("index.json", %{activities: activities, for: user, as: :activity})
389 end
390
391 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
392 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
393 true <- Visibility.visible_for_user?(activity, user) do
394 conn
395 |> put_view(StatusView)
396 |> try_render("status.json", %{activity: activity, for: user})
397 end
398 end
399
400 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
401 with %Activity{} = activity <- Activity.get_by_id(id),
402 activities <-
403 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
404 "blocking_user" => user,
405 "user" => user
406 }),
407 activities <-
408 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
409 activities <-
410 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
411 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
412 result = %{
413 ancestors:
414 StatusView.render(
415 "index.json",
416 for: user,
417 activities: grouped_activities[true] || [],
418 as: :activity
419 )
420 |> Enum.reverse(),
421 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
422 descendants:
423 StatusView.render(
424 "index.json",
425 for: user,
426 activities: grouped_activities[false] || [],
427 as: :activity
428 )
429 |> Enum.reverse()
430 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
431 }
432
433 json(conn, result)
434 end
435 end
436
437 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
438 with %Object{} = object <- Object.get_by_id(id),
439 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
440 true <- Visibility.visible_for_user?(activity, user) do
441 conn
442 |> put_view(StatusView)
443 |> try_render("poll.json", %{object: object, for: user})
444 else
445 nil ->
446 conn
447 |> put_status(404)
448 |> json(%{error: "Record not found"})
449
450 false ->
451 conn
452 |> put_status(404)
453 |> json(%{error: "Record not found"})
454 end
455 end
456
457 defp get_cached_vote_or_vote(user, object, choices) do
458 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
459
460 {_, res} =
461 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
462 case CommonAPI.vote(user, object, choices) do
463 {:error, _message} = res -> {:ignore, res}
464 res -> {:commit, res}
465 end
466 end)
467
468 res
469 end
470
471 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
472 with %Object{} = object <- Object.get_by_id(id),
473 true <- object.data["type"] == "Question",
474 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
475 true <- Visibility.visible_for_user?(activity, user),
476 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
477 conn
478 |> put_view(StatusView)
479 |> try_render("poll.json", %{object: object, for: user})
480 else
481 nil ->
482 conn
483 |> put_status(404)
484 |> json(%{error: "Record not found"})
485
486 false ->
487 conn
488 |> put_status(404)
489 |> json(%{error: "Record not found"})
490
491 {:error, message} ->
492 conn
493 |> put_status(422)
494 |> json(%{error: message})
495 end
496 end
497
498 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
499 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
500 conn
501 |> add_link_headers(:scheduled_statuses, scheduled_activities)
502 |> put_view(ScheduledActivityView)
503 |> render("index.json", %{scheduled_activities: scheduled_activities})
504 end
505 end
506
507 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
508 with %ScheduledActivity{} = scheduled_activity <-
509 ScheduledActivity.get(user, scheduled_activity_id) do
510 conn
511 |> put_view(ScheduledActivityView)
512 |> render("show.json", %{scheduled_activity: scheduled_activity})
513 else
514 _ -> {:error, :not_found}
515 end
516 end
517
518 def update_scheduled_status(
519 %{assigns: %{user: user}} = conn,
520 %{"id" => scheduled_activity_id} = params
521 ) do
522 with %ScheduledActivity{} = scheduled_activity <-
523 ScheduledActivity.get(user, scheduled_activity_id),
524 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
525 conn
526 |> put_view(ScheduledActivityView)
527 |> render("show.json", %{scheduled_activity: scheduled_activity})
528 else
529 nil -> {:error, :not_found}
530 error -> error
531 end
532 end
533
534 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
535 with %ScheduledActivity{} = scheduled_activity <-
536 ScheduledActivity.get(user, scheduled_activity_id),
537 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
538 conn
539 |> put_view(ScheduledActivityView)
540 |> render("show.json", %{scheduled_activity: scheduled_activity})
541 else
542 nil -> {:error, :not_found}
543 error -> error
544 end
545 end
546
547 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
548 params =
549 params
550 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
551
552 scheduled_at = params["scheduled_at"]
553
554 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
555 with {:ok, scheduled_activity} <-
556 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
557 conn
558 |> put_view(ScheduledActivityView)
559 |> render("show.json", %{scheduled_activity: scheduled_activity})
560 end
561 else
562 params = Map.drop(params, ["scheduled_at"])
563
564 case CommonAPI.post(user, params) do
565 {:error, message} ->
566 conn
567 |> put_status(:unprocessable_entity)
568 |> json(%{error: message})
569
570 {:ok, activity} ->
571 conn
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
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("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