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