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