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