Merge branch 'docs/remove-tootdon' into 'develop'
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7 alias Ecto.Changeset
8 alias Pleroma.Activity
9 alias Pleroma.Bookmark
10 alias Pleroma.Config
11 alias Pleroma.Conversation.Participation
12 alias Pleroma.Filter
13 alias Pleroma.Formatter
14 alias Pleroma.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 get_cached_status_or_post(conn, params) do
565 {:ignore, message} ->
566 conn
567 |> put_status(422)
568 |> json(%{error: message})
569
570 {:error, message} ->
571 conn
572 |> put_status(422)
573 |> json(%{error: message})
574
575 {_, activity} ->
576 conn
577 |> put_view(StatusView)
578 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
579 end
580 end
581 end
582
583 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
584 idempotency_key =
585 case get_req_header(conn, "idempotency-key") do
586 [key] -> key
587 _ -> Ecto.UUID.generate()
588 end
589
590 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
591 case CommonAPI.post(user, params) do
592 {:ok, activity} -> activity
593 {:error, message} -> {:ignore, message}
594 end
595 end)
596 end
597
598 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
599 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
600 json(conn, %{})
601 else
602 _e ->
603 conn
604 |> put_status(403)
605 |> json(%{error: "Can't delete this post"})
606 end
607 end
608
609 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
610 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
611 %Activity{} = announce <- Activity.normalize(announce.data) do
612 conn
613 |> put_view(StatusView)
614 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
615 end
616 end
617
618 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
619 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
620 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
621 conn
622 |> put_view(StatusView)
623 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
624 end
625 end
626
627 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
628 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
629 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
630 conn
631 |> put_view(StatusView)
632 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
633 end
634 end
635
636 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
637 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
638 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
639 conn
640 |> put_view(StatusView)
641 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
642 end
643 end
644
645 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
646 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
647 conn
648 |> put_view(StatusView)
649 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
650 else
651 {:error, reason} ->
652 conn
653 |> put_resp_content_type("application/json")
654 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
655 end
656 end
657
658 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
659 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
660 conn
661 |> put_view(StatusView)
662 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
663 end
664 end
665
666 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
667 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
668 %User{} = user <- User.get_cached_by_nickname(user.nickname),
669 true <- Visibility.visible_for_user?(activity, user),
670 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
671 conn
672 |> put_view(StatusView)
673 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 end
675 end
676
677 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
678 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
679 %User{} = user <- User.get_cached_by_nickname(user.nickname),
680 true <- Visibility.visible_for_user?(activity, user),
681 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
682 conn
683 |> put_view(StatusView)
684 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
685 end
686 end
687
688 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
689 activity = Activity.get_by_id(id)
690
691 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
692 conn
693 |> put_view(StatusView)
694 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
695 else
696 {:error, reason} ->
697 conn
698 |> put_resp_content_type("application/json")
699 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
700 end
701 end
702
703 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
704 activity = Activity.get_by_id(id)
705
706 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
707 conn
708 |> put_view(StatusView)
709 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
710 end
711 end
712
713 def notifications(%{assigns: %{user: user}} = conn, params) do
714 notifications = MastodonAPI.get_notifications(user, params)
715
716 conn
717 |> add_link_headers(:notifications, notifications)
718 |> put_view(NotificationView)
719 |> render("index.json", %{notifications: notifications, for: user})
720 end
721
722 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
723 with {:ok, notification} <- Notification.get(user, id) do
724 conn
725 |> put_view(NotificationView)
726 |> render("show.json", %{notification: notification, for: user})
727 else
728 {:error, reason} ->
729 conn
730 |> put_resp_content_type("application/json")
731 |> send_resp(403, Jason.encode!(%{"error" => reason}))
732 end
733 end
734
735 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
736 Notification.clear(user)
737 json(conn, %{})
738 end
739
740 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
741 with {:ok, _notif} <- Notification.dismiss(user, id) do
742 json(conn, %{})
743 else
744 {:error, reason} ->
745 conn
746 |> put_resp_content_type("application/json")
747 |> send_resp(403, Jason.encode!(%{"error" => reason}))
748 end
749 end
750
751 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
752 Notification.destroy_multiple(user, ids)
753 json(conn, %{})
754 end
755
756 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
757 id = List.wrap(id)
758 q = from(u in User, where: u.id in ^id)
759 targets = Repo.all(q)
760
761 conn
762 |> put_view(AccountView)
763 |> render("relationships.json", %{user: user, targets: targets})
764 end
765
766 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
767 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
768
769 def update_media(%{assigns: %{user: user}} = conn, data) do
770 with %Object{} = object <- Repo.get(Object, data["id"]),
771 true <- Object.authorize_mutation(object, user),
772 true <- is_binary(data["description"]),
773 description <- data["description"] do
774 new_data = %{object.data | "name" => description}
775
776 {:ok, _} =
777 object
778 |> Object.change(%{data: new_data})
779 |> Repo.update()
780
781 attachment_data = Map.put(new_data, "id", object.id)
782
783 conn
784 |> put_view(StatusView)
785 |> render("attachment.json", %{attachment: attachment_data})
786 end
787 end
788
789 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
790 with {:ok, object} <-
791 ActivityPub.upload(
792 file,
793 actor: User.ap_id(user),
794 description: Map.get(data, "description")
795 ) do
796 attachment_data = Map.put(object.data, "id", object.id)
797
798 conn
799 |> put_view(StatusView)
800 |> render("attachment.json", %{attachment: attachment_data})
801 end
802 end
803
804 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
805 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
806 %{} = attachment_data <- Map.put(object.data, "id", object.id),
807 %{type: type} = rendered <-
808 StatusView.render("attachment.json", %{attachment: attachment_data}) do
809 # Reject if not an image
810 if type == "image" do
811 # Sure!
812 # Save to the user's info
813 info_changeset = User.Info.mascot_update(user.info, rendered)
814
815 user_changeset =
816 user
817 |> Ecto.Changeset.change()
818 |> Ecto.Changeset.put_embed(:info, info_changeset)
819
820 {:ok, _user} = User.update_and_set_cache(user_changeset)
821
822 conn
823 |> json(rendered)
824 else
825 conn
826 |> put_resp_content_type("application/json")
827 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
828 end
829 end
830 end
831
832 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
833 mascot = User.get_mascot(user)
834
835 conn
836 |> json(mascot)
837 end
838
839 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
840 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
841 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
842 q = from(u in User, where: u.ap_id in ^likes)
843 users = Repo.all(q)
844
845 conn
846 |> put_view(AccountView)
847 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
848 else
849 _ -> json(conn, [])
850 end
851 end
852
853 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
854 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
855 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
856 q = from(u in User, where: u.ap_id in ^announces)
857 users = Repo.all(q)
858
859 conn
860 |> put_view(AccountView)
861 |> render("accounts.json", %{for: user, users: users, as: :user})
862 else
863 _ -> json(conn, [])
864 end
865 end
866
867 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
868 local_only = params["local"] in [true, "True", "true", "1"]
869
870 tags =
871 [params["tag"], params["any"]]
872 |> List.flatten()
873 |> Enum.uniq()
874 |> Enum.filter(& &1)
875 |> Enum.map(&String.downcase(&1))
876
877 tag_all =
878 params["all"] ||
879 []
880 |> Enum.map(&String.downcase(&1))
881
882 tag_reject =
883 params["none"] ||
884 []
885 |> Enum.map(&String.downcase(&1))
886
887 activities =
888 params
889 |> Map.put("type", "Create")
890 |> Map.put("local_only", local_only)
891 |> Map.put("blocking_user", user)
892 |> Map.put("muting_user", user)
893 |> Map.put("tag", tags)
894 |> Map.put("tag_all", tag_all)
895 |> Map.put("tag_reject", tag_reject)
896 |> ActivityPub.fetch_public_activities()
897 |> Enum.reverse()
898
899 conn
900 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
901 |> put_view(StatusView)
902 |> render("index.json", %{activities: activities, for: user, as: :activity})
903 end
904
905 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
906 with %User{} = user <- User.get_cached_by_id(id),
907 followers <- MastodonAPI.get_followers(user, params) do
908 followers =
909 cond do
910 for_user && user.id == for_user.id -> followers
911 user.info.hide_followers -> []
912 true -> followers
913 end
914
915 conn
916 |> add_link_headers(:followers, followers, user)
917 |> put_view(AccountView)
918 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
919 end
920 end
921
922 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
923 with %User{} = user <- User.get_cached_by_id(id),
924 followers <- MastodonAPI.get_friends(user, params) do
925 followers =
926 cond do
927 for_user && user.id == for_user.id -> followers
928 user.info.hide_follows -> []
929 true -> followers
930 end
931
932 conn
933 |> add_link_headers(:following, followers, user)
934 |> put_view(AccountView)
935 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
936 end
937 end
938
939 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
940 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
941 conn
942 |> put_view(AccountView)
943 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
944 end
945 end
946
947 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
948 with %User{} = follower <- User.get_cached_by_id(id),
949 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
950 conn
951 |> put_view(AccountView)
952 |> render("relationship.json", %{user: followed, target: follower})
953 else
954 {:error, message} ->
955 conn
956 |> put_resp_content_type("application/json")
957 |> send_resp(403, Jason.encode!(%{"error" => message}))
958 end
959 end
960
961 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
962 with %User{} = follower <- User.get_cached_by_id(id),
963 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
964 conn
965 |> put_view(AccountView)
966 |> render("relationship.json", %{user: followed, target: follower})
967 else
968 {:error, message} ->
969 conn
970 |> put_resp_content_type("application/json")
971 |> send_resp(403, Jason.encode!(%{"error" => message}))
972 end
973 end
974
975 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
976 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
977 {_, true} <- {:followed, follower.id != followed.id},
978 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
979 conn
980 |> put_view(AccountView)
981 |> render("relationship.json", %{user: follower, target: followed})
982 else
983 {:followed, _} ->
984 {:error, :not_found}
985
986 {:error, message} ->
987 conn
988 |> put_resp_content_type("application/json")
989 |> send_resp(403, Jason.encode!(%{"error" => message}))
990 end
991 end
992
993 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
994 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
995 {_, true} <- {:followed, follower.id != followed.id},
996 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
997 conn
998 |> put_view(AccountView)
999 |> render("account.json", %{user: followed, for: follower})
1000 else
1001 {:followed, _} ->
1002 {:error, :not_found}
1003
1004 {:error, message} ->
1005 conn
1006 |> put_resp_content_type("application/json")
1007 |> send_resp(403, Jason.encode!(%{"error" => message}))
1008 end
1009 end
1010
1011 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1012 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1013 {_, true} <- {:followed, follower.id != followed.id},
1014 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1015 conn
1016 |> put_view(AccountView)
1017 |> render("relationship.json", %{user: follower, target: followed})
1018 else
1019 {:followed, _} ->
1020 {:error, :not_found}
1021
1022 error ->
1023 error
1024 end
1025 end
1026
1027 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1028 with %User{} = muted <- User.get_cached_by_id(id),
1029 {:ok, muter} <- User.mute(muter, muted) do
1030 conn
1031 |> put_view(AccountView)
1032 |> render("relationship.json", %{user: muter, target: muted})
1033 else
1034 {:error, message} ->
1035 conn
1036 |> put_resp_content_type("application/json")
1037 |> send_resp(403, Jason.encode!(%{"error" => message}))
1038 end
1039 end
1040
1041 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1042 with %User{} = muted <- User.get_cached_by_id(id),
1043 {:ok, muter} <- User.unmute(muter, muted) do
1044 conn
1045 |> put_view(AccountView)
1046 |> render("relationship.json", %{user: muter, target: muted})
1047 else
1048 {:error, message} ->
1049 conn
1050 |> put_resp_content_type("application/json")
1051 |> send_resp(403, Jason.encode!(%{"error" => message}))
1052 end
1053 end
1054
1055 def mutes(%{assigns: %{user: user}} = conn, _) do
1056 with muted_accounts <- User.muted_users(user) do
1057 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1058 json(conn, res)
1059 end
1060 end
1061
1062 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1063 with %User{} = blocked <- User.get_cached_by_id(id),
1064 {:ok, blocker} <- User.block(blocker, blocked),
1065 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1066 conn
1067 |> put_view(AccountView)
1068 |> render("relationship.json", %{user: blocker, target: blocked})
1069 else
1070 {:error, message} ->
1071 conn
1072 |> put_resp_content_type("application/json")
1073 |> send_resp(403, Jason.encode!(%{"error" => message}))
1074 end
1075 end
1076
1077 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1078 with %User{} = blocked <- User.get_cached_by_id(id),
1079 {:ok, blocker} <- User.unblock(blocker, blocked),
1080 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1081 conn
1082 |> put_view(AccountView)
1083 |> render("relationship.json", %{user: blocker, target: blocked})
1084 else
1085 {:error, message} ->
1086 conn
1087 |> put_resp_content_type("application/json")
1088 |> send_resp(403, Jason.encode!(%{"error" => message}))
1089 end
1090 end
1091
1092 def blocks(%{assigns: %{user: user}} = conn, _) do
1093 with blocked_accounts <- User.blocked_users(user) do
1094 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1095 json(conn, res)
1096 end
1097 end
1098
1099 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1100 json(conn, info.domain_blocks || [])
1101 end
1102
1103 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1104 User.block_domain(blocker, domain)
1105 json(conn, %{})
1106 end
1107
1108 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1109 User.unblock_domain(blocker, domain)
1110 json(conn, %{})
1111 end
1112
1113 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1114 with %User{} = subscription_target <- User.get_cached_by_id(id),
1115 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1116 conn
1117 |> put_view(AccountView)
1118 |> render("relationship.json", %{user: user, target: subscription_target})
1119 else
1120 {:error, message} ->
1121 conn
1122 |> put_resp_content_type("application/json")
1123 |> send_resp(403, Jason.encode!(%{"error" => message}))
1124 end
1125 end
1126
1127 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1128 with %User{} = subscription_target <- User.get_cached_by_id(id),
1129 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1130 conn
1131 |> put_view(AccountView)
1132 |> render("relationship.json", %{user: user, target: subscription_target})
1133 else
1134 {:error, message} ->
1135 conn
1136 |> put_resp_content_type("application/json")
1137 |> send_resp(403, Jason.encode!(%{"error" => message}))
1138 end
1139 end
1140
1141 def favourites(%{assigns: %{user: user}} = conn, params) do
1142 params =
1143 params
1144 |> Map.put("type", "Create")
1145 |> Map.put("favorited_by", user.ap_id)
1146 |> Map.put("blocking_user", user)
1147
1148 activities =
1149 ActivityPub.fetch_activities([], params)
1150 |> Enum.reverse()
1151
1152 conn
1153 |> add_link_headers(:favourites, activities)
1154 |> put_view(StatusView)
1155 |> render("index.json", %{activities: activities, for: user, as: :activity})
1156 end
1157
1158 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1159 with %User{} = user <- User.get_by_id(id),
1160 false <- user.info.hide_favorites do
1161 params =
1162 params
1163 |> Map.put("type", "Create")
1164 |> Map.put("favorited_by", user.ap_id)
1165 |> Map.put("blocking_user", for_user)
1166
1167 recipients =
1168 if for_user do
1169 ["https://www.w3.org/ns/activitystreams#Public"] ++
1170 [for_user.ap_id | for_user.following]
1171 else
1172 ["https://www.w3.org/ns/activitystreams#Public"]
1173 end
1174
1175 activities =
1176 recipients
1177 |> ActivityPub.fetch_activities(params)
1178 |> Enum.reverse()
1179
1180 conn
1181 |> add_link_headers(:favourites, activities)
1182 |> put_view(StatusView)
1183 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1184 else
1185 nil ->
1186 {:error, :not_found}
1187
1188 true ->
1189 conn
1190 |> put_status(403)
1191 |> json(%{error: "Can't get favorites"})
1192 end
1193 end
1194
1195 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1196 user = User.get_cached_by_id(user.id)
1197
1198 bookmarks =
1199 Bookmark.for_user_query(user.id)
1200 |> Pagination.fetch_paginated(params)
1201
1202 activities =
1203 bookmarks
1204 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1205
1206 conn
1207 |> add_link_headers(:bookmarks, bookmarks)
1208 |> put_view(StatusView)
1209 |> render("index.json", %{activities: activities, for: user, as: :activity})
1210 end
1211
1212 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1213 lists = Pleroma.List.for_user(user, opts)
1214 res = ListView.render("lists.json", lists: lists)
1215 json(conn, res)
1216 end
1217
1218 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1219 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1220 res = ListView.render("list.json", list: list)
1221 json(conn, res)
1222 else
1223 _e ->
1224 conn
1225 |> put_status(404)
1226 |> json(%{error: "Record not found"})
1227 end
1228 end
1229
1230 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1231 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1232 res = ListView.render("lists.json", lists: lists)
1233 json(conn, res)
1234 end
1235
1236 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1237 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1238 {:ok, _list} <- Pleroma.List.delete(list) do
1239 json(conn, %{})
1240 else
1241 _e ->
1242 json(conn, "error")
1243 end
1244 end
1245
1246 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1247 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1248 res = ListView.render("list.json", list: list)
1249 json(conn, res)
1250 end
1251 end
1252
1253 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1254 accounts
1255 |> Enum.each(fn account_id ->
1256 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1257 %User{} = followed <- User.get_cached_by_id(account_id) do
1258 Pleroma.List.follow(list, followed)
1259 end
1260 end)
1261
1262 json(conn, %{})
1263 end
1264
1265 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1266 accounts
1267 |> Enum.each(fn account_id ->
1268 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1269 %User{} = followed <- User.get_cached_by_id(account_id) do
1270 Pleroma.List.unfollow(list, followed)
1271 end
1272 end)
1273
1274 json(conn, %{})
1275 end
1276
1277 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1278 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1279 {:ok, users} = Pleroma.List.get_following(list) do
1280 conn
1281 |> put_view(AccountView)
1282 |> render("accounts.json", %{for: user, users: users, as: :user})
1283 end
1284 end
1285
1286 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1287 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1288 {:ok, list} <- Pleroma.List.rename(list, title) do
1289 res = ListView.render("list.json", list: list)
1290 json(conn, res)
1291 else
1292 _e ->
1293 json(conn, "error")
1294 end
1295 end
1296
1297 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1298 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1299 params =
1300 params
1301 |> Map.put("type", "Create")
1302 |> Map.put("blocking_user", user)
1303 |> Map.put("muting_user", user)
1304
1305 # we must filter the following list for the user to avoid leaking statuses the user
1306 # does not actually have permission to see (for more info, peruse security issue #270).
1307 activities =
1308 following
1309 |> Enum.filter(fn x -> x in user.following end)
1310 |> ActivityPub.fetch_activities_bounded(following, params)
1311 |> Enum.reverse()
1312
1313 conn
1314 |> put_view(StatusView)
1315 |> render("index.json", %{activities: activities, for: user, as: :activity})
1316 else
1317 _e ->
1318 conn
1319 |> put_status(403)
1320 |> json(%{error: "Error."})
1321 end
1322 end
1323
1324 def index(%{assigns: %{user: user}} = conn, _params) do
1325 token = get_session(conn, :oauth_token)
1326
1327 if user && token do
1328 mastodon_emoji = mastodonized_emoji()
1329
1330 limit = Config.get([:instance, :limit])
1331
1332 accounts =
1333 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1334
1335 initial_state =
1336 %{
1337 meta: %{
1338 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1339 access_token: token,
1340 locale: "en",
1341 domain: Pleroma.Web.Endpoint.host(),
1342 admin: "1",
1343 me: "#{user.id}",
1344 unfollow_modal: false,
1345 boost_modal: false,
1346 delete_modal: true,
1347 auto_play_gif: false,
1348 display_sensitive_media: false,
1349 reduce_motion: false,
1350 max_toot_chars: limit,
1351 mascot: User.get_mascot(user)["url"]
1352 },
1353 poll_limits: Config.get([:instance, :poll_limits]),
1354 rights: %{
1355 delete_others_notice: present?(user.info.is_moderator),
1356 admin: present?(user.info.is_admin)
1357 },
1358 compose: %{
1359 me: "#{user.id}",
1360 default_privacy: user.info.default_scope,
1361 default_sensitive: false,
1362 allow_content_types: Config.get([:instance, :allowed_post_formats])
1363 },
1364 media_attachments: %{
1365 accept_content_types: [
1366 ".jpg",
1367 ".jpeg",
1368 ".png",
1369 ".gif",
1370 ".webm",
1371 ".mp4",
1372 ".m4v",
1373 "image\/jpeg",
1374 "image\/png",
1375 "image\/gif",
1376 "video\/webm",
1377 "video\/mp4"
1378 ]
1379 },
1380 settings:
1381 user.info.settings ||
1382 %{
1383 onboarded: true,
1384 home: %{
1385 shows: %{
1386 reblog: true,
1387 reply: true
1388 }
1389 },
1390 notifications: %{
1391 alerts: %{
1392 follow: true,
1393 favourite: true,
1394 reblog: true,
1395 mention: true
1396 },
1397 shows: %{
1398 follow: true,
1399 favourite: true,
1400 reblog: true,
1401 mention: true
1402 },
1403 sounds: %{
1404 follow: true,
1405 favourite: true,
1406 reblog: true,
1407 mention: true
1408 }
1409 }
1410 },
1411 push_subscription: nil,
1412 accounts: accounts,
1413 custom_emojis: mastodon_emoji,
1414 char_limit: limit
1415 }
1416 |> Jason.encode!()
1417
1418 conn
1419 |> put_layout(false)
1420 |> put_view(MastodonView)
1421 |> render("index.html", %{initial_state: initial_state})
1422 else
1423 conn
1424 |> put_session(:return_to, conn.request_path)
1425 |> redirect(to: "/web/login")
1426 end
1427 end
1428
1429 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1430 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1431
1432 with changeset <- Ecto.Changeset.change(user),
1433 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1434 {:ok, _user} <- User.update_and_set_cache(changeset) do
1435 json(conn, %{})
1436 else
1437 e ->
1438 conn
1439 |> put_resp_content_type("application/json")
1440 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1441 end
1442 end
1443
1444 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1445 redirect(conn, to: local_mastodon_root_path(conn))
1446 end
1447
1448 @doc "Local Mastodon FE login init action"
1449 def login(conn, %{"code" => auth_token}) do
1450 with {:ok, app} <- get_or_make_app(),
1451 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1452 {:ok, token} <- Token.exchange_token(app, auth) do
1453 conn
1454 |> put_session(:oauth_token, token.token)
1455 |> redirect(to: local_mastodon_root_path(conn))
1456 end
1457 end
1458
1459 @doc "Local Mastodon FE callback action"
1460 def login(conn, _) do
1461 with {:ok, app} <- get_or_make_app() do
1462 path =
1463 o_auth_path(
1464 conn,
1465 :authorize,
1466 response_type: "code",
1467 client_id: app.client_id,
1468 redirect_uri: ".",
1469 scope: Enum.join(app.scopes, " ")
1470 )
1471
1472 redirect(conn, to: path)
1473 end
1474 end
1475
1476 defp local_mastodon_root_path(conn) do
1477 case get_session(conn, :return_to) do
1478 nil ->
1479 mastodon_api_path(conn, :index, ["getting-started"])
1480
1481 return_to ->
1482 delete_session(conn, :return_to)
1483 return_to
1484 end
1485 end
1486
1487 defp get_or_make_app do
1488 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1489 scopes = ["read", "write", "follow", "push"]
1490
1491 with %App{} = app <- Repo.get_by(App, find_attrs) do
1492 {:ok, app} =
1493 if app.scopes == scopes do
1494 {:ok, app}
1495 else
1496 app
1497 |> Ecto.Changeset.change(%{scopes: scopes})
1498 |> Repo.update()
1499 end
1500
1501 {:ok, app}
1502 else
1503 _e ->
1504 cs =
1505 App.register_changeset(
1506 %App{},
1507 Map.put(find_attrs, :scopes, scopes)
1508 )
1509
1510 Repo.insert(cs)
1511 end
1512 end
1513
1514 def logout(conn, _) do
1515 conn
1516 |> clear_session
1517 |> redirect(to: "/")
1518 end
1519
1520 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1521 Logger.debug("Unimplemented, returning unmodified relationship")
1522
1523 with %User{} = target <- User.get_cached_by_id(id) do
1524 conn
1525 |> put_view(AccountView)
1526 |> render("relationship.json", %{user: user, target: target})
1527 end
1528 end
1529
1530 def empty_array(conn, _) do
1531 Logger.debug("Unimplemented, returning an empty array")
1532 json(conn, [])
1533 end
1534
1535 def empty_object(conn, _) do
1536 Logger.debug("Unimplemented, returning an empty object")
1537 json(conn, %{})
1538 end
1539
1540 def get_filters(%{assigns: %{user: user}} = conn, _) do
1541 filters = Filter.get_filters(user)
1542 res = FilterView.render("filters.json", filters: filters)
1543 json(conn, res)
1544 end
1545
1546 def create_filter(
1547 %{assigns: %{user: user}} = conn,
1548 %{"phrase" => phrase, "context" => context} = params
1549 ) do
1550 query = %Filter{
1551 user_id: user.id,
1552 phrase: phrase,
1553 context: context,
1554 hide: Map.get(params, "irreversible", false),
1555 whole_word: Map.get(params, "boolean", true)
1556 # expires_at
1557 }
1558
1559 {:ok, response} = Filter.create(query)
1560 res = FilterView.render("filter.json", filter: response)
1561 json(conn, res)
1562 end
1563
1564 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1565 filter = Filter.get(filter_id, user)
1566 res = FilterView.render("filter.json", filter: filter)
1567 json(conn, res)
1568 end
1569
1570 def update_filter(
1571 %{assigns: %{user: user}} = conn,
1572 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1573 ) do
1574 query = %Filter{
1575 user_id: user.id,
1576 filter_id: filter_id,
1577 phrase: phrase,
1578 context: context,
1579 hide: Map.get(params, "irreversible", nil),
1580 whole_word: Map.get(params, "boolean", true)
1581 # expires_at
1582 }
1583
1584 {:ok, response} = Filter.update(query)
1585 res = FilterView.render("filter.json", filter: response)
1586 json(conn, res)
1587 end
1588
1589 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1590 query = %Filter{
1591 user_id: user.id,
1592 filter_id: filter_id
1593 }
1594
1595 {:ok, _} = Filter.delete(query)
1596 json(conn, %{})
1597 end
1598
1599 # fallback action
1600 #
1601 def errors(conn, {:error, %Changeset{} = changeset}) do
1602 error_message =
1603 changeset
1604 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1605 |> Enum.map_join(", ", fn {_k, v} -> v end)
1606
1607 conn
1608 |> put_status(422)
1609 |> json(%{error: error_message})
1610 end
1611
1612 def errors(conn, {:error, :not_found}) do
1613 conn
1614 |> put_status(404)
1615 |> json(%{error: "Record not found"})
1616 end
1617
1618 def errors(conn, _) do
1619 conn
1620 |> put_status(500)
1621 |> json("Something went wrong")
1622 end
1623
1624 def suggestions(%{assigns: %{user: user}} = conn, _) do
1625 suggestions = Config.get(:suggestions)
1626
1627 if Keyword.get(suggestions, :enabled, false) do
1628 api = Keyword.get(suggestions, :third_party_engine, "")
1629 timeout = Keyword.get(suggestions, :timeout, 5000)
1630 limit = Keyword.get(suggestions, :limit, 23)
1631
1632 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1633
1634 user = user.nickname
1635
1636 url =
1637 api
1638 |> String.replace("{{host}}", host)
1639 |> String.replace("{{user}}", user)
1640
1641 with {:ok, %{status: 200, body: body}} <-
1642 HTTP.get(
1643 url,
1644 [],
1645 adapter: [
1646 recv_timeout: timeout,
1647 pool: :default
1648 ]
1649 ),
1650 {:ok, data} <- Jason.decode(body) do
1651 data =
1652 data
1653 |> Enum.slice(0, limit)
1654 |> Enum.map(fn x ->
1655 Map.put(
1656 x,
1657 "id",
1658 case User.get_or_fetch(x["acct"]) do
1659 {:ok, %User{id: id}} -> id
1660 _ -> 0
1661 end
1662 )
1663 end)
1664 |> Enum.map(fn x ->
1665 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1666 end)
1667 |> Enum.map(fn x ->
1668 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1669 end)
1670
1671 conn
1672 |> json(data)
1673 else
1674 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1675 end
1676 else
1677 json(conn, [])
1678 end
1679 end
1680
1681 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1682 with %Activity{} = activity <- Activity.get_by_id(status_id),
1683 true <- Visibility.visible_for_user?(activity, user) do
1684 data =
1685 StatusView.render(
1686 "card.json",
1687 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1688 )
1689
1690 json(conn, data)
1691 else
1692 _e ->
1693 %{}
1694 end
1695 end
1696
1697 def reports(%{assigns: %{user: user}} = conn, params) do
1698 case CommonAPI.report(user, params) do
1699 {:ok, activity} ->
1700 conn
1701 |> put_view(ReportView)
1702 |> try_render("report.json", %{activity: activity})
1703
1704 {:error, err} ->
1705 conn
1706 |> put_status(:bad_request)
1707 |> json(%{error: err})
1708 end
1709 end
1710
1711 def account_register(
1712 %{assigns: %{app: app}} = conn,
1713 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1714 ) do
1715 params =
1716 params
1717 |> Map.take([
1718 "email",
1719 "captcha_solution",
1720 "captcha_token",
1721 "captcha_answer_data",
1722 "token",
1723 "password"
1724 ])
1725 |> Map.put("nickname", nickname)
1726 |> Map.put("fullname", params["fullname"] || nickname)
1727 |> Map.put("bio", params["bio"] || "")
1728 |> Map.put("confirm", params["password"])
1729
1730 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1731 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1732 json(conn, %{
1733 token_type: "Bearer",
1734 access_token: token.token,
1735 scope: app.scopes,
1736 created_at: Token.Utils.format_created_at(token)
1737 })
1738 else
1739 {:error, errors} ->
1740 conn
1741 |> put_status(400)
1742 |> json(Jason.encode!(errors))
1743 end
1744 end
1745
1746 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1747 conn
1748 |> put_status(400)
1749 |> json(%{error: "Missing parameters"})
1750 end
1751
1752 def account_register(conn, _) do
1753 conn
1754 |> put_status(403)
1755 |> json(%{error: "Invalid credentials"})
1756 end
1757
1758 def conversations(%{assigns: %{user: user}} = conn, params) do
1759 participations = Participation.for_user_with_last_activity_id(user, params)
1760
1761 conversations =
1762 Enum.map(participations, fn participation ->
1763 ConversationView.render("participation.json", %{participation: participation, user: user})
1764 end)
1765
1766 conn
1767 |> add_link_headers(:conversations, participations)
1768 |> json(conversations)
1769 end
1770
1771 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1772 with %Participation{} = participation <-
1773 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1774 {:ok, participation} <- Participation.mark_as_read(participation) do
1775 participation_view =
1776 ConversationView.render("participation.json", %{participation: participation, user: user})
1777
1778 conn
1779 |> json(participation_view)
1780 end
1781 end
1782
1783 def try_render(conn, target, params)
1784 when is_binary(target) do
1785 res = render(conn, target, params)
1786
1787 if res == nil do
1788 conn
1789 |> put_status(501)
1790 |> json(%{error: "Can't display this activity"})
1791 else
1792 res
1793 end
1794 end
1795
1796 def try_render(conn, _, _) do
1797 conn
1798 |> put_status(501)
1799 |> json(%{error: "Can't display this activity"})
1800 end
1801
1802 defp present?(nil), do: false
1803 defp present?(false), do: false
1804 defp present?(_), do: true
1805 end