ccebcd415ac199d459049c03488ff99fc84c2601
[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
887 users = Repo.all(q)
888 users = if is_nil(user) do
889 users
890 else
891 Enum.filter(users, &(not User.blocks?(user, &1)))
892 end
893
894 conn
895 |> put_view(AccountView)
896 |> render("accounts.json", %{for: user, users: users, as: :user})
897 else
898 _ -> json(conn, [])
899 end
900 end
901
902 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
903 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
904 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
905 q = from(u in User, where: u.ap_id in ^announces)
906
907 users = Repo.all(q)
908 users = if is_nil(user) do
909 users
910 else
911 Enum.filter(users, &(not User.blocks?(user, &1)))
912 end
913
914 conn
915 |> put_view(AccountView)
916 |> render("accounts.json", %{for: user, users: users, as: :user})
917 else
918 _ -> json(conn, [])
919 end
920 end
921
922 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
923 local_only = params["local"] in [true, "True", "true", "1"]
924
925 tags =
926 [params["tag"], params["any"]]
927 |> List.flatten()
928 |> Enum.uniq()
929 |> Enum.filter(& &1)
930 |> Enum.map(&String.downcase(&1))
931
932 tag_all =
933 params["all"] ||
934 []
935 |> Enum.map(&String.downcase(&1))
936
937 tag_reject =
938 params["none"] ||
939 []
940 |> Enum.map(&String.downcase(&1))
941
942 activities =
943 params
944 |> Map.put("type", "Create")
945 |> Map.put("local_only", local_only)
946 |> Map.put("blocking_user", user)
947 |> Map.put("muting_user", user)
948 |> Map.put("tag", tags)
949 |> Map.put("tag_all", tag_all)
950 |> Map.put("tag_reject", tag_reject)
951 |> ActivityPub.fetch_public_activities()
952 |> Enum.reverse()
953
954 conn
955 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
956 |> put_view(StatusView)
957 |> render("index.json", %{activities: activities, for: user, as: :activity})
958 end
959
960 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
961 with %User{} = user <- User.get_cached_by_id(id),
962 followers <- MastodonAPI.get_followers(user, params) do
963 followers =
964 cond do
965 for_user && user.id == for_user.id -> followers
966 user.info.hide_followers -> []
967 true -> followers
968 end
969
970 conn
971 |> add_link_headers(:followers, followers, user)
972 |> put_view(AccountView)
973 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
974 end
975 end
976
977 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
978 with %User{} = user <- User.get_cached_by_id(id),
979 followers <- MastodonAPI.get_friends(user, params) do
980 followers =
981 cond do
982 for_user && user.id == for_user.id -> followers
983 user.info.hide_follows -> []
984 true -> followers
985 end
986
987 conn
988 |> add_link_headers(:following, followers, user)
989 |> put_view(AccountView)
990 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
991 end
992 end
993
994 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
995 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
996 conn
997 |> put_view(AccountView)
998 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
999 end
1000 end
1001
1002 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1003 with %User{} = follower <- User.get_cached_by_id(id),
1004 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1005 conn
1006 |> put_view(AccountView)
1007 |> render("relationship.json", %{user: followed, target: follower})
1008 else
1009 {:error, message} ->
1010 conn
1011 |> put_status(:forbidden)
1012 |> json(%{error: message})
1013 end
1014 end
1015
1016 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1017 with %User{} = follower <- User.get_cached_by_id(id),
1018 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1019 conn
1020 |> put_view(AccountView)
1021 |> render("relationship.json", %{user: followed, target: follower})
1022 else
1023 {:error, message} ->
1024 conn
1025 |> put_status(:forbidden)
1026 |> json(%{error: message})
1027 end
1028 end
1029
1030 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1031 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1032 {_, true} <- {:followed, follower.id != followed.id},
1033 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1034 conn
1035 |> put_view(AccountView)
1036 |> render("relationship.json", %{user: follower, target: followed})
1037 else
1038 {:followed, _} ->
1039 {:error, :not_found}
1040
1041 {:error, message} ->
1042 conn
1043 |> put_status(:forbidden)
1044 |> json(%{error: message})
1045 end
1046 end
1047
1048 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1049 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1050 {_, true} <- {:followed, follower.id != followed.id},
1051 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1052 conn
1053 |> put_view(AccountView)
1054 |> render("account.json", %{user: followed, for: follower})
1055 else
1056 {:followed, _} ->
1057 {:error, :not_found}
1058
1059 {:error, message} ->
1060 conn
1061 |> put_status(:forbidden)
1062 |> json(%{error: message})
1063 end
1064 end
1065
1066 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1067 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1068 {_, true} <- {:followed, follower.id != followed.id},
1069 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1070 conn
1071 |> put_view(AccountView)
1072 |> render("relationship.json", %{user: follower, target: followed})
1073 else
1074 {:followed, _} ->
1075 {:error, :not_found}
1076
1077 error ->
1078 error
1079 end
1080 end
1081
1082 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1083 notifications =
1084 if Map.has_key?(params, "notifications"),
1085 do: params["notifications"] in [true, "True", "true", "1"],
1086 else: true
1087
1088 with %User{} = muted <- User.get_cached_by_id(id),
1089 {:ok, muter} <- User.mute(muter, muted, notifications) do
1090 conn
1091 |> put_view(AccountView)
1092 |> render("relationship.json", %{user: muter, target: muted})
1093 else
1094 {:error, message} ->
1095 conn
1096 |> put_status(:forbidden)
1097 |> json(%{error: message})
1098 end
1099 end
1100
1101 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1102 with %User{} = muted <- User.get_cached_by_id(id),
1103 {:ok, muter} <- User.unmute(muter, muted) do
1104 conn
1105 |> put_view(AccountView)
1106 |> render("relationship.json", %{user: muter, target: muted})
1107 else
1108 {:error, message} ->
1109 conn
1110 |> put_status(:forbidden)
1111 |> json(%{error: message})
1112 end
1113 end
1114
1115 def mutes(%{assigns: %{user: user}} = conn, _) do
1116 with muted_accounts <- User.muted_users(user) do
1117 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1118 json(conn, res)
1119 end
1120 end
1121
1122 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1123 with %User{} = blocked <- User.get_cached_by_id(id),
1124 {:ok, blocker} <- User.block(blocker, blocked),
1125 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1126 conn
1127 |> put_view(AccountView)
1128 |> render("relationship.json", %{user: blocker, target: blocked})
1129 else
1130 {:error, message} ->
1131 conn
1132 |> put_status(:forbidden)
1133 |> json(%{error: message})
1134 end
1135 end
1136
1137 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1138 with %User{} = blocked <- User.get_cached_by_id(id),
1139 {:ok, blocker} <- User.unblock(blocker, blocked),
1140 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1141 conn
1142 |> put_view(AccountView)
1143 |> render("relationship.json", %{user: blocker, target: blocked})
1144 else
1145 {:error, message} ->
1146 conn
1147 |> put_status(:forbidden)
1148 |> json(%{error: message})
1149 end
1150 end
1151
1152 def blocks(%{assigns: %{user: user}} = conn, _) do
1153 with blocked_accounts <- User.blocked_users(user) do
1154 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1155 json(conn, res)
1156 end
1157 end
1158
1159 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1160 json(conn, info.domain_blocks || [])
1161 end
1162
1163 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1164 User.block_domain(blocker, domain)
1165 json(conn, %{})
1166 end
1167
1168 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1169 User.unblock_domain(blocker, domain)
1170 json(conn, %{})
1171 end
1172
1173 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1174 with %User{} = subscription_target <- User.get_cached_by_id(id),
1175 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1176 conn
1177 |> put_view(AccountView)
1178 |> render("relationship.json", %{user: user, target: subscription_target})
1179 else
1180 {:error, message} ->
1181 conn
1182 |> put_status(:forbidden)
1183 |> json(%{error: message})
1184 end
1185 end
1186
1187 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1188 with %User{} = subscription_target <- User.get_cached_by_id(id),
1189 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1190 conn
1191 |> put_view(AccountView)
1192 |> render("relationship.json", %{user: user, target: subscription_target})
1193 else
1194 {:error, message} ->
1195 conn
1196 |> put_status(:forbidden)
1197 |> json(%{error: message})
1198 end
1199 end
1200
1201 def favourites(%{assigns: %{user: user}} = conn, params) do
1202 params =
1203 params
1204 |> Map.put("type", "Create")
1205 |> Map.put("favorited_by", user.ap_id)
1206 |> Map.put("blocking_user", user)
1207
1208 activities =
1209 ActivityPub.fetch_activities([], params)
1210 |> Enum.reverse()
1211
1212 conn
1213 |> add_link_headers(:favourites, activities)
1214 |> put_view(StatusView)
1215 |> render("index.json", %{activities: activities, for: user, as: :activity})
1216 end
1217
1218 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1219 with %User{} = user <- User.get_by_id(id),
1220 false <- user.info.hide_favorites do
1221 params =
1222 params
1223 |> Map.put("type", "Create")
1224 |> Map.put("favorited_by", user.ap_id)
1225 |> Map.put("blocking_user", for_user)
1226
1227 recipients =
1228 if for_user do
1229 ["https://www.w3.org/ns/activitystreams#Public"] ++
1230 [for_user.ap_id | for_user.following]
1231 else
1232 ["https://www.w3.org/ns/activitystreams#Public"]
1233 end
1234
1235 activities =
1236 recipients
1237 |> ActivityPub.fetch_activities(params)
1238 |> Enum.reverse()
1239
1240 conn
1241 |> add_link_headers(:favourites, activities)
1242 |> put_view(StatusView)
1243 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1244 else
1245 nil -> {:error, :not_found}
1246 true -> render_error(conn, :forbidden, "Can't get favorites")
1247 end
1248 end
1249
1250 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1251 user = User.get_cached_by_id(user.id)
1252
1253 bookmarks =
1254 Bookmark.for_user_query(user.id)
1255 |> Pagination.fetch_paginated(params)
1256
1257 activities =
1258 bookmarks
1259 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1260
1261 conn
1262 |> add_link_headers(:bookmarks, bookmarks)
1263 |> put_view(StatusView)
1264 |> render("index.json", %{activities: activities, for: user, as: :activity})
1265 end
1266
1267 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1268 lists = Pleroma.List.for_user(user, opts)
1269 res = ListView.render("lists.json", lists: lists)
1270 json(conn, res)
1271 end
1272
1273 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1274 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1275 res = ListView.render("list.json", list: list)
1276 json(conn, res)
1277 else
1278 _e -> render_error(conn, :not_found, "Record not found")
1279 end
1280 end
1281
1282 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1283 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1284 res = ListView.render("lists.json", lists: lists)
1285 json(conn, res)
1286 end
1287
1288 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1289 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1290 {:ok, _list} <- Pleroma.List.delete(list) do
1291 json(conn, %{})
1292 else
1293 _e ->
1294 json(conn, dgettext("errors", "error"))
1295 end
1296 end
1297
1298 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1299 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1300 res = ListView.render("list.json", list: list)
1301 json(conn, res)
1302 end
1303 end
1304
1305 def add_to_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.follow(list, followed)
1311 end
1312 end)
1313
1314 json(conn, %{})
1315 end
1316
1317 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1318 accounts
1319 |> Enum.each(fn account_id ->
1320 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1321 %User{} = followed <- User.get_cached_by_id(account_id) do
1322 Pleroma.List.unfollow(list, followed)
1323 end
1324 end)
1325
1326 json(conn, %{})
1327 end
1328
1329 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1330 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1331 {:ok, users} = Pleroma.List.get_following(list) do
1332 conn
1333 |> put_view(AccountView)
1334 |> render("accounts.json", %{for: user, users: users, as: :user})
1335 end
1336 end
1337
1338 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1339 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1340 {:ok, list} <- Pleroma.List.rename(list, title) do
1341 res = ListView.render("list.json", list: list)
1342 json(conn, res)
1343 else
1344 _e ->
1345 json(conn, dgettext("errors", "error"))
1346 end
1347 end
1348
1349 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1350 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1351 params =
1352 params
1353 |> Map.put("type", "Create")
1354 |> Map.put("blocking_user", user)
1355 |> Map.put("muting_user", user)
1356
1357 # we must filter the following list for the user to avoid leaking statuses the user
1358 # does not actually have permission to see (for more info, peruse security issue #270).
1359 activities =
1360 following
1361 |> Enum.filter(fn x -> x in user.following end)
1362 |> ActivityPub.fetch_activities_bounded(following, params)
1363 |> Enum.reverse()
1364
1365 conn
1366 |> put_view(StatusView)
1367 |> render("index.json", %{activities: activities, for: user, as: :activity})
1368 else
1369 _e -> render_error(conn, :forbidden, "Error.")
1370 end
1371 end
1372
1373 def index(%{assigns: %{user: user}} = conn, _params) do
1374 token = get_session(conn, :oauth_token)
1375
1376 if user && token do
1377 mastodon_emoji = mastodonized_emoji()
1378
1379 limit = Config.get([:instance, :limit])
1380
1381 accounts =
1382 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1383
1384 initial_state =
1385 %{
1386 meta: %{
1387 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1388 access_token: token,
1389 locale: "en",
1390 domain: Pleroma.Web.Endpoint.host(),
1391 admin: "1",
1392 me: "#{user.id}",
1393 unfollow_modal: false,
1394 boost_modal: false,
1395 delete_modal: true,
1396 auto_play_gif: false,
1397 display_sensitive_media: false,
1398 reduce_motion: false,
1399 max_toot_chars: limit,
1400 mascot: User.get_mascot(user)["url"]
1401 },
1402 poll_limits: Config.get([:instance, :poll_limits]),
1403 rights: %{
1404 delete_others_notice: present?(user.info.is_moderator),
1405 admin: present?(user.info.is_admin)
1406 },
1407 compose: %{
1408 me: "#{user.id}",
1409 default_privacy: user.info.default_scope,
1410 default_sensitive: false,
1411 allow_content_types: Config.get([:instance, :allowed_post_formats])
1412 },
1413 media_attachments: %{
1414 accept_content_types: [
1415 ".jpg",
1416 ".jpeg",
1417 ".png",
1418 ".gif",
1419 ".webm",
1420 ".mp4",
1421 ".m4v",
1422 "image\/jpeg",
1423 "image\/png",
1424 "image\/gif",
1425 "video\/webm",
1426 "video\/mp4"
1427 ]
1428 },
1429 settings:
1430 user.info.settings ||
1431 %{
1432 onboarded: true,
1433 home: %{
1434 shows: %{
1435 reblog: true,
1436 reply: true
1437 }
1438 },
1439 notifications: %{
1440 alerts: %{
1441 follow: true,
1442 favourite: true,
1443 reblog: true,
1444 mention: true
1445 },
1446 shows: %{
1447 follow: true,
1448 favourite: true,
1449 reblog: true,
1450 mention: true
1451 },
1452 sounds: %{
1453 follow: true,
1454 favourite: true,
1455 reblog: true,
1456 mention: true
1457 }
1458 }
1459 },
1460 push_subscription: nil,
1461 accounts: accounts,
1462 custom_emojis: mastodon_emoji,
1463 char_limit: limit
1464 }
1465 |> Jason.encode!()
1466
1467 conn
1468 |> put_layout(false)
1469 |> put_view(MastodonView)
1470 |> render("index.html", %{initial_state: initial_state})
1471 else
1472 conn
1473 |> put_session(:return_to, conn.request_path)
1474 |> redirect(to: "/web/login")
1475 end
1476 end
1477
1478 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1479 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1480
1481 with changeset <- Ecto.Changeset.change(user),
1482 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1483 {:ok, _user} <- User.update_and_set_cache(changeset) do
1484 json(conn, %{})
1485 else
1486 e ->
1487 conn
1488 |> put_status(:internal_server_error)
1489 |> json(%{error: inspect(e)})
1490 end
1491 end
1492
1493 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1494 redirect(conn, to: local_mastodon_root_path(conn))
1495 end
1496
1497 @doc "Local Mastodon FE login init action"
1498 def login(conn, %{"code" => auth_token}) do
1499 with {:ok, app} <- get_or_make_app(),
1500 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1501 {:ok, token} <- Token.exchange_token(app, auth) do
1502 conn
1503 |> put_session(:oauth_token, token.token)
1504 |> redirect(to: local_mastodon_root_path(conn))
1505 end
1506 end
1507
1508 @doc "Local Mastodon FE callback action"
1509 def login(conn, _) do
1510 with {:ok, app} <- get_or_make_app() do
1511 path =
1512 o_auth_path(
1513 conn,
1514 :authorize,
1515 response_type: "code",
1516 client_id: app.client_id,
1517 redirect_uri: ".",
1518 scope: Enum.join(app.scopes, " ")
1519 )
1520
1521 redirect(conn, to: path)
1522 end
1523 end
1524
1525 defp local_mastodon_root_path(conn) do
1526 case get_session(conn, :return_to) do
1527 nil ->
1528 mastodon_api_path(conn, :index, ["getting-started"])
1529
1530 return_to ->
1531 delete_session(conn, :return_to)
1532 return_to
1533 end
1534 end
1535
1536 defp get_or_make_app do
1537 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1538 scopes = ["read", "write", "follow", "push"]
1539
1540 with %App{} = app <- Repo.get_by(App, find_attrs) do
1541 {:ok, app} =
1542 if app.scopes == scopes do
1543 {:ok, app}
1544 else
1545 app
1546 |> Ecto.Changeset.change(%{scopes: scopes})
1547 |> Repo.update()
1548 end
1549
1550 {:ok, app}
1551 else
1552 _e ->
1553 cs =
1554 App.register_changeset(
1555 %App{},
1556 Map.put(find_attrs, :scopes, scopes)
1557 )
1558
1559 Repo.insert(cs)
1560 end
1561 end
1562
1563 def logout(conn, _) do
1564 conn
1565 |> clear_session
1566 |> redirect(to: "/")
1567 end
1568
1569 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1570 Logger.debug("Unimplemented, returning unmodified relationship")
1571
1572 with %User{} = target <- User.get_cached_by_id(id) do
1573 conn
1574 |> put_view(AccountView)
1575 |> render("relationship.json", %{user: user, target: target})
1576 end
1577 end
1578
1579 def empty_array(conn, _) do
1580 Logger.debug("Unimplemented, returning an empty array")
1581 json(conn, [])
1582 end
1583
1584 def empty_object(conn, _) do
1585 Logger.debug("Unimplemented, returning an empty object")
1586 json(conn, %{})
1587 end
1588
1589 def get_filters(%{assigns: %{user: user}} = conn, _) do
1590 filters = Filter.get_filters(user)
1591 res = FilterView.render("filters.json", filters: filters)
1592 json(conn, res)
1593 end
1594
1595 def create_filter(
1596 %{assigns: %{user: user}} = conn,
1597 %{"phrase" => phrase, "context" => context} = params
1598 ) do
1599 query = %Filter{
1600 user_id: user.id,
1601 phrase: phrase,
1602 context: context,
1603 hide: Map.get(params, "irreversible", false),
1604 whole_word: Map.get(params, "boolean", true)
1605 # expires_at
1606 }
1607
1608 {:ok, response} = Filter.create(query)
1609 res = FilterView.render("filter.json", filter: response)
1610 json(conn, res)
1611 end
1612
1613 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1614 filter = Filter.get(filter_id, user)
1615 res = FilterView.render("filter.json", filter: filter)
1616 json(conn, res)
1617 end
1618
1619 def update_filter(
1620 %{assigns: %{user: user}} = conn,
1621 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1622 ) do
1623 query = %Filter{
1624 user_id: user.id,
1625 filter_id: filter_id,
1626 phrase: phrase,
1627 context: context,
1628 hide: Map.get(params, "irreversible", nil),
1629 whole_word: Map.get(params, "boolean", true)
1630 # expires_at
1631 }
1632
1633 {:ok, response} = Filter.update(query)
1634 res = FilterView.render("filter.json", filter: response)
1635 json(conn, res)
1636 end
1637
1638 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1639 query = %Filter{
1640 user_id: user.id,
1641 filter_id: filter_id
1642 }
1643
1644 {:ok, _} = Filter.delete(query)
1645 json(conn, %{})
1646 end
1647
1648 # fallback action
1649 #
1650 def errors(conn, {:error, %Changeset{} = changeset}) do
1651 error_message =
1652 changeset
1653 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1654 |> Enum.map_join(", ", fn {_k, v} -> v end)
1655
1656 conn
1657 |> put_status(:unprocessable_entity)
1658 |> json(%{error: error_message})
1659 end
1660
1661 def errors(conn, {:error, :not_found}) do
1662 render_error(conn, :not_found, "Record not found")
1663 end
1664
1665 def errors(conn, {:error, error_message}) do
1666 conn
1667 |> put_status(:bad_request)
1668 |> json(%{error: error_message})
1669 end
1670
1671 def errors(conn, _) do
1672 conn
1673 |> put_status(:internal_server_error)
1674 |> json(dgettext("errors", "Something went wrong"))
1675 end
1676
1677 def suggestions(%{assigns: %{user: user}} = conn, _) do
1678 suggestions = Config.get(:suggestions)
1679
1680 if Keyword.get(suggestions, :enabled, false) do
1681 api = Keyword.get(suggestions, :third_party_engine, "")
1682 timeout = Keyword.get(suggestions, :timeout, 5000)
1683 limit = Keyword.get(suggestions, :limit, 23)
1684
1685 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1686
1687 user = user.nickname
1688
1689 url =
1690 api
1691 |> String.replace("{{host}}", host)
1692 |> String.replace("{{user}}", user)
1693
1694 with {:ok, %{status: 200, body: body}} <-
1695 HTTP.get(
1696 url,
1697 [],
1698 adapter: [
1699 recv_timeout: timeout,
1700 pool: :default
1701 ]
1702 ),
1703 {:ok, data} <- Jason.decode(body) do
1704 data =
1705 data
1706 |> Enum.slice(0, limit)
1707 |> Enum.map(fn x ->
1708 Map.put(
1709 x,
1710 "id",
1711 case User.get_or_fetch(x["acct"]) do
1712 {:ok, %User{id: id}} -> id
1713 _ -> 0
1714 end
1715 )
1716 end)
1717 |> Enum.map(fn x ->
1718 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1719 end)
1720 |> Enum.map(fn x ->
1721 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1722 end)
1723
1724 conn
1725 |> json(data)
1726 else
1727 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1728 end
1729 else
1730 json(conn, [])
1731 end
1732 end
1733
1734 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1735 with %Activity{} = activity <- Activity.get_by_id(status_id),
1736 true <- Visibility.visible_for_user?(activity, user) do
1737 data =
1738 StatusView.render(
1739 "card.json",
1740 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1741 )
1742
1743 json(conn, data)
1744 else
1745 _e ->
1746 %{}
1747 end
1748 end
1749
1750 def reports(%{assigns: %{user: user}} = conn, params) do
1751 case CommonAPI.report(user, params) do
1752 {:ok, activity} ->
1753 conn
1754 |> put_view(ReportView)
1755 |> try_render("report.json", %{activity: activity})
1756
1757 {:error, err} ->
1758 conn
1759 |> put_status(:bad_request)
1760 |> json(%{error: err})
1761 end
1762 end
1763
1764 def account_register(
1765 %{assigns: %{app: app}} = conn,
1766 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1767 ) do
1768 params =
1769 params
1770 |> Map.take([
1771 "email",
1772 "captcha_solution",
1773 "captcha_token",
1774 "captcha_answer_data",
1775 "token",
1776 "password"
1777 ])
1778 |> Map.put("nickname", nickname)
1779 |> Map.put("fullname", params["fullname"] || nickname)
1780 |> Map.put("bio", params["bio"] || "")
1781 |> Map.put("confirm", params["password"])
1782
1783 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1784 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1785 json(conn, %{
1786 token_type: "Bearer",
1787 access_token: token.token,
1788 scope: app.scopes,
1789 created_at: Token.Utils.format_created_at(token)
1790 })
1791 else
1792 {:error, errors} ->
1793 conn
1794 |> put_status(:bad_request)
1795 |> json(errors)
1796 end
1797 end
1798
1799 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1800 render_error(conn, :bad_request, "Missing parameters")
1801 end
1802
1803 def account_register(conn, _) do
1804 render_error(conn, :forbidden, "Invalid credentials")
1805 end
1806
1807 def conversations(%{assigns: %{user: user}} = conn, params) do
1808 participations = Participation.for_user_with_last_activity_id(user, params)
1809
1810 conversations =
1811 Enum.map(participations, fn participation ->
1812 ConversationView.render("participation.json", %{participation: participation, user: user})
1813 end)
1814
1815 conn
1816 |> add_link_headers(:conversations, participations)
1817 |> json(conversations)
1818 end
1819
1820 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1821 with %Participation{} = participation <-
1822 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1823 {:ok, participation} <- Participation.mark_as_read(participation) do
1824 participation_view =
1825 ConversationView.render("participation.json", %{participation: participation, user: user})
1826
1827 conn
1828 |> json(participation_view)
1829 end
1830 end
1831
1832 def password_reset(conn, params) do
1833 nickname_or_email = params["email"] || params["nickname"]
1834
1835 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1836 conn
1837 |> put_status(:no_content)
1838 |> json("")
1839 else
1840 {:error, "unknown user"} ->
1841 send_resp(conn, :not_found, "")
1842
1843 {:error, _} ->
1844 send_resp(conn, :bad_request, "")
1845 end
1846 end
1847
1848 def try_render(conn, target, params)
1849 when is_binary(target) do
1850 case render(conn, target, params) do
1851 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1852 res -> res
1853 end
1854 end
1855
1856 def try_render(conn, _, _) do
1857 render_error(conn, :not_implemented, "Can't display this activity")
1858 end
1859
1860 defp present?(nil), do: false
1861 defp present?(false), do: false
1862 defp present?(_), do: true
1863 end