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