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