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