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