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