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