Merge branch 'feature/polls-2-electric-boogalo' into 'develop'
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7 alias Ecto.Changeset
8 alias Pleroma.Activity
9 alias Pleroma.Bookmark
10 alias Pleroma.Config
11 alias Pleroma.Conversation.Participation
12 alias Pleroma.Filter
13 alias Pleroma.Formatter
14 alias Pleroma.HTTP
15 alias Pleroma.Notification
16 alias Pleroma.Object
17 alias Pleroma.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 initial_state =
1415 %{
1416 meta: %{
1417 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1418 access_token: token,
1419 locale: "en",
1420 domain: Pleroma.Web.Endpoint.host(),
1421 admin: "1",
1422 me: "#{user.id}",
1423 unfollow_modal: false,
1424 boost_modal: false,
1425 delete_modal: true,
1426 auto_play_gif: false,
1427 display_sensitive_media: false,
1428 reduce_motion: false,
1429 max_toot_chars: limit,
1430 mascot: User.get_mascot(user)["url"]
1431 },
1432 poll_limits: Config.get([:instance, :poll_limits]),
1433 rights: %{
1434 delete_others_notice: present?(user.info.is_moderator),
1435 admin: present?(user.info.is_admin)
1436 },
1437 compose: %{
1438 me: "#{user.id}",
1439 default_privacy: user.info.default_scope,
1440 default_sensitive: false,
1441 allow_content_types: Config.get([:instance, :allowed_post_formats])
1442 },
1443 media_attachments: %{
1444 accept_content_types: [
1445 ".jpg",
1446 ".jpeg",
1447 ".png",
1448 ".gif",
1449 ".webm",
1450 ".mp4",
1451 ".m4v",
1452 "image\/jpeg",
1453 "image\/png",
1454 "image\/gif",
1455 "video\/webm",
1456 "video\/mp4"
1457 ]
1458 },
1459 settings:
1460 user.info.settings ||
1461 %{
1462 onboarded: true,
1463 home: %{
1464 shows: %{
1465 reblog: true,
1466 reply: true
1467 }
1468 },
1469 notifications: %{
1470 alerts: %{
1471 follow: true,
1472 favourite: true,
1473 reblog: true,
1474 mention: true
1475 },
1476 shows: %{
1477 follow: true,
1478 favourite: true,
1479 reblog: true,
1480 mention: true
1481 },
1482 sounds: %{
1483 follow: true,
1484 favourite: true,
1485 reblog: true,
1486 mention: true
1487 }
1488 }
1489 },
1490 push_subscription: nil,
1491 accounts: accounts,
1492 custom_emojis: mastodon_emoji,
1493 char_limit: limit
1494 }
1495 |> Jason.encode!()
1496
1497 conn
1498 |> put_layout(false)
1499 |> put_view(MastodonView)
1500 |> render("index.html", %{initial_state: initial_state})
1501 else
1502 conn
1503 |> put_session(:return_to, conn.request_path)
1504 |> redirect(to: "/web/login")
1505 end
1506 end
1507
1508 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1509 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1510
1511 with changeset <- Ecto.Changeset.change(user),
1512 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1513 {:ok, _user} <- User.update_and_set_cache(changeset) do
1514 json(conn, %{})
1515 else
1516 e ->
1517 conn
1518 |> put_resp_content_type("application/json")
1519 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1520 end
1521 end
1522
1523 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1524 redirect(conn, to: local_mastodon_root_path(conn))
1525 end
1526
1527 @doc "Local Mastodon FE login init action"
1528 def login(conn, %{"code" => auth_token}) do
1529 with {:ok, app} <- get_or_make_app(),
1530 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1531 {:ok, token} <- Token.exchange_token(app, auth) do
1532 conn
1533 |> put_session(:oauth_token, token.token)
1534 |> redirect(to: local_mastodon_root_path(conn))
1535 end
1536 end
1537
1538 @doc "Local Mastodon FE callback action"
1539 def login(conn, _) do
1540 with {:ok, app} <- get_or_make_app() do
1541 path =
1542 o_auth_path(
1543 conn,
1544 :authorize,
1545 response_type: "code",
1546 client_id: app.client_id,
1547 redirect_uri: ".",
1548 scope: Enum.join(app.scopes, " ")
1549 )
1550
1551 redirect(conn, to: path)
1552 end
1553 end
1554
1555 defp local_mastodon_root_path(conn) do
1556 case get_session(conn, :return_to) do
1557 nil ->
1558 mastodon_api_path(conn, :index, ["getting-started"])
1559
1560 return_to ->
1561 delete_session(conn, :return_to)
1562 return_to
1563 end
1564 end
1565
1566 defp get_or_make_app do
1567 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1568 scopes = ["read", "write", "follow", "push"]
1569
1570 with %App{} = app <- Repo.get_by(App, find_attrs) do
1571 {:ok, app} =
1572 if app.scopes == scopes do
1573 {:ok, app}
1574 else
1575 app
1576 |> Ecto.Changeset.change(%{scopes: scopes})
1577 |> Repo.update()
1578 end
1579
1580 {:ok, app}
1581 else
1582 _e ->
1583 cs =
1584 App.register_changeset(
1585 %App{},
1586 Map.put(find_attrs, :scopes, scopes)
1587 )
1588
1589 Repo.insert(cs)
1590 end
1591 end
1592
1593 def logout(conn, _) do
1594 conn
1595 |> clear_session
1596 |> redirect(to: "/")
1597 end
1598
1599 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1600 Logger.debug("Unimplemented, returning unmodified relationship")
1601
1602 with %User{} = target <- User.get_cached_by_id(id) do
1603 conn
1604 |> put_view(AccountView)
1605 |> render("relationship.json", %{user: user, target: target})
1606 end
1607 end
1608
1609 def empty_array(conn, _) do
1610 Logger.debug("Unimplemented, returning an empty array")
1611 json(conn, [])
1612 end
1613
1614 def empty_object(conn, _) do
1615 Logger.debug("Unimplemented, returning an empty object")
1616 json(conn, %{})
1617 end
1618
1619 def get_filters(%{assigns: %{user: user}} = conn, _) do
1620 filters = Filter.get_filters(user)
1621 res = FilterView.render("filters.json", filters: filters)
1622 json(conn, res)
1623 end
1624
1625 def create_filter(
1626 %{assigns: %{user: user}} = conn,
1627 %{"phrase" => phrase, "context" => context} = params
1628 ) do
1629 query = %Filter{
1630 user_id: user.id,
1631 phrase: phrase,
1632 context: context,
1633 hide: Map.get(params, "irreversible", false),
1634 whole_word: Map.get(params, "boolean", true)
1635 # expires_at
1636 }
1637
1638 {:ok, response} = Filter.create(query)
1639 res = FilterView.render("filter.json", filter: response)
1640 json(conn, res)
1641 end
1642
1643 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1644 filter = Filter.get(filter_id, user)
1645 res = FilterView.render("filter.json", filter: filter)
1646 json(conn, res)
1647 end
1648
1649 def update_filter(
1650 %{assigns: %{user: user}} = conn,
1651 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1652 ) do
1653 query = %Filter{
1654 user_id: user.id,
1655 filter_id: filter_id,
1656 phrase: phrase,
1657 context: context,
1658 hide: Map.get(params, "irreversible", nil),
1659 whole_word: Map.get(params, "boolean", true)
1660 # expires_at
1661 }
1662
1663 {:ok, response} = Filter.update(query)
1664 res = FilterView.render("filter.json", filter: response)
1665 json(conn, res)
1666 end
1667
1668 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1669 query = %Filter{
1670 user_id: user.id,
1671 filter_id: filter_id
1672 }
1673
1674 {:ok, _} = Filter.delete(query)
1675 json(conn, %{})
1676 end
1677
1678 # fallback action
1679 #
1680 def errors(conn, {:error, %Changeset{} = changeset}) do
1681 error_message =
1682 changeset
1683 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1684 |> Enum.map_join(", ", fn {_k, v} -> v end)
1685
1686 conn
1687 |> put_status(422)
1688 |> json(%{error: error_message})
1689 end
1690
1691 def errors(conn, {:error, :not_found}) do
1692 conn
1693 |> put_status(404)
1694 |> json(%{error: "Record not found"})
1695 end
1696
1697 def errors(conn, _) do
1698 conn
1699 |> put_status(500)
1700 |> json("Something went wrong")
1701 end
1702
1703 def suggestions(%{assigns: %{user: user}} = conn, _) do
1704 suggestions = Config.get(:suggestions)
1705
1706 if Keyword.get(suggestions, :enabled, false) do
1707 api = Keyword.get(suggestions, :third_party_engine, "")
1708 timeout = Keyword.get(suggestions, :timeout, 5000)
1709 limit = Keyword.get(suggestions, :limit, 23)
1710
1711 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1712
1713 user = user.nickname
1714
1715 url =
1716 api
1717 |> String.replace("{{host}}", host)
1718 |> String.replace("{{user}}", user)
1719
1720 with {:ok, %{status: 200, body: body}} <-
1721 HTTP.get(
1722 url,
1723 [],
1724 adapter: [
1725 recv_timeout: timeout,
1726 pool: :default
1727 ]
1728 ),
1729 {:ok, data} <- Jason.decode(body) do
1730 data =
1731 data
1732 |> Enum.slice(0, limit)
1733 |> Enum.map(fn x ->
1734 Map.put(
1735 x,
1736 "id",
1737 case User.get_or_fetch(x["acct"]) do
1738 {:ok, %User{id: id}} -> id
1739 _ -> 0
1740 end
1741 )
1742 end)
1743 |> Enum.map(fn x ->
1744 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1745 end)
1746 |> Enum.map(fn x ->
1747 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1748 end)
1749
1750 conn
1751 |> json(data)
1752 else
1753 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1754 end
1755 else
1756 json(conn, [])
1757 end
1758 end
1759
1760 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1761 with %Activity{} = activity <- Activity.get_by_id(status_id),
1762 true <- Visibility.visible_for_user?(activity, user) do
1763 data =
1764 StatusView.render(
1765 "card.json",
1766 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1767 )
1768
1769 json(conn, data)
1770 else
1771 _e ->
1772 %{}
1773 end
1774 end
1775
1776 def reports(%{assigns: %{user: user}} = conn, params) do
1777 case CommonAPI.report(user, params) do
1778 {:ok, activity} ->
1779 conn
1780 |> put_view(ReportView)
1781 |> try_render("report.json", %{activity: activity})
1782
1783 {:error, err} ->
1784 conn
1785 |> put_status(:bad_request)
1786 |> json(%{error: err})
1787 end
1788 end
1789
1790 def account_register(
1791 %{assigns: %{app: app}} = conn,
1792 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1793 ) do
1794 params =
1795 params
1796 |> Map.take([
1797 "email",
1798 "captcha_solution",
1799 "captcha_token",
1800 "captcha_answer_data",
1801 "token",
1802 "password"
1803 ])
1804 |> Map.put("nickname", nickname)
1805 |> Map.put("fullname", params["fullname"] || nickname)
1806 |> Map.put("bio", params["bio"] || "")
1807 |> Map.put("confirm", params["password"])
1808
1809 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1810 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1811 json(conn, %{
1812 token_type: "Bearer",
1813 access_token: token.token,
1814 scope: app.scopes,
1815 created_at: Token.Utils.format_created_at(token)
1816 })
1817 else
1818 {:error, errors} ->
1819 conn
1820 |> put_status(400)
1821 |> json(Jason.encode!(errors))
1822 end
1823 end
1824
1825 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1826 conn
1827 |> put_status(400)
1828 |> json(%{error: "Missing parameters"})
1829 end
1830
1831 def account_register(conn, _) do
1832 conn
1833 |> put_status(403)
1834 |> json(%{error: "Invalid credentials"})
1835 end
1836
1837 def conversations(%{assigns: %{user: user}} = conn, params) do
1838 participations = Participation.for_user_with_last_activity_id(user, params)
1839
1840 conversations =
1841 Enum.map(participations, fn participation ->
1842 ConversationView.render("participation.json", %{participation: participation, user: user})
1843 end)
1844
1845 conn
1846 |> add_link_headers(:conversations, participations)
1847 |> json(conversations)
1848 end
1849
1850 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1851 with %Participation{} = participation <-
1852 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1853 {:ok, participation} <- Participation.mark_as_read(participation) do
1854 participation_view =
1855 ConversationView.render("participation.json", %{participation: participation, user: user})
1856
1857 conn
1858 |> json(participation_view)
1859 end
1860 end
1861
1862 def try_render(conn, target, params)
1863 when is_binary(target) do
1864 res = render(conn, target, params)
1865
1866 if res == nil do
1867 conn
1868 |> put_status(501)
1869 |> json(%{error: "Can't display this activity"})
1870 else
1871 res
1872 end
1873 end
1874
1875 def try_render(conn, _, _) do
1876 conn
1877 |> put_status(501)
1878 |> json(%{error: "Can't display this activity"})
1879 end
1880
1881 defp present?(nil), do: false
1882 defp present?(false), do: false
1883 defp present?(_), do: true
1884 end