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