Merge branch 'fix/truncate-remote-user-fields' into 'develop'
[akkoma] / lib / pleroma / web / mastodon_api / controllers / 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: 2, 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(Pleroma.Web.MastodonAPI.FallbackController)
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 info_params =
147 [
148 :no_rich_text,
149 :locked,
150 :hide_followers,
151 :hide_follows,
152 :hide_favorites,
153 :show_role,
154 :skip_thread_containment
155 ]
156 |> Enum.reduce(%{}, fn key, acc ->
157 add_if_present(acc, params, to_string(key), key, fn value ->
158 {:ok, ControllerHelper.truthy_param?(value)}
159 end)
160 end)
161 |> add_if_present(params, "default_scope", :default_scope)
162 |> add_if_present(params, "fields", :fields, fn fields ->
163 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
164
165 {:ok, fields}
166 end)
167 |> add_if_present(params, "fields", :raw_fields)
168 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
169 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
170 end)
171 |> add_if_present(params, "header", :banner, fn value ->
172 with %Plug.Upload{} <- value,
173 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
174 {:ok, object.data}
175 else
176 _ -> :error
177 end
178 end)
179 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
180 with %Plug.Upload{} <- value,
181 {:ok, object} <- ActivityPub.upload(value, type: :background) do
182 {:ok, object.data}
183 else
184 _ -> :error
185 end
186 end)
187 |> Map.put(:emoji, user_info_emojis)
188
189 info_cng = User.Info.profile_update(user.info, info_params)
190
191 with changeset <- User.update_changeset(user, user_params),
192 changeset <- Changeset.put_embed(changeset, :info, info_cng),
193 {:ok, user} <- User.update_and_set_cache(changeset) do
194 if original_user != user do
195 CommonAPI.update(user)
196 end
197
198 json(
199 conn,
200 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
201 )
202 else
203 _e -> render_error(conn, :forbidden, "Invalid request")
204 end
205 end
206
207 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
208 change = Changeset.change(user, %{avatar: nil})
209 {:ok, user} = User.update_and_set_cache(change)
210 CommonAPI.update(user)
211
212 json(conn, %{url: nil})
213 end
214
215 def update_avatar(%{assigns: %{user: user}} = conn, params) do
216 {:ok, object} = ActivityPub.upload(params, type: :avatar)
217 change = Changeset.change(user, %{avatar: object.data})
218 {:ok, user} = User.update_and_set_cache(change)
219 CommonAPI.update(user)
220 %{"url" => [%{"href" => href} | _]} = object.data
221
222 json(conn, %{url: href})
223 end
224
225 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
226 with new_info <- %{"banner" => %{}},
227 info_cng <- User.Info.profile_update(user.info, new_info),
228 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
229 {:ok, user} <- User.update_and_set_cache(changeset) do
230 CommonAPI.update(user)
231
232 json(conn, %{url: nil})
233 end
234 end
235
236 def update_banner(%{assigns: %{user: user}} = conn, params) do
237 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
238 new_info <- %{"banner" => object.data},
239 info_cng <- User.Info.profile_update(user.info, new_info),
240 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
241 {:ok, user} <- User.update_and_set_cache(changeset) do
242 CommonAPI.update(user)
243 %{"url" => [%{"href" => href} | _]} = object.data
244
245 json(conn, %{url: href})
246 end
247 end
248
249 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
250 with new_info <- %{"background" => %{}},
251 info_cng <- User.Info.profile_update(user.info, new_info),
252 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
253 {:ok, _user} <- User.update_and_set_cache(changeset) do
254 json(conn, %{url: nil})
255 end
256 end
257
258 def update_background(%{assigns: %{user: user}} = conn, params) do
259 with {:ok, object} <- ActivityPub.upload(params, type: :background),
260 new_info <- %{"background" => object.data},
261 info_cng <- User.Info.profile_update(user.info, new_info),
262 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
263 {:ok, _user} <- User.update_and_set_cache(changeset) do
264 %{"url" => [%{"href" => href} | _]} = object.data
265
266 json(conn, %{url: href})
267 end
268 end
269
270 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
271 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
272
273 account =
274 AccountView.render("account.json", %{
275 user: user,
276 for: user,
277 with_pleroma_settings: true,
278 with_chat_token: chat_token
279 })
280
281 json(conn, account)
282 end
283
284 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
285 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
286 conn
287 |> put_view(AppView)
288 |> render("short.json", %{app: app})
289 end
290 end
291
292 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
293 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
294 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
295 account = AccountView.render("account.json", %{user: user, for: for_user})
296 json(conn, account)
297 else
298 _e -> render_error(conn, :not_found, "Can't find user")
299 end
300 end
301
302 @mastodon_api_level "2.7.2"
303
304 def masto_instance(conn, _params) do
305 instance = Config.get(:instance)
306
307 response = %{
308 uri: Web.base_url(),
309 title: Keyword.get(instance, :name),
310 description: Keyword.get(instance, :description),
311 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
312 email: Keyword.get(instance, :email),
313 urls: %{
314 streaming_api: Pleroma.Web.Endpoint.websocket_url()
315 },
316 stats: Stats.get_stats(),
317 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
318 languages: ["en"],
319 registrations: Pleroma.Config.get([:instance, :registrations_open]),
320 # Extra (not present in Mastodon):
321 max_toot_chars: Keyword.get(instance, :limit),
322 poll_limits: Keyword.get(instance, :poll_limits)
323 }
324
325 json(conn, response)
326 end
327
328 def peers(conn, _params) do
329 json(conn, Stats.get_peers())
330 end
331
332 defp mastodonized_emoji do
333 Pleroma.Emoji.get_all()
334 |> Enum.map(fn {shortcode, relative_url, tags} ->
335 url = to_string(URI.merge(Web.base_url(), relative_url))
336
337 %{
338 "shortcode" => shortcode,
339 "static_url" => url,
340 "visible_in_picker" => true,
341 "url" => url,
342 "tags" => tags,
343 # Assuming that a comma is authorized in the category name
344 "category" => (tags -- ["Custom"]) |> Enum.join(",")
345 }
346 end)
347 end
348
349 def custom_emojis(conn, _params) do
350 mastodon_emoji = mastodonized_emoji()
351 json(conn, mastodon_emoji)
352 end
353
354 def home_timeline(%{assigns: %{user: user}} = conn, params) do
355 params =
356 params
357 |> Map.put("type", ["Create", "Announce"])
358 |> Map.put("blocking_user", user)
359 |> Map.put("muting_user", user)
360 |> Map.put("user", user)
361
362 activities =
363 [user.ap_id | user.following]
364 |> ActivityPub.fetch_activities(params)
365 |> Enum.reverse()
366
367 conn
368 |> add_link_headers(activities)
369 |> put_view(StatusView)
370 |> render("index.json", %{activities: activities, for: user, as: :activity})
371 end
372
373 def public_timeline(%{assigns: %{user: user}} = conn, params) do
374 local_only = params["local"] in [true, "True", "true", "1"]
375
376 activities =
377 params
378 |> Map.put("type", ["Create", "Announce"])
379 |> Map.put("local_only", local_only)
380 |> Map.put("blocking_user", user)
381 |> Map.put("muting_user", user)
382 |> Map.put("user", user)
383 |> ActivityPub.fetch_public_activities()
384 |> Enum.reverse()
385
386 conn
387 |> add_link_headers(activities, %{"local" => local_only})
388 |> put_view(StatusView)
389 |> render("index.json", %{activities: activities, for: user, as: :activity})
390 end
391
392 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
393 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
394 params =
395 params
396 |> Map.put("tag", params["tagged"])
397
398 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
399
400 conn
401 |> add_link_headers(activities)
402 |> put_view(StatusView)
403 |> render("index.json", %{
404 activities: activities,
405 for: reading_user,
406 as: :activity
407 })
408 end
409 end
410
411 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
412 params =
413 params
414 |> Map.put("type", "Create")
415 |> Map.put("blocking_user", user)
416 |> Map.put("user", user)
417 |> Map.put(:visibility, "direct")
418
419 activities =
420 [user.ap_id]
421 |> ActivityPub.fetch_activities_query(params)
422 |> Pagination.fetch_paginated(params)
423
424 conn
425 |> add_link_headers(activities)
426 |> put_view(StatusView)
427 |> render("index.json", %{activities: activities, for: user, as: :activity})
428 end
429
430 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
431 limit = 100
432
433 activities =
434 ids
435 |> Enum.take(limit)
436 |> Activity.all_by_ids_with_object()
437 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
438
439 conn
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_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)
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 |> Changeset.change()
824 |> 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 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
846 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
847 q = from(u in User, where: u.ap_id in ^likes)
848
849 users =
850 Repo.all(q)
851 |> Enum.filter(&(not User.blocks?(user, &1)))
852
853 conn
854 |> put_view(AccountView)
855 |> render("accounts.json", %{for: user, users: users, as: :user})
856 else
857 {:visible, false} -> {:error, :not_found}
858 _ -> json(conn, [])
859 end
860 end
861
862 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
863 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
864 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
865 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
866 q = from(u in User, where: u.ap_id in ^announces)
867
868 users =
869 Repo.all(q)
870 |> Enum.filter(&(not User.blocks?(user, &1)))
871
872 conn
873 |> put_view(AccountView)
874 |> render("accounts.json", %{for: user, users: users, as: :user})
875 else
876 {:visible, false} -> {:error, :not_found}
877 _ -> json(conn, [])
878 end
879 end
880
881 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
882 local_only = params["local"] in [true, "True", "true", "1"]
883
884 tags =
885 [params["tag"], params["any"]]
886 |> List.flatten()
887 |> Enum.uniq()
888 |> Enum.filter(& &1)
889 |> Enum.map(&String.downcase(&1))
890
891 tag_all =
892 params["all"] ||
893 []
894 |> Enum.map(&String.downcase(&1))
895
896 tag_reject =
897 params["none"] ||
898 []
899 |> Enum.map(&String.downcase(&1))
900
901 activities =
902 params
903 |> Map.put("type", "Create")
904 |> Map.put("local_only", local_only)
905 |> Map.put("blocking_user", user)
906 |> Map.put("muting_user", user)
907 |> Map.put("user", user)
908 |> Map.put("tag", tags)
909 |> Map.put("tag_all", tag_all)
910 |> Map.put("tag_reject", tag_reject)
911 |> ActivityPub.fetch_public_activities()
912 |> Enum.reverse()
913
914 conn
915 |> add_link_headers(activities, %{"local" => local_only})
916 |> put_view(StatusView)
917 |> render("index.json", %{activities: activities, for: user, as: :activity})
918 end
919
920 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
921 with %User{} = user <- User.get_cached_by_id(id),
922 followers <- MastodonAPI.get_followers(user, params) do
923 followers =
924 cond do
925 for_user && user.id == for_user.id -> followers
926 user.info.hide_followers -> []
927 true -> followers
928 end
929
930 conn
931 |> add_link_headers(followers)
932 |> put_view(AccountView)
933 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
934 end
935 end
936
937 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
938 with %User{} = user <- User.get_cached_by_id(id),
939 followers <- MastodonAPI.get_friends(user, params) do
940 followers =
941 cond do
942 for_user && user.id == for_user.id -> followers
943 user.info.hide_follows -> []
944 true -> followers
945 end
946
947 conn
948 |> add_link_headers(followers)
949 |> put_view(AccountView)
950 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
951 end
952 end
953
954 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
955 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
956 conn
957 |> put_view(AccountView)
958 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
959 end
960 end
961
962 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
963 with %User{} = follower <- User.get_cached_by_id(id),
964 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
965 conn
966 |> put_view(AccountView)
967 |> render("relationship.json", %{user: followed, target: follower})
968 else
969 {:error, message} ->
970 conn
971 |> put_status(:forbidden)
972 |> json(%{error: message})
973 end
974 end
975
976 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
977 with %User{} = follower <- User.get_cached_by_id(id),
978 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
979 conn
980 |> put_view(AccountView)
981 |> render("relationship.json", %{user: followed, target: follower})
982 else
983 {:error, message} ->
984 conn
985 |> put_status(:forbidden)
986 |> json(%{error: message})
987 end
988 end
989
990 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
991 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
992 {_, true} <- {:followed, follower.id != followed.id},
993 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
994 conn
995 |> put_view(AccountView)
996 |> render("relationship.json", %{user: follower, target: followed})
997 else
998 {:followed, _} ->
999 {:error, :not_found}
1000
1001 {:error, message} ->
1002 conn
1003 |> put_status(:forbidden)
1004 |> json(%{error: message})
1005 end
1006 end
1007
1008 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1009 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1010 {_, true} <- {:followed, follower.id != followed.id},
1011 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1012 conn
1013 |> put_view(AccountView)
1014 |> render("account.json", %{user: followed, for: follower})
1015 else
1016 {:followed, _} ->
1017 {:error, :not_found}
1018
1019 {:error, message} ->
1020 conn
1021 |> put_status(:forbidden)
1022 |> json(%{error: message})
1023 end
1024 end
1025
1026 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1027 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1028 {_, true} <- {:followed, follower.id != followed.id},
1029 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1030 conn
1031 |> put_view(AccountView)
1032 |> render("relationship.json", %{user: follower, target: followed})
1033 else
1034 {:followed, _} ->
1035 {:error, :not_found}
1036
1037 error ->
1038 error
1039 end
1040 end
1041
1042 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1043 notifications =
1044 if Map.has_key?(params, "notifications"),
1045 do: params["notifications"] in [true, "True", "true", "1"],
1046 else: true
1047
1048 with %User{} = muted <- User.get_cached_by_id(id),
1049 {:ok, muter} <- User.mute(muter, muted, notifications) do
1050 conn
1051 |> put_view(AccountView)
1052 |> render("relationship.json", %{user: muter, target: muted})
1053 else
1054 {:error, message} ->
1055 conn
1056 |> put_status(:forbidden)
1057 |> json(%{error: message})
1058 end
1059 end
1060
1061 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1062 with %User{} = muted <- User.get_cached_by_id(id),
1063 {:ok, muter} <- User.unmute(muter, muted) do
1064 conn
1065 |> put_view(AccountView)
1066 |> render("relationship.json", %{user: muter, target: muted})
1067 else
1068 {:error, message} ->
1069 conn
1070 |> put_status(:forbidden)
1071 |> json(%{error: message})
1072 end
1073 end
1074
1075 def mutes(%{assigns: %{user: user}} = conn, _) do
1076 with muted_accounts <- User.muted_users(user) do
1077 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1078 json(conn, res)
1079 end
1080 end
1081
1082 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1083 with %User{} = blocked <- User.get_cached_by_id(id),
1084 {:ok, blocker} <- User.block(blocker, blocked),
1085 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1086 conn
1087 |> put_view(AccountView)
1088 |> render("relationship.json", %{user: blocker, target: blocked})
1089 else
1090 {:error, message} ->
1091 conn
1092 |> put_status(:forbidden)
1093 |> json(%{error: message})
1094 end
1095 end
1096
1097 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1098 with %User{} = blocked <- User.get_cached_by_id(id),
1099 {:ok, blocker} <- User.unblock(blocker, blocked),
1100 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1101 conn
1102 |> put_view(AccountView)
1103 |> render("relationship.json", %{user: blocker, target: blocked})
1104 else
1105 {:error, message} ->
1106 conn
1107 |> put_status(:forbidden)
1108 |> json(%{error: message})
1109 end
1110 end
1111
1112 def blocks(%{assigns: %{user: user}} = conn, _) do
1113 with blocked_accounts <- User.blocked_users(user) do
1114 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1115 json(conn, res)
1116 end
1117 end
1118
1119 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1120 json(conn, info.domain_blocks || [])
1121 end
1122
1123 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1124 User.block_domain(blocker, domain)
1125 json(conn, %{})
1126 end
1127
1128 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1129 User.unblock_domain(blocker, domain)
1130 json(conn, %{})
1131 end
1132
1133 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1134 with %User{} = subscription_target <- User.get_cached_by_id(id),
1135 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1136 conn
1137 |> put_view(AccountView)
1138 |> render("relationship.json", %{user: user, target: subscription_target})
1139 else
1140 {:error, message} ->
1141 conn
1142 |> put_status(:forbidden)
1143 |> json(%{error: message})
1144 end
1145 end
1146
1147 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1148 with %User{} = subscription_target <- User.get_cached_by_id(id),
1149 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1150 conn
1151 |> put_view(AccountView)
1152 |> render("relationship.json", %{user: user, target: subscription_target})
1153 else
1154 {:error, message} ->
1155 conn
1156 |> put_status(:forbidden)
1157 |> json(%{error: message})
1158 end
1159 end
1160
1161 def favourites(%{assigns: %{user: user}} = conn, params) do
1162 params =
1163 params
1164 |> Map.put("type", "Create")
1165 |> Map.put("favorited_by", user.ap_id)
1166 |> Map.put("blocking_user", user)
1167
1168 activities =
1169 ActivityPub.fetch_activities([], params)
1170 |> Enum.reverse()
1171
1172 conn
1173 |> add_link_headers(activities)
1174 |> put_view(StatusView)
1175 |> render("index.json", %{activities: activities, for: user, as: :activity})
1176 end
1177
1178 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1179 with %User{} = user <- User.get_by_id(id),
1180 false <- user.info.hide_favorites do
1181 params =
1182 params
1183 |> Map.put("type", "Create")
1184 |> Map.put("favorited_by", user.ap_id)
1185 |> Map.put("blocking_user", for_user)
1186
1187 recipients =
1188 if for_user do
1189 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1190 else
1191 [Pleroma.Constants.as_public()]
1192 end
1193
1194 activities =
1195 recipients
1196 |> ActivityPub.fetch_activities(params)
1197 |> Enum.reverse()
1198
1199 conn
1200 |> add_link_headers(activities)
1201 |> put_view(StatusView)
1202 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1203 else
1204 nil -> {:error, :not_found}
1205 true -> render_error(conn, :forbidden, "Can't get favorites")
1206 end
1207 end
1208
1209 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1210 user = User.get_cached_by_id(user.id)
1211
1212 bookmarks =
1213 Bookmark.for_user_query(user.id)
1214 |> Pagination.fetch_paginated(params)
1215
1216 activities =
1217 bookmarks
1218 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1219
1220 conn
1221 |> add_link_headers(bookmarks)
1222 |> put_view(StatusView)
1223 |> render("index.json", %{activities: activities, for: user, as: :activity})
1224 end
1225
1226 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1227 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1228 res = ListView.render("lists.json", lists: lists)
1229 json(conn, res)
1230 end
1231
1232 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1233 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1234 params =
1235 params
1236 |> Map.put("type", "Create")
1237 |> Map.put("blocking_user", user)
1238 |> Map.put("user", user)
1239 |> Map.put("muting_user", user)
1240
1241 # we must filter the following list for the user to avoid leaking statuses the user
1242 # does not actually have permission to see (for more info, peruse security issue #270).
1243 activities =
1244 following
1245 |> Enum.filter(fn x -> x in user.following end)
1246 |> ActivityPub.fetch_activities_bounded(following, params)
1247 |> Enum.reverse()
1248
1249 conn
1250 |> put_view(StatusView)
1251 |> render("index.json", %{activities: activities, for: user, as: :activity})
1252 else
1253 _e -> render_error(conn, :forbidden, "Error.")
1254 end
1255 end
1256
1257 def index(%{assigns: %{user: user}} = conn, _params) do
1258 token = get_session(conn, :oauth_token)
1259
1260 if user && token do
1261 mastodon_emoji = mastodonized_emoji()
1262
1263 limit = Config.get([:instance, :limit])
1264
1265 accounts =
1266 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1267
1268 initial_state =
1269 %{
1270 meta: %{
1271 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1272 access_token: token,
1273 locale: "en",
1274 domain: Pleroma.Web.Endpoint.host(),
1275 admin: "1",
1276 me: "#{user.id}",
1277 unfollow_modal: false,
1278 boost_modal: false,
1279 delete_modal: true,
1280 auto_play_gif: false,
1281 display_sensitive_media: false,
1282 reduce_motion: false,
1283 max_toot_chars: limit,
1284 mascot: User.get_mascot(user)["url"]
1285 },
1286 poll_limits: Config.get([:instance, :poll_limits]),
1287 rights: %{
1288 delete_others_notice: present?(user.info.is_moderator),
1289 admin: present?(user.info.is_admin)
1290 },
1291 compose: %{
1292 me: "#{user.id}",
1293 default_privacy: user.info.default_scope,
1294 default_sensitive: false,
1295 allow_content_types: Config.get([:instance, :allowed_post_formats])
1296 },
1297 media_attachments: %{
1298 accept_content_types: [
1299 ".jpg",
1300 ".jpeg",
1301 ".png",
1302 ".gif",
1303 ".webm",
1304 ".mp4",
1305 ".m4v",
1306 "image\/jpeg",
1307 "image\/png",
1308 "image\/gif",
1309 "video\/webm",
1310 "video\/mp4"
1311 ]
1312 },
1313 settings:
1314 user.info.settings ||
1315 %{
1316 onboarded: true,
1317 home: %{
1318 shows: %{
1319 reblog: true,
1320 reply: true
1321 }
1322 },
1323 notifications: %{
1324 alerts: %{
1325 follow: true,
1326 favourite: true,
1327 reblog: true,
1328 mention: true
1329 },
1330 shows: %{
1331 follow: true,
1332 favourite: true,
1333 reblog: true,
1334 mention: true
1335 },
1336 sounds: %{
1337 follow: true,
1338 favourite: true,
1339 reblog: true,
1340 mention: true
1341 }
1342 }
1343 },
1344 push_subscription: nil,
1345 accounts: accounts,
1346 custom_emojis: mastodon_emoji,
1347 char_limit: limit
1348 }
1349 |> Jason.encode!()
1350
1351 conn
1352 |> put_layout(false)
1353 |> put_view(MastodonView)
1354 |> render("index.html", %{initial_state: initial_state})
1355 else
1356 conn
1357 |> put_session(:return_to, conn.request_path)
1358 |> redirect(to: "/web/login")
1359 end
1360 end
1361
1362 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1363 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1364
1365 with changeset <- Changeset.change(user),
1366 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1367 {:ok, _user} <- User.update_and_set_cache(changeset) do
1368 json(conn, %{})
1369 else
1370 e ->
1371 conn
1372 |> put_status(:internal_server_error)
1373 |> json(%{error: inspect(e)})
1374 end
1375 end
1376
1377 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1378 redirect(conn, to: local_mastodon_root_path(conn))
1379 end
1380
1381 @doc "Local Mastodon FE login init action"
1382 def login(conn, %{"code" => auth_token}) do
1383 with {:ok, app} <- get_or_make_app(),
1384 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1385 {:ok, token} <- Token.exchange_token(app, auth) do
1386 conn
1387 |> put_session(:oauth_token, token.token)
1388 |> redirect(to: local_mastodon_root_path(conn))
1389 end
1390 end
1391
1392 @doc "Local Mastodon FE callback action"
1393 def login(conn, _) do
1394 with {:ok, app} <- get_or_make_app() do
1395 path =
1396 o_auth_path(
1397 conn,
1398 :authorize,
1399 response_type: "code",
1400 client_id: app.client_id,
1401 redirect_uri: ".",
1402 scope: Enum.join(app.scopes, " ")
1403 )
1404
1405 redirect(conn, to: path)
1406 end
1407 end
1408
1409 defp local_mastodon_root_path(conn) do
1410 case get_session(conn, :return_to) do
1411 nil ->
1412 mastodon_api_path(conn, :index, ["getting-started"])
1413
1414 return_to ->
1415 delete_session(conn, :return_to)
1416 return_to
1417 end
1418 end
1419
1420 defp get_or_make_app do
1421 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1422 scopes = ["read", "write", "follow", "push"]
1423
1424 with %App{} = app <- Repo.get_by(App, find_attrs) do
1425 {:ok, app} =
1426 if app.scopes == scopes do
1427 {:ok, app}
1428 else
1429 app
1430 |> Changeset.change(%{scopes: scopes})
1431 |> Repo.update()
1432 end
1433
1434 {:ok, app}
1435 else
1436 _e ->
1437 cs =
1438 App.register_changeset(
1439 %App{},
1440 Map.put(find_attrs, :scopes, scopes)
1441 )
1442
1443 Repo.insert(cs)
1444 end
1445 end
1446
1447 def logout(conn, _) do
1448 conn
1449 |> clear_session
1450 |> redirect(to: "/")
1451 end
1452
1453 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1454 Logger.debug("Unimplemented, returning unmodified relationship")
1455
1456 with %User{} = target <- User.get_cached_by_id(id) do
1457 conn
1458 |> put_view(AccountView)
1459 |> render("relationship.json", %{user: user, target: target})
1460 end
1461 end
1462
1463 def empty_array(conn, _) do
1464 Logger.debug("Unimplemented, returning an empty array")
1465 json(conn, [])
1466 end
1467
1468 def empty_object(conn, _) do
1469 Logger.debug("Unimplemented, returning an empty object")
1470 json(conn, %{})
1471 end
1472
1473 def get_filters(%{assigns: %{user: user}} = conn, _) do
1474 filters = Filter.get_filters(user)
1475 res = FilterView.render("filters.json", filters: filters)
1476 json(conn, res)
1477 end
1478
1479 def create_filter(
1480 %{assigns: %{user: user}} = conn,
1481 %{"phrase" => phrase, "context" => context} = params
1482 ) do
1483 query = %Filter{
1484 user_id: user.id,
1485 phrase: phrase,
1486 context: context,
1487 hide: Map.get(params, "irreversible", false),
1488 whole_word: Map.get(params, "boolean", true)
1489 # expires_at
1490 }
1491
1492 {:ok, response} = Filter.create(query)
1493 res = FilterView.render("filter.json", filter: response)
1494 json(conn, res)
1495 end
1496
1497 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1498 filter = Filter.get(filter_id, user)
1499 res = FilterView.render("filter.json", filter: filter)
1500 json(conn, res)
1501 end
1502
1503 def update_filter(
1504 %{assigns: %{user: user}} = conn,
1505 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1506 ) do
1507 query = %Filter{
1508 user_id: user.id,
1509 filter_id: filter_id,
1510 phrase: phrase,
1511 context: context,
1512 hide: Map.get(params, "irreversible", nil),
1513 whole_word: Map.get(params, "boolean", true)
1514 # expires_at
1515 }
1516
1517 {:ok, response} = Filter.update(query)
1518 res = FilterView.render("filter.json", filter: response)
1519 json(conn, res)
1520 end
1521
1522 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1523 query = %Filter{
1524 user_id: user.id,
1525 filter_id: filter_id
1526 }
1527
1528 {:ok, _} = Filter.delete(query)
1529 json(conn, %{})
1530 end
1531
1532 def suggestions(%{assigns: %{user: user}} = conn, _) do
1533 suggestions = Config.get(:suggestions)
1534
1535 if Keyword.get(suggestions, :enabled, false) do
1536 api = Keyword.get(suggestions, :third_party_engine, "")
1537 timeout = Keyword.get(suggestions, :timeout, 5000)
1538 limit = Keyword.get(suggestions, :limit, 23)
1539
1540 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1541
1542 user = user.nickname
1543
1544 url =
1545 api
1546 |> String.replace("{{host}}", host)
1547 |> String.replace("{{user}}", user)
1548
1549 with {:ok, %{status: 200, body: body}} <-
1550 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1551 {:ok, data} <- Jason.decode(body) do
1552 data =
1553 data
1554 |> Enum.slice(0, limit)
1555 |> Enum.map(fn x ->
1556 x
1557 |> Map.put("id", fetch_suggestion_id(x))
1558 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1559 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1560 end)
1561
1562 json(conn, data)
1563 else
1564 e ->
1565 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1566 end
1567 else
1568 json(conn, [])
1569 end
1570 end
1571
1572 defp fetch_suggestion_id(attrs) do
1573 case User.get_or_fetch(attrs["acct"]) do
1574 {:ok, %User{id: id}} -> id
1575 _ -> 0
1576 end
1577 end
1578
1579 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1580 with %Activity{} = activity <- Activity.get_by_id(status_id),
1581 true <- Visibility.visible_for_user?(activity, user) do
1582 data =
1583 StatusView.render(
1584 "card.json",
1585 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1586 )
1587
1588 json(conn, data)
1589 else
1590 _e ->
1591 %{}
1592 end
1593 end
1594
1595 def reports(%{assigns: %{user: user}} = conn, params) do
1596 case CommonAPI.report(user, params) do
1597 {:ok, activity} ->
1598 conn
1599 |> put_view(ReportView)
1600 |> try_render("report.json", %{activity: activity})
1601
1602 {:error, err} ->
1603 conn
1604 |> put_status(:bad_request)
1605 |> json(%{error: err})
1606 end
1607 end
1608
1609 def account_register(
1610 %{assigns: %{app: app}} = conn,
1611 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1612 ) do
1613 params =
1614 params
1615 |> Map.take([
1616 "email",
1617 "captcha_solution",
1618 "captcha_token",
1619 "captcha_answer_data",
1620 "token",
1621 "password"
1622 ])
1623 |> Map.put("nickname", nickname)
1624 |> Map.put("fullname", params["fullname"] || nickname)
1625 |> Map.put("bio", params["bio"] || "")
1626 |> Map.put("confirm", params["password"])
1627
1628 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1629 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1630 json(conn, %{
1631 token_type: "Bearer",
1632 access_token: token.token,
1633 scope: app.scopes,
1634 created_at: Token.Utils.format_created_at(token)
1635 })
1636 else
1637 {:error, errors} ->
1638 conn
1639 |> put_status(:bad_request)
1640 |> json(errors)
1641 end
1642 end
1643
1644 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1645 render_error(conn, :bad_request, "Missing parameters")
1646 end
1647
1648 def account_register(conn, _) do
1649 render_error(conn, :forbidden, "Invalid credentials")
1650 end
1651
1652 def conversations(%{assigns: %{user: user}} = conn, params) do
1653 participations = Participation.for_user_with_last_activity_id(user, params)
1654
1655 conversations =
1656 Enum.map(participations, fn participation ->
1657 ConversationView.render("participation.json", %{participation: participation, for: user})
1658 end)
1659
1660 conn
1661 |> add_link_headers(participations)
1662 |> json(conversations)
1663 end
1664
1665 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1666 with %Participation{} = participation <-
1667 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1668 {:ok, participation} <- Participation.mark_as_read(participation) do
1669 participation_view =
1670 ConversationView.render("participation.json", %{participation: participation, for: user})
1671
1672 conn
1673 |> json(participation_view)
1674 end
1675 end
1676
1677 def password_reset(conn, params) do
1678 nickname_or_email = params["email"] || params["nickname"]
1679
1680 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1681 conn
1682 |> put_status(:no_content)
1683 |> json("")
1684 else
1685 {:error, "unknown user"} ->
1686 send_resp(conn, :not_found, "")
1687
1688 {:error, _} ->
1689 send_resp(conn, :bad_request, "")
1690 end
1691 end
1692
1693 def account_confirmation_resend(conn, params) do
1694 nickname_or_email = params["email"] || params["nickname"]
1695
1696 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1697 {:ok, _} <- User.try_send_confirmation_email(user) do
1698 conn
1699 |> json_response(:no_content, "")
1700 end
1701 end
1702
1703 def try_render(conn, target, params)
1704 when is_binary(target) do
1705 case render(conn, target, params) do
1706 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1707 res -> res
1708 end
1709 end
1710
1711 def try_render(conn, _, _) do
1712 render_error(conn, :not_implemented, "Can't display this activity")
1713 end
1714
1715 defp present?(nil), do: false
1716 defp present?(false), do: false
1717 defp present?(_), do: true
1718 end