c54462bb38a51d4a4d3ed04cf39b50b26f494ec0
[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_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
431 limit = 100
432
433 activities =
434 ids
435 |> Enum.take(limit)
436 |> Activity.all_by_ids_with_object()
437 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
438
439 conn
440 |> put_view(StatusView)
441 |> render("index.json", activities: activities, for: user, as: :activity)
442 end
443
444 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
445 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
446 true <- Visibility.visible_for_user?(activity, user) do
447 conn
448 |> put_view(StatusView)
449 |> try_render("status.json", %{activity: activity, for: user})
450 end
451 end
452
453 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
454 with %Activity{} = activity <- Activity.get_by_id(id),
455 activities <-
456 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
457 "blocking_user" => user,
458 "user" => user,
459 "exclude_id" => activity.id
460 }),
461 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
462 result = %{
463 ancestors:
464 StatusView.render(
465 "index.json",
466 for: user,
467 activities: grouped_activities[true] || [],
468 as: :activity
469 )
470 |> Enum.reverse(),
471 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
472 descendants:
473 StatusView.render(
474 "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(id),
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_statuses, 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", %{activity: activity, for: user, as: :activity})
613 end
614 end
615 end
616
617 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
618 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
619 json(conn, %{})
620 else
621 _e -> render_error(conn, :forbidden, "Can't delete this post")
622 end
623 end
624
625 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
626 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
627 %Activity{} = announce <- Activity.normalize(announce.data) do
628 conn
629 |> put_view(StatusView)
630 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
631 end
632 end
633
634 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
635 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
636 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
637 conn
638 |> put_view(StatusView)
639 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
640 end
641 end
642
643 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
644 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
645 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
646 conn
647 |> put_view(StatusView)
648 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
649 end
650 end
651
652 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
653 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
654 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
655 conn
656 |> put_view(StatusView)
657 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
658 end
659 end
660
661 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
662 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
663 conn
664 |> put_view(StatusView)
665 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
666 end
667 end
668
669 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
670 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
671 conn
672 |> put_view(StatusView)
673 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 end
675 end
676
677 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
678 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
679 %User{} = user <- User.get_cached_by_nickname(user.nickname),
680 true <- Visibility.visible_for_user?(activity, user),
681 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
682 conn
683 |> put_view(StatusView)
684 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
685 end
686 end
687
688 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
689 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
690 %User{} = user <- User.get_cached_by_nickname(user.nickname),
691 true <- Visibility.visible_for_user?(activity, user),
692 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
693 conn
694 |> put_view(StatusView)
695 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
696 end
697 end
698
699 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
700 activity = Activity.get_by_id(id)
701
702 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
703 conn
704 |> put_view(StatusView)
705 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
706 end
707 end
708
709 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
710 activity = Activity.get_by_id(id)
711
712 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
713 conn
714 |> put_view(StatusView)
715 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
716 end
717 end
718
719 def notifications(%{assigns: %{user: user}} = conn, params) do
720 notifications = MastodonAPI.get_notifications(user, params)
721
722 conn
723 |> add_link_headers(:notifications, notifications)
724 |> put_view(NotificationView)
725 |> render("index.json", %{notifications: notifications, for: user})
726 end
727
728 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
729 with {:ok, notification} <- Notification.get(user, id) do
730 conn
731 |> put_view(NotificationView)
732 |> render("show.json", %{notification: notification, for: user})
733 else
734 {:error, reason} ->
735 conn
736 |> put_status(:forbidden)
737 |> json(%{"error" => reason})
738 end
739 end
740
741 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
742 Notification.clear(user)
743 json(conn, %{})
744 end
745
746 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
747 with {:ok, _notif} <- Notification.dismiss(user, id) do
748 json(conn, %{})
749 else
750 {:error, reason} ->
751 conn
752 |> put_status(:forbidden)
753 |> json(%{"error" => reason})
754 end
755 end
756
757 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
758 Notification.destroy_multiple(user, ids)
759 json(conn, %{})
760 end
761
762 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
763 id = List.wrap(id)
764 q = from(u in User, where: u.id in ^id)
765 targets = Repo.all(q)
766
767 conn
768 |> put_view(AccountView)
769 |> render("relationships.json", %{user: user, targets: targets})
770 end
771
772 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
773 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
774
775 def update_media(%{assigns: %{user: user}} = conn, data) do
776 with %Object{} = object <- Repo.get(Object, data["id"]),
777 true <- Object.authorize_mutation(object, user),
778 true <- is_binary(data["description"]),
779 description <- data["description"] do
780 new_data = %{object.data | "name" => description}
781
782 {:ok, _} =
783 object
784 |> Object.change(%{data: new_data})
785 |> Repo.update()
786
787 attachment_data = Map.put(new_data, "id", object.id)
788
789 conn
790 |> put_view(StatusView)
791 |> render("attachment.json", %{attachment: attachment_data})
792 end
793 end
794
795 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
796 with {:ok, object} <-
797 ActivityPub.upload(
798 file,
799 actor: User.ap_id(user),
800 description: Map.get(data, "description")
801 ) do
802 attachment_data = Map.put(object.data, "id", object.id)
803
804 conn
805 |> put_view(StatusView)
806 |> render("attachment.json", %{attachment: attachment_data})
807 end
808 end
809
810 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
811 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
812 %{} = attachment_data <- Map.put(object.data, "id", object.id),
813 %{type: type} = rendered <-
814 StatusView.render("attachment.json", %{attachment: attachment_data}) do
815 # Reject if not an image
816 if type == "image" do
817 # Sure!
818 # Save to the user's info
819 info_changeset = User.Info.mascot_update(user.info, rendered)
820
821 user_changeset =
822 user
823 |> Changeset.change()
824 |> Changeset.put_embed(:info, info_changeset)
825
826 {:ok, _user} = User.update_and_set_cache(user_changeset)
827
828 conn
829 |> json(rendered)
830 else
831 render_error(conn, :unsupported_media_type, "mascots can only be images")
832 end
833 end
834 end
835
836 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
837 mascot = User.get_mascot(user)
838
839 conn
840 |> json(mascot)
841 end
842
843 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
844 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
845 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
846 q = from(u in User, where: u.ap_id in ^likes)
847
848 users =
849 Repo.all(q)
850 |> Enum.filter(&(not User.blocks?(user, &1)))
851
852 conn
853 |> put_view(AccountView)
854 |> render("accounts.json", %{for: user, users: users, as: :user})
855 else
856 _ -> json(conn, [])
857 end
858 end
859
860 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
861 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
862 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
863 q = from(u in User, where: u.ap_id in ^announces)
864
865 users =
866 Repo.all(q)
867 |> Enum.filter(&(not User.blocks?(user, &1)))
868
869 conn
870 |> put_view(AccountView)
871 |> render("accounts.json", %{for: user, users: users, as: :user})
872 else
873 _ -> json(conn, [])
874 end
875 end
876
877 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
878 local_only = params["local"] in [true, "True", "true", "1"]
879
880 tags =
881 [params["tag"], params["any"]]
882 |> List.flatten()
883 |> Enum.uniq()
884 |> Enum.filter(& &1)
885 |> Enum.map(&String.downcase(&1))
886
887 tag_all =
888 params["all"] ||
889 []
890 |> Enum.map(&String.downcase(&1))
891
892 tag_reject =
893 params["none"] ||
894 []
895 |> Enum.map(&String.downcase(&1))
896
897 activities =
898 params
899 |> Map.put("type", "Create")
900 |> Map.put("local_only", local_only)
901 |> Map.put("blocking_user", user)
902 |> Map.put("muting_user", user)
903 |> Map.put("user", user)
904 |> Map.put("tag", tags)
905 |> Map.put("tag_all", tag_all)
906 |> Map.put("tag_reject", tag_reject)
907 |> ActivityPub.fetch_public_activities()
908 |> Enum.reverse()
909
910 conn
911 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
912 |> put_view(StatusView)
913 |> render("index.json", %{activities: activities, for: user, as: :activity})
914 end
915
916 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
917 with %User{} = user <- User.get_cached_by_id(id),
918 followers <- MastodonAPI.get_followers(user, params) do
919 followers =
920 cond do
921 for_user && user.id == for_user.id -> followers
922 user.info.hide_followers -> []
923 true -> followers
924 end
925
926 conn
927 |> add_link_headers(:followers, followers, user)
928 |> put_view(AccountView)
929 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
930 end
931 end
932
933 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
934 with %User{} = user <- User.get_cached_by_id(id),
935 followers <- MastodonAPI.get_friends(user, params) do
936 followers =
937 cond do
938 for_user && user.id == for_user.id -> followers
939 user.info.hide_follows -> []
940 true -> followers
941 end
942
943 conn
944 |> add_link_headers(:following, followers, user)
945 |> put_view(AccountView)
946 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
947 end
948 end
949
950 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
951 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
952 conn
953 |> put_view(AccountView)
954 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
955 end
956 end
957
958 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
959 with %User{} = follower <- User.get_cached_by_id(id),
960 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
961 conn
962 |> put_view(AccountView)
963 |> render("relationship.json", %{user: followed, target: follower})
964 else
965 {:error, message} ->
966 conn
967 |> put_status(:forbidden)
968 |> json(%{error: message})
969 end
970 end
971
972 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
973 with %User{} = follower <- User.get_cached_by_id(id),
974 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
975 conn
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: followed, target: follower})
978 else
979 {:error, message} ->
980 conn
981 |> put_status(:forbidden)
982 |> json(%{error: message})
983 end
984 end
985
986 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
987 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
988 {_, true} <- {:followed, follower.id != followed.id},
989 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
990 conn
991 |> put_view(AccountView)
992 |> render("relationship.json", %{user: follower, target: followed})
993 else
994 {:followed, _} ->
995 {:error, :not_found}
996
997 {:error, message} ->
998 conn
999 |> put_status(:forbidden)
1000 |> json(%{error: message})
1001 end
1002 end
1003
1004 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1005 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1006 {_, true} <- {:followed, follower.id != followed.id},
1007 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1008 conn
1009 |> put_view(AccountView)
1010 |> render("account.json", %{user: followed, for: follower})
1011 else
1012 {:followed, _} ->
1013 {:error, :not_found}
1014
1015 {:error, message} ->
1016 conn
1017 |> put_status(:forbidden)
1018 |> json(%{error: message})
1019 end
1020 end
1021
1022 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1023 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1024 {_, true} <- {:followed, follower.id != followed.id},
1025 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1026 conn
1027 |> put_view(AccountView)
1028 |> render("relationship.json", %{user: follower, target: followed})
1029 else
1030 {:followed, _} ->
1031 {:error, :not_found}
1032
1033 error ->
1034 error
1035 end
1036 end
1037
1038 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1039 notifications =
1040 if Map.has_key?(params, "notifications"),
1041 do: params["notifications"] in [true, "True", "true", "1"],
1042 else: true
1043
1044 with %User{} = muted <- User.get_cached_by_id(id),
1045 {:ok, muter} <- User.mute(muter, muted, notifications) do
1046 conn
1047 |> put_view(AccountView)
1048 |> render("relationship.json", %{user: muter, target: muted})
1049 else
1050 {:error, message} ->
1051 conn
1052 |> put_status(:forbidden)
1053 |> json(%{error: message})
1054 end
1055 end
1056
1057 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1058 with %User{} = muted <- User.get_cached_by_id(id),
1059 {:ok, muter} <- User.unmute(muter, muted) do
1060 conn
1061 |> put_view(AccountView)
1062 |> render("relationship.json", %{user: muter, target: muted})
1063 else
1064 {:error, message} ->
1065 conn
1066 |> put_status(:forbidden)
1067 |> json(%{error: message})
1068 end
1069 end
1070
1071 def mutes(%{assigns: %{user: user}} = conn, _) do
1072 with muted_accounts <- User.muted_users(user) do
1073 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1074 json(conn, res)
1075 end
1076 end
1077
1078 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1079 with %User{} = blocked <- User.get_cached_by_id(id),
1080 {:ok, blocker} <- User.block(blocker, blocked),
1081 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1082 conn
1083 |> put_view(AccountView)
1084 |> render("relationship.json", %{user: blocker, target: blocked})
1085 else
1086 {:error, message} ->
1087 conn
1088 |> put_status(:forbidden)
1089 |> json(%{error: message})
1090 end
1091 end
1092
1093 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1094 with %User{} = blocked <- User.get_cached_by_id(id),
1095 {:ok, blocker} <- User.unblock(blocker, blocked),
1096 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1097 conn
1098 |> put_view(AccountView)
1099 |> render("relationship.json", %{user: blocker, target: blocked})
1100 else
1101 {:error, message} ->
1102 conn
1103 |> put_status(:forbidden)
1104 |> json(%{error: message})
1105 end
1106 end
1107
1108 def blocks(%{assigns: %{user: user}} = conn, _) do
1109 with blocked_accounts <- User.blocked_users(user) do
1110 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1111 json(conn, res)
1112 end
1113 end
1114
1115 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1116 json(conn, info.domain_blocks || [])
1117 end
1118
1119 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1120 User.block_domain(blocker, domain)
1121 json(conn, %{})
1122 end
1123
1124 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1125 User.unblock_domain(blocker, domain)
1126 json(conn, %{})
1127 end
1128
1129 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1130 with %User{} = subscription_target <- User.get_cached_by_id(id),
1131 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1132 conn
1133 |> put_view(AccountView)
1134 |> render("relationship.json", %{user: user, target: subscription_target})
1135 else
1136 {:error, message} ->
1137 conn
1138 |> put_status(:forbidden)
1139 |> json(%{error: message})
1140 end
1141 end
1142
1143 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1144 with %User{} = subscription_target <- User.get_cached_by_id(id),
1145 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1146 conn
1147 |> put_view(AccountView)
1148 |> render("relationship.json", %{user: user, target: subscription_target})
1149 else
1150 {:error, message} ->
1151 conn
1152 |> put_status(:forbidden)
1153 |> json(%{error: message})
1154 end
1155 end
1156
1157 def favourites(%{assigns: %{user: user}} = conn, params) do
1158 params =
1159 params
1160 |> Map.put("type", "Create")
1161 |> Map.put("favorited_by", user.ap_id)
1162 |> Map.put("blocking_user", user)
1163
1164 activities =
1165 ActivityPub.fetch_activities([], params)
1166 |> Enum.reverse()
1167
1168 conn
1169 |> add_link_headers(:favourites, activities)
1170 |> put_view(StatusView)
1171 |> render("index.json", %{activities: activities, for: user, as: :activity})
1172 end
1173
1174 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1175 with %User{} = user <- User.get_by_id(id),
1176 false <- user.info.hide_favorites do
1177 params =
1178 params
1179 |> Map.put("type", "Create")
1180 |> Map.put("favorited_by", user.ap_id)
1181 |> Map.put("blocking_user", for_user)
1182
1183 recipients =
1184 if for_user do
1185 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1186 else
1187 [Pleroma.Constants.as_public()]
1188 end
1189
1190 activities =
1191 recipients
1192 |> ActivityPub.fetch_activities(params)
1193 |> Enum.reverse()
1194
1195 conn
1196 |> add_link_headers(:favourites, activities)
1197 |> put_view(StatusView)
1198 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1199 else
1200 nil -> {:error, :not_found}
1201 true -> render_error(conn, :forbidden, "Can't get favorites")
1202 end
1203 end
1204
1205 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1206 user = User.get_cached_by_id(user.id)
1207
1208 bookmarks =
1209 Bookmark.for_user_query(user.id)
1210 |> Pagination.fetch_paginated(params)
1211
1212 activities =
1213 bookmarks
1214 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1215
1216 conn
1217 |> add_link_headers(:bookmarks, bookmarks)
1218 |> put_view(StatusView)
1219 |> render("index.json", %{activities: activities, for: user, as: :activity})
1220 end
1221
1222 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1223 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1224 res = ListView.render("lists.json", lists: lists)
1225 json(conn, res)
1226 end
1227
1228 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1229 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1230 params =
1231 params
1232 |> Map.put("type", "Create")
1233 |> Map.put("blocking_user", user)
1234 |> Map.put("user", user)
1235 |> Map.put("muting_user", user)
1236
1237 # we must filter the following list for the user to avoid leaking statuses the user
1238 # does not actually have permission to see (for more info, peruse security issue #270).
1239 activities =
1240 following
1241 |> Enum.filter(fn x -> x in user.following end)
1242 |> ActivityPub.fetch_activities_bounded(following, params)
1243 |> Enum.reverse()
1244
1245 conn
1246 |> put_view(StatusView)
1247 |> render("index.json", %{activities: activities, for: user, as: :activity})
1248 else
1249 _e -> render_error(conn, :forbidden, "Error.")
1250 end
1251 end
1252
1253 def index(%{assigns: %{user: user}} = conn, _params) do
1254 token = get_session(conn, :oauth_token)
1255
1256 if user && token do
1257 mastodon_emoji = mastodonized_emoji()
1258
1259 limit = Config.get([:instance, :limit])
1260
1261 accounts =
1262 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1263
1264 initial_state =
1265 %{
1266 meta: %{
1267 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1268 access_token: token,
1269 locale: "en",
1270 domain: Pleroma.Web.Endpoint.host(),
1271 admin: "1",
1272 me: "#{user.id}",
1273 unfollow_modal: false,
1274 boost_modal: false,
1275 delete_modal: true,
1276 auto_play_gif: false,
1277 display_sensitive_media: false,
1278 reduce_motion: false,
1279 max_toot_chars: limit,
1280 mascot: User.get_mascot(user)["url"]
1281 },
1282 poll_limits: Config.get([:instance, :poll_limits]),
1283 rights: %{
1284 delete_others_notice: present?(user.info.is_moderator),
1285 admin: present?(user.info.is_admin)
1286 },
1287 compose: %{
1288 me: "#{user.id}",
1289 default_privacy: user.info.default_scope,
1290 default_sensitive: false,
1291 allow_content_types: Config.get([:instance, :allowed_post_formats])
1292 },
1293 media_attachments: %{
1294 accept_content_types: [
1295 ".jpg",
1296 ".jpeg",
1297 ".png",
1298 ".gif",
1299 ".webm",
1300 ".mp4",
1301 ".m4v",
1302 "image\/jpeg",
1303 "image\/png",
1304 "image\/gif",
1305 "video\/webm",
1306 "video\/mp4"
1307 ]
1308 },
1309 settings:
1310 user.info.settings ||
1311 %{
1312 onboarded: true,
1313 home: %{
1314 shows: %{
1315 reblog: true,
1316 reply: true
1317 }
1318 },
1319 notifications: %{
1320 alerts: %{
1321 follow: true,
1322 favourite: true,
1323 reblog: true,
1324 mention: true
1325 },
1326 shows: %{
1327 follow: true,
1328 favourite: true,
1329 reblog: true,
1330 mention: true
1331 },
1332 sounds: %{
1333 follow: true,
1334 favourite: true,
1335 reblog: true,
1336 mention: true
1337 }
1338 }
1339 },
1340 push_subscription: nil,
1341 accounts: accounts,
1342 custom_emojis: mastodon_emoji,
1343 char_limit: limit
1344 }
1345 |> Jason.encode!()
1346
1347 conn
1348 |> put_layout(false)
1349 |> put_view(MastodonView)
1350 |> render("index.html", %{initial_state: initial_state})
1351 else
1352 conn
1353 |> put_session(:return_to, conn.request_path)
1354 |> redirect(to: "/web/login")
1355 end
1356 end
1357
1358 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1359 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1360
1361 with changeset <- Changeset.change(user),
1362 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1363 {:ok, _user} <- User.update_and_set_cache(changeset) do
1364 json(conn, %{})
1365 else
1366 e ->
1367 conn
1368 |> put_status(:internal_server_error)
1369 |> json(%{error: inspect(e)})
1370 end
1371 end
1372
1373 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1374 redirect(conn, to: local_mastodon_root_path(conn))
1375 end
1376
1377 @doc "Local Mastodon FE login init action"
1378 def login(conn, %{"code" => auth_token}) do
1379 with {:ok, app} <- get_or_make_app(),
1380 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1381 {:ok, token} <- Token.exchange_token(app, auth) do
1382 conn
1383 |> put_session(:oauth_token, token.token)
1384 |> redirect(to: local_mastodon_root_path(conn))
1385 end
1386 end
1387
1388 @doc "Local Mastodon FE callback action"
1389 def login(conn, _) do
1390 with {:ok, app} <- get_or_make_app() do
1391 path =
1392 o_auth_path(
1393 conn,
1394 :authorize,
1395 response_type: "code",
1396 client_id: app.client_id,
1397 redirect_uri: ".",
1398 scope: Enum.join(app.scopes, " ")
1399 )
1400
1401 redirect(conn, to: path)
1402 end
1403 end
1404
1405 defp local_mastodon_root_path(conn) do
1406 case get_session(conn, :return_to) do
1407 nil ->
1408 mastodon_api_path(conn, :index, ["getting-started"])
1409
1410 return_to ->
1411 delete_session(conn, :return_to)
1412 return_to
1413 end
1414 end
1415
1416 defp get_or_make_app do
1417 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1418 scopes = ["read", "write", "follow", "push"]
1419
1420 with %App{} = app <- Repo.get_by(App, find_attrs) do
1421 {:ok, app} =
1422 if app.scopes == scopes do
1423 {:ok, app}
1424 else
1425 app
1426 |> Changeset.change(%{scopes: scopes})
1427 |> Repo.update()
1428 end
1429
1430 {:ok, app}
1431 else
1432 _e ->
1433 cs =
1434 App.register_changeset(
1435 %App{},
1436 Map.put(find_attrs, :scopes, scopes)
1437 )
1438
1439 Repo.insert(cs)
1440 end
1441 end
1442
1443 def logout(conn, _) do
1444 conn
1445 |> clear_session
1446 |> redirect(to: "/")
1447 end
1448
1449 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1450 Logger.debug("Unimplemented, returning unmodified relationship")
1451
1452 with %User{} = target <- User.get_cached_by_id(id) do
1453 conn
1454 |> put_view(AccountView)
1455 |> render("relationship.json", %{user: user, target: target})
1456 end
1457 end
1458
1459 def empty_array(conn, _) do
1460 Logger.debug("Unimplemented, returning an empty array")
1461 json(conn, [])
1462 end
1463
1464 def empty_object(conn, _) do
1465 Logger.debug("Unimplemented, returning an empty object")
1466 json(conn, %{})
1467 end
1468
1469 def get_filters(%{assigns: %{user: user}} = conn, _) do
1470 filters = Filter.get_filters(user)
1471 res = FilterView.render("filters.json", filters: filters)
1472 json(conn, res)
1473 end
1474
1475 def create_filter(
1476 %{assigns: %{user: user}} = conn,
1477 %{"phrase" => phrase, "context" => context} = params
1478 ) do
1479 query = %Filter{
1480 user_id: user.id,
1481 phrase: phrase,
1482 context: context,
1483 hide: Map.get(params, "irreversible", false),
1484 whole_word: Map.get(params, "boolean", true)
1485 # expires_at
1486 }
1487
1488 {:ok, response} = Filter.create(query)
1489 res = FilterView.render("filter.json", filter: response)
1490 json(conn, res)
1491 end
1492
1493 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1494 filter = Filter.get(filter_id, user)
1495 res = FilterView.render("filter.json", filter: filter)
1496 json(conn, res)
1497 end
1498
1499 def update_filter(
1500 %{assigns: %{user: user}} = conn,
1501 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1502 ) do
1503 query = %Filter{
1504 user_id: user.id,
1505 filter_id: filter_id,
1506 phrase: phrase,
1507 context: context,
1508 hide: Map.get(params, "irreversible", nil),
1509 whole_word: Map.get(params, "boolean", true)
1510 # expires_at
1511 }
1512
1513 {:ok, response} = Filter.update(query)
1514 res = FilterView.render("filter.json", filter: response)
1515 json(conn, res)
1516 end
1517
1518 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1519 query = %Filter{
1520 user_id: user.id,
1521 filter_id: filter_id
1522 }
1523
1524 {:ok, _} = Filter.delete(query)
1525 json(conn, %{})
1526 end
1527
1528 def suggestions(%{assigns: %{user: user}} = conn, _) do
1529 suggestions = Config.get(:suggestions)
1530
1531 if Keyword.get(suggestions, :enabled, false) do
1532 api = Keyword.get(suggestions, :third_party_engine, "")
1533 timeout = Keyword.get(suggestions, :timeout, 5000)
1534 limit = Keyword.get(suggestions, :limit, 23)
1535
1536 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1537
1538 user = user.nickname
1539
1540 url =
1541 api
1542 |> String.replace("{{host}}", host)
1543 |> String.replace("{{user}}", user)
1544
1545 with {:ok, %{status: 200, body: body}} <-
1546 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1547 {:ok, data} <- Jason.decode(body) do
1548 data =
1549 data
1550 |> Enum.slice(0, limit)
1551 |> Enum.map(fn x ->
1552 x
1553 |> Map.put("id", fetch_suggestion_id(x))
1554 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1555 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1556 end)
1557
1558 json(conn, data)
1559 else
1560 e ->
1561 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1562 end
1563 else
1564 json(conn, [])
1565 end
1566 end
1567
1568 defp fetch_suggestion_id(attrs) do
1569 case User.get_or_fetch(attrs["acct"]) do
1570 {:ok, %User{id: id}} -> id
1571 _ -> 0
1572 end
1573 end
1574
1575 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1576 with %Activity{} = activity <- Activity.get_by_id(status_id),
1577 true <- Visibility.visible_for_user?(activity, user) do
1578 data =
1579 StatusView.render(
1580 "card.json",
1581 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1582 )
1583
1584 json(conn, data)
1585 else
1586 _e ->
1587 %{}
1588 end
1589 end
1590
1591 def reports(%{assigns: %{user: user}} = conn, params) do
1592 case CommonAPI.report(user, params) do
1593 {:ok, activity} ->
1594 conn
1595 |> put_view(ReportView)
1596 |> try_render("report.json", %{activity: activity})
1597
1598 {:error, err} ->
1599 conn
1600 |> put_status(:bad_request)
1601 |> json(%{error: err})
1602 end
1603 end
1604
1605 def account_register(
1606 %{assigns: %{app: app}} = conn,
1607 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1608 ) do
1609 params =
1610 params
1611 |> Map.take([
1612 "email",
1613 "captcha_solution",
1614 "captcha_token",
1615 "captcha_answer_data",
1616 "token",
1617 "password"
1618 ])
1619 |> Map.put("nickname", nickname)
1620 |> Map.put("fullname", params["fullname"] || nickname)
1621 |> Map.put("bio", params["bio"] || "")
1622 |> Map.put("confirm", params["password"])
1623
1624 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1625 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1626 json(conn, %{
1627 token_type: "Bearer",
1628 access_token: token.token,
1629 scope: app.scopes,
1630 created_at: Token.Utils.format_created_at(token)
1631 })
1632 else
1633 {:error, errors} ->
1634 conn
1635 |> put_status(:bad_request)
1636 |> json(errors)
1637 end
1638 end
1639
1640 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1641 render_error(conn, :bad_request, "Missing parameters")
1642 end
1643
1644 def account_register(conn, _) do
1645 render_error(conn, :forbidden, "Invalid credentials")
1646 end
1647
1648 def conversations(%{assigns: %{user: user}} = conn, params) do
1649 participations = Participation.for_user_with_last_activity_id(user, params)
1650
1651 conversations =
1652 Enum.map(participations, fn participation ->
1653 ConversationView.render("participation.json", %{participation: participation, for: user})
1654 end)
1655
1656 conn
1657 |> add_link_headers(:conversations, participations)
1658 |> json(conversations)
1659 end
1660
1661 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1662 with %Participation{} = participation <-
1663 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1664 {:ok, participation} <- Participation.mark_as_read(participation) do
1665 participation_view =
1666 ConversationView.render("participation.json", %{participation: participation, for: user})
1667
1668 conn
1669 |> json(participation_view)
1670 end
1671 end
1672
1673 def password_reset(conn, params) do
1674 nickname_or_email = params["email"] || params["nickname"]
1675
1676 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1677 conn
1678 |> put_status(:no_content)
1679 |> json("")
1680 else
1681 {:error, "unknown user"} ->
1682 send_resp(conn, :not_found, "")
1683
1684 {:error, _} ->
1685 send_resp(conn, :bad_request, "")
1686 end
1687 end
1688
1689 def account_confirmation_resend(conn, params) do
1690 nickname_or_email = params["email"] || params["nickname"]
1691
1692 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1693 {:ok, _} <- User.try_send_confirmation_email(user) do
1694 conn
1695 |> json_response(:no_content, "")
1696 end
1697 end
1698
1699 def try_render(conn, target, params)
1700 when is_binary(target) do
1701 case render(conn, target, params) do
1702 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1703 res -> res
1704 end
1705 end
1706
1707 def try_render(conn, _, _) do
1708 render_error(conn, :not_implemented, "Can't display this activity")
1709 end
1710
1711 defp present?(nil), do: false
1712 defp present?(false), do: false
1713 defp present?(_), do: true
1714 end