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