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