fa768fa9335858c8a694e185204259f077278dd7
[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.RichMedia
48 alias Pleroma.Web.TwitterAPI.TwitterAPI
49
50 alias Pleroma.Web.ControllerHelper
51 import Ecto.Query
52
53 require Logger
54 require Pleroma.Constants
55
56 @rate_limited_relations_actions ~w(follow unfollow)a
57
58 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
59 post_status delete_status)a
60
61 plug(
62 RateLimiter,
63 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
64 when action in ~w(reblog_status unreblog_status)a
65 )
66
67 plug(
68 RateLimiter,
69 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
70 when action in ~w(fav_status unfav_status)a
71 )
72
73 plug(
74 RateLimiter,
75 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
76 )
77
78 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
79 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
80 plug(RateLimiter, :app_account_creation when action == :account_register)
81 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
82 plug(RateLimiter, :password_reset when action == :password_reset)
83 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
84
85 @local_mastodon_name "Mastodon-Local"
86
87 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
88
89 def create_app(conn, params) do
90 scopes = Scopes.fetch_scopes(params, ["read"])
91
92 app_attrs =
93 params
94 |> Map.drop(["scope", "scopes"])
95 |> Map.put("scopes", scopes)
96
97 with cs <- App.register_changeset(%App{}, app_attrs),
98 false <- cs.changes[:client_name] == @local_mastodon_name,
99 {:ok, app} <- Repo.insert(cs) do
100 conn
101 |> put_view(AppView)
102 |> render("show.json", %{app: app})
103 end
104 end
105
106 defp add_if_present(
107 map,
108 params,
109 params_field,
110 map_field,
111 value_function \\ fn x -> {:ok, x} end
112 ) do
113 if Map.has_key?(params, params_field) do
114 case value_function.(params[params_field]) do
115 {:ok, new_value} -> Map.put(map, map_field, new_value)
116 :error -> map
117 end
118 else
119 map
120 end
121 end
122
123 def update_credentials(%{assigns: %{user: user}} = conn, params) do
124 original_user = user
125
126 user_params =
127 %{}
128 |> add_if_present(params, "display_name", :name)
129 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
130 |> add_if_present(params, "avatar", :avatar, fn value ->
131 with %Plug.Upload{} <- value,
132 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
133 {:ok, object.data}
134 else
135 _ -> :error
136 end
137 end)
138
139 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
140
141 user_info_emojis =
142 user.info
143 |> Map.get(:emoji, [])
144 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
145 |> Enum.dedup()
146
147 info_params =
148 [
149 :no_rich_text,
150 :locked,
151 :hide_followers_count,
152 :hide_follows_count,
153 :hide_followers,
154 :hide_follows,
155 :hide_favorites,
156 :show_role,
157 :skip_thread_containment
158 ]
159 |> Enum.reduce(%{}, fn key, acc ->
160 add_if_present(acc, params, to_string(key), key, fn value ->
161 {:ok, ControllerHelper.truthy_param?(value)}
162 end)
163 end)
164 |> add_if_present(params, "default_scope", :default_scope)
165 |> add_if_present(params, "fields", :fields, fn fields ->
166 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
167
168 {:ok, fields}
169 end)
170 |> add_if_present(params, "fields", :raw_fields)
171 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
172 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
173 end)
174 |> add_if_present(params, "header", :banner, fn value ->
175 with %Plug.Upload{} <- value,
176 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
177 {:ok, object.data}
178 else
179 _ -> :error
180 end
181 end)
182 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
183 with %Plug.Upload{} <- value,
184 {:ok, object} <- ActivityPub.upload(value, type: :background) do
185 {:ok, object.data}
186 else
187 _ -> :error
188 end
189 end)
190 |> Map.put(:emoji, user_info_emojis)
191
192 info_cng = User.Info.profile_update(user.info, info_params)
193
194 with changeset <- User.update_changeset(user, user_params),
195 changeset <- Changeset.put_embed(changeset, :info, info_cng),
196 {:ok, user} <- User.update_and_set_cache(changeset) do
197 if original_user != user do
198 CommonAPI.update(user)
199 end
200
201 json(
202 conn,
203 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
204 )
205 else
206 _e -> render_error(conn, :forbidden, "Invalid request")
207 end
208 end
209
210 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
211 change = Changeset.change(user, %{avatar: nil})
212 {:ok, user} = User.update_and_set_cache(change)
213 CommonAPI.update(user)
214
215 json(conn, %{url: nil})
216 end
217
218 def update_avatar(%{assigns: %{user: user}} = conn, params) do
219 {:ok, object} = ActivityPub.upload(params, type: :avatar)
220 change = Changeset.change(user, %{avatar: object.data})
221 {:ok, user} = User.update_and_set_cache(change)
222 CommonAPI.update(user)
223 %{"url" => [%{"href" => href} | _]} = object.data
224
225 json(conn, %{url: href})
226 end
227
228 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
229 with new_info <- %{"banner" => %{}},
230 info_cng <- User.Info.profile_update(user.info, new_info),
231 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
232 {:ok, user} <- User.update_and_set_cache(changeset) do
233 CommonAPI.update(user)
234
235 json(conn, %{url: nil})
236 end
237 end
238
239 def update_banner(%{assigns: %{user: user}} = conn, params) do
240 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
241 new_info <- %{"banner" => object.data},
242 info_cng <- User.Info.profile_update(user.info, new_info),
243 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
244 {:ok, user} <- User.update_and_set_cache(changeset) do
245 CommonAPI.update(user)
246 %{"url" => [%{"href" => href} | _]} = object.data
247
248 json(conn, %{url: href})
249 end
250 end
251
252 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
253 with new_info <- %{"background" => %{}},
254 info_cng <- User.Info.profile_update(user.info, new_info),
255 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
256 {:ok, _user} <- User.update_and_set_cache(changeset) do
257 json(conn, %{url: nil})
258 end
259 end
260
261 def update_background(%{assigns: %{user: user}} = conn, params) do
262 with {:ok, object} <- ActivityPub.upload(params, type: :background),
263 new_info <- %{"background" => object.data},
264 info_cng <- User.Info.profile_update(user.info, new_info),
265 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
266 {:ok, _user} <- User.update_and_set_cache(changeset) do
267 %{"url" => [%{"href" => href} | _]} = object.data
268
269 json(conn, %{url: href})
270 end
271 end
272
273 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
274 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
275
276 account =
277 AccountView.render("account.json", %{
278 user: user,
279 for: user,
280 with_pleroma_settings: true,
281 with_chat_token: chat_token
282 })
283
284 json(conn, account)
285 end
286
287 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
288 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
289 conn
290 |> put_view(AppView)
291 |> render("short.json", %{app: app})
292 end
293 end
294
295 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
296 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
297 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
298 account = AccountView.render("account.json", %{user: user, for: for_user})
299 json(conn, account)
300 else
301 _e -> render_error(conn, :not_found, "Can't find user")
302 end
303 end
304
305 @mastodon_api_level "2.7.2"
306
307 def masto_instance(conn, _params) do
308 instance = Config.get(:instance)
309
310 response = %{
311 uri: Web.base_url(),
312 title: Keyword.get(instance, :name),
313 description: Keyword.get(instance, :description),
314 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
315 email: Keyword.get(instance, :email),
316 urls: %{
317 streaming_api: Pleroma.Web.Endpoint.websocket_url()
318 },
319 stats: Stats.get_stats(),
320 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
321 languages: ["en"],
322 registrations: Pleroma.Config.get([:instance, :registrations_open]),
323 # Extra (not present in Mastodon):
324 max_toot_chars: Keyword.get(instance, :limit),
325 poll_limits: Keyword.get(instance, :poll_limits)
326 }
327
328 json(conn, response)
329 end
330
331 def peers(conn, _params) do
332 json(conn, Stats.get_peers())
333 end
334
335 defp mastodonized_emoji do
336 Pleroma.Emoji.get_all()
337 |> Enum.map(fn {shortcode, relative_url, tags} ->
338 url = to_string(URI.merge(Web.base_url(), relative_url))
339
340 %{
341 "shortcode" => shortcode,
342 "static_url" => url,
343 "visible_in_picker" => true,
344 "url" => url,
345 "tags" => tags,
346 # Assuming that a comma is authorized in the category name
347 "category" => (tags -- ["Custom"]) |> Enum.join(",")
348 }
349 end)
350 end
351
352 def custom_emojis(conn, _params) do
353 mastodon_emoji = mastodonized_emoji()
354 json(conn, mastodon_emoji)
355 end
356
357 def home_timeline(%{assigns: %{user: user}} = conn, params) do
358 params =
359 params
360 |> Map.put("type", ["Create", "Announce"])
361 |> Map.put("blocking_user", user)
362 |> Map.put("muting_user", user)
363 |> Map.put("user", user)
364
365 activities =
366 [user.ap_id | user.following]
367 |> ActivityPub.fetch_activities(params)
368 |> Enum.reverse()
369
370 conn
371 |> add_link_headers(activities)
372 |> put_view(StatusView)
373 |> render("index.json", %{activities: activities, for: user, as: :activity})
374 end
375
376 def public_timeline(%{assigns: %{user: user}} = conn, params) do
377 local_only = params["local"] in [true, "True", "true", "1"]
378
379 activities =
380 params
381 |> Map.put("type", ["Create", "Announce"])
382 |> Map.put("local_only", local_only)
383 |> Map.put("blocking_user", user)
384 |> Map.put("muting_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" => id}) do
1534 with %Activity{} = activity <- Activity.get_by_id(id),
1535 true <- Visibility.visible_for_user?(activity, user) do
1536 data = RichMedia.Helpers.fetch_data_for_activity(activity)
1537
1538 conn
1539 |> put_view(StatusView)
1540 |> render("card.json", data)
1541 else
1542 _e -> {:error, :not_found}
1543 end
1544 end
1545
1546 def reports(%{assigns: %{user: user}} = conn, params) do
1547 case CommonAPI.report(user, params) do
1548 {:ok, activity} ->
1549 conn
1550 |> put_view(ReportView)
1551 |> try_render("report.json", %{activity: activity})
1552
1553 {:error, err} ->
1554 conn
1555 |> put_status(:bad_request)
1556 |> json(%{error: err})
1557 end
1558 end
1559
1560 def account_register(
1561 %{assigns: %{app: app}} = conn,
1562 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1563 ) do
1564 params =
1565 params
1566 |> Map.take([
1567 "email",
1568 "captcha_solution",
1569 "captcha_token",
1570 "captcha_answer_data",
1571 "token",
1572 "password"
1573 ])
1574 |> Map.put("nickname", nickname)
1575 |> Map.put("fullname", params["fullname"] || nickname)
1576 |> Map.put("bio", params["bio"] || "")
1577 |> Map.put("confirm", params["password"])
1578
1579 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1580 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1581 json(conn, %{
1582 token_type: "Bearer",
1583 access_token: token.token,
1584 scope: app.scopes,
1585 created_at: Token.Utils.format_created_at(token)
1586 })
1587 else
1588 {:error, errors} ->
1589 conn
1590 |> put_status(:bad_request)
1591 |> json(errors)
1592 end
1593 end
1594
1595 def account_register(%{assigns: %{app: _app}} = conn, _) do
1596 render_error(conn, :bad_request, "Missing parameters")
1597 end
1598
1599 def account_register(conn, _) do
1600 render_error(conn, :forbidden, "Invalid credentials")
1601 end
1602
1603 def conversations(%{assigns: %{user: user}} = conn, params) do
1604 participations = Participation.for_user_with_last_activity_id(user, params)
1605
1606 conversations =
1607 Enum.map(participations, fn participation ->
1608 ConversationView.render("participation.json", %{participation: participation, for: user})
1609 end)
1610
1611 conn
1612 |> add_link_headers(participations)
1613 |> json(conversations)
1614 end
1615
1616 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1617 with %Participation{} = participation <-
1618 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1619 {:ok, participation} <- Participation.mark_as_read(participation) do
1620 participation_view =
1621 ConversationView.render("participation.json", %{participation: participation, for: user})
1622
1623 conn
1624 |> json(participation_view)
1625 end
1626 end
1627
1628 def password_reset(conn, params) do
1629 nickname_or_email = params["email"] || params["nickname"]
1630
1631 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1632 conn
1633 |> put_status(:no_content)
1634 |> json("")
1635 else
1636 {:error, "unknown user"} ->
1637 send_resp(conn, :not_found, "")
1638
1639 {:error, _} ->
1640 send_resp(conn, :bad_request, "")
1641 end
1642 end
1643
1644 def account_confirmation_resend(conn, params) do
1645 nickname_or_email = params["email"] || params["nickname"]
1646
1647 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1648 {:ok, _} <- User.try_send_confirmation_email(user) do
1649 conn
1650 |> json_response(:no_content, "")
1651 end
1652 end
1653
1654 defp try_render(conn, target, params)
1655 when is_binary(target) do
1656 case render(conn, target, params) do
1657 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1658 res -> res
1659 end
1660 end
1661
1662 defp try_render(conn, _, _) do
1663 render_error(conn, :not_implemented, "Can't display this activity")
1664 end
1665
1666 defp present?(nil), do: false
1667 defp present?(false), do: false
1668 defp present?(_), do: true
1669 end