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