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