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