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