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