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