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