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