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