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