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