[#1149] Added Oban job for "activity_expiration". Merged remote-tracking branch ...
[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: 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(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),
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(:home_timeline, 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(:public_timeline, activities, false, %{"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"]) 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(:user_statuses, activities, params["id"])
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(:dm_timeline, activities)
426 |> put_view(StatusView)
427 |> render("index.json", %{activities: activities, for: user, as: :activity})
428 end
429
430 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
431 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
432 true <- Visibility.visible_for_user?(activity, user) do
433 conn
434 |> put_view(StatusView)
435 |> try_render("status.json", %{activity: activity, for: user})
436 end
437 end
438
439 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
440 with %Activity{} = activity <- Activity.get_by_id(id),
441 activities <-
442 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
443 "blocking_user" => user,
444 "user" => user,
445 "exclude_id" => activity.id
446 }),
447 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
448 result = %{
449 ancestors:
450 StatusView.render(
451 "index.json",
452 for: user,
453 activities: grouped_activities[true] || [],
454 as: :activity
455 )
456 |> Enum.reverse(),
457 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
458 descendants:
459 StatusView.render(
460 "index.json",
461 for: user,
462 activities: grouped_activities[false] || [],
463 as: :activity
464 )
465 |> Enum.reverse()
466 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
467 }
468
469 json(conn, result)
470 end
471 end
472
473 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
474 with %Object{} = object <- Object.get_by_id(id),
475 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
476 true <- Visibility.visible_for_user?(activity, user) do
477 conn
478 |> put_view(StatusView)
479 |> try_render("poll.json", %{object: object, for: user})
480 else
481 error when is_nil(error) or error == false ->
482 render_error(conn, :not_found, "Record not found")
483 end
484 end
485
486 defp get_cached_vote_or_vote(user, object, choices) do
487 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
488
489 {_, res} =
490 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
491 case CommonAPI.vote(user, object, choices) do
492 {:error, _message} = res -> {:ignore, res}
493 res -> {:commit, res}
494 end
495 end)
496
497 res
498 end
499
500 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
501 with %Object{} = object <- Object.get_by_id(id),
502 true <- object.data["type"] == "Question",
503 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
504 true <- Visibility.visible_for_user?(activity, user),
505 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
506 conn
507 |> put_view(StatusView)
508 |> try_render("poll.json", %{object: object, for: user})
509 else
510 nil ->
511 render_error(conn, :not_found, "Record not found")
512
513 false ->
514 render_error(conn, :not_found, "Record not found")
515
516 {:error, message} ->
517 conn
518 |> put_status(:unprocessable_entity)
519 |> json(%{error: message})
520 end
521 end
522
523 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
524 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
525 conn
526 |> add_link_headers(:scheduled_statuses, scheduled_activities)
527 |> put_view(ScheduledActivityView)
528 |> render("index.json", %{scheduled_activities: scheduled_activities})
529 end
530 end
531
532 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
533 with %ScheduledActivity{} = scheduled_activity <-
534 ScheduledActivity.get(user, scheduled_activity_id) do
535 conn
536 |> put_view(ScheduledActivityView)
537 |> render("show.json", %{scheduled_activity: scheduled_activity})
538 else
539 _ -> {:error, :not_found}
540 end
541 end
542
543 def update_scheduled_status(
544 %{assigns: %{user: user}} = conn,
545 %{"id" => scheduled_activity_id} = params
546 ) do
547 with %ScheduledActivity{} = scheduled_activity <-
548 ScheduledActivity.get(user, scheduled_activity_id),
549 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
550 conn
551 |> put_view(ScheduledActivityView)
552 |> render("show.json", %{scheduled_activity: scheduled_activity})
553 else
554 nil -> {:error, :not_found}
555 error -> error
556 end
557 end
558
559 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
560 with %ScheduledActivity{} = scheduled_activity <-
561 ScheduledActivity.get(user, scheduled_activity_id),
562 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
563 conn
564 |> put_view(ScheduledActivityView)
565 |> render("show.json", %{scheduled_activity: scheduled_activity})
566 else
567 nil -> {:error, :not_found}
568 error -> error
569 end
570 end
571
572 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
573 params =
574 params
575 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
576
577 scheduled_at = params["scheduled_at"]
578
579 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
580 with {:ok, scheduled_activity} <-
581 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
582 conn
583 |> put_view(ScheduledActivityView)
584 |> render("show.json", %{scheduled_activity: scheduled_activity})
585 end
586 else
587 params = Map.drop(params, ["scheduled_at"])
588
589 case CommonAPI.post(user, params) do
590 {:error, message} ->
591 conn
592 |> put_status(:unprocessable_entity)
593 |> json(%{error: message})
594
595 {:ok, activity} ->
596 conn
597 |> put_view(StatusView)
598 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
599 end
600 end
601 end
602
603 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
604 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
605 json(conn, %{})
606 else
607 _e -> render_error(conn, :forbidden, "Can't delete this post")
608 end
609 end
610
611 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
612 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
613 %Activity{} = announce <- Activity.normalize(announce.data) do
614 conn
615 |> put_view(StatusView)
616 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
617 end
618 end
619
620 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
621 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
622 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
623 conn
624 |> put_view(StatusView)
625 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
626 end
627 end
628
629 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
630 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
631 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
632 conn
633 |> put_view(StatusView)
634 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
635 end
636 end
637
638 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
639 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
640 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
641 conn
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
644 end
645 end
646
647 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
648 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
649 conn
650 |> put_view(StatusView)
651 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
652 end
653 end
654
655 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
656 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
657 conn
658 |> put_view(StatusView)
659 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
660 end
661 end
662
663 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
664 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
665 %User{} = user <- User.get_cached_by_nickname(user.nickname),
666 true <- Visibility.visible_for_user?(activity, user),
667 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
668 conn
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
671 end
672 end
673
674 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
675 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
676 %User{} = user <- User.get_cached_by_nickname(user.nickname),
677 true <- Visibility.visible_for_user?(activity, user),
678 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
679 conn
680 |> put_view(StatusView)
681 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
682 end
683 end
684
685 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
686 activity = Activity.get_by_id(id)
687
688 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
689 conn
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
692 end
693 end
694
695 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 activity = Activity.get_by_id(id)
697
698 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
699 conn
700 |> put_view(StatusView)
701 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
702 end
703 end
704
705 def notifications(%{assigns: %{user: user}} = conn, params) do
706 notifications = MastodonAPI.get_notifications(user, params)
707
708 conn
709 |> add_link_headers(:notifications, notifications)
710 |> put_view(NotificationView)
711 |> render("index.json", %{notifications: notifications, for: user})
712 end
713
714 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
715 with {:ok, notification} <- Notification.get(user, id) do
716 conn
717 |> put_view(NotificationView)
718 |> render("show.json", %{notification: notification, for: user})
719 else
720 {:error, reason} ->
721 conn
722 |> put_status(:forbidden)
723 |> json(%{"error" => reason})
724 end
725 end
726
727 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
728 Notification.clear(user)
729 json(conn, %{})
730 end
731
732 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
733 with {:ok, _notif} <- Notification.dismiss(user, id) do
734 json(conn, %{})
735 else
736 {:error, reason} ->
737 conn
738 |> put_status(:forbidden)
739 |> json(%{"error" => reason})
740 end
741 end
742
743 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
744 Notification.destroy_multiple(user, ids)
745 json(conn, %{})
746 end
747
748 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
749 id = List.wrap(id)
750 q = from(u in User, where: u.id in ^id)
751 targets = Repo.all(q)
752
753 conn
754 |> put_view(AccountView)
755 |> render("relationships.json", %{user: user, targets: targets})
756 end
757
758 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
759 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
760
761 def update_media(%{assigns: %{user: user}} = conn, data) do
762 with %Object{} = object <- Repo.get(Object, data["id"]),
763 true <- Object.authorize_mutation(object, user),
764 true <- is_binary(data["description"]),
765 description <- data["description"] do
766 new_data = %{object.data | "name" => description}
767
768 {:ok, _} =
769 object
770 |> Object.change(%{data: new_data})
771 |> Repo.update()
772
773 attachment_data = Map.put(new_data, "id", object.id)
774
775 conn
776 |> put_view(StatusView)
777 |> render("attachment.json", %{attachment: attachment_data})
778 end
779 end
780
781 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
782 with {:ok, object} <-
783 ActivityPub.upload(
784 file,
785 actor: User.ap_id(user),
786 description: Map.get(data, "description")
787 ) do
788 attachment_data = Map.put(object.data, "id", object.id)
789
790 conn
791 |> put_view(StatusView)
792 |> render("attachment.json", %{attachment: attachment_data})
793 end
794 end
795
796 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
797 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
798 %{} = attachment_data <- Map.put(object.data, "id", object.id),
799 %{type: type} = rendered <-
800 StatusView.render("attachment.json", %{attachment: attachment_data}) do
801 # Reject if not an image
802 if type == "image" do
803 # Sure!
804 # Save to the user's info
805 info_changeset = User.Info.mascot_update(user.info, rendered)
806
807 user_changeset =
808 user
809 |> Changeset.change()
810 |> Changeset.put_embed(:info, info_changeset)
811
812 {:ok, _user} = User.update_and_set_cache(user_changeset)
813
814 conn
815 |> json(rendered)
816 else
817 render_error(conn, :unsupported_media_type, "mascots can only be images")
818 end
819 end
820 end
821
822 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
823 mascot = User.get_mascot(user)
824
825 conn
826 |> json(mascot)
827 end
828
829 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
830 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
831 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
832 q = from(u in User, where: u.ap_id in ^likes)
833
834 users =
835 Repo.all(q)
836 |> Enum.filter(&(not User.blocks?(user, &1)))
837
838 conn
839 |> put_view(AccountView)
840 |> render("accounts.json", %{for: user, users: users, as: :user})
841 else
842 _ -> json(conn, [])
843 end
844 end
845
846 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
847 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
848 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
849 q = from(u in User, where: u.ap_id in ^announces)
850
851 users =
852 Repo.all(q)
853 |> Enum.filter(&(not User.blocks?(user, &1)))
854
855 conn
856 |> put_view(AccountView)
857 |> render("accounts.json", %{for: user, users: users, as: :user})
858 else
859 _ -> json(conn, [])
860 end
861 end
862
863 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
864 local_only = params["local"] in [true, "True", "true", "1"]
865
866 tags =
867 [params["tag"], params["any"]]
868 |> List.flatten()
869 |> Enum.uniq()
870 |> Enum.filter(& &1)
871 |> Enum.map(&String.downcase(&1))
872
873 tag_all =
874 params["all"] ||
875 []
876 |> Enum.map(&String.downcase(&1))
877
878 tag_reject =
879 params["none"] ||
880 []
881 |> Enum.map(&String.downcase(&1))
882
883 activities =
884 params
885 |> Map.put("type", "Create")
886 |> Map.put("local_only", local_only)
887 |> Map.put("blocking_user", user)
888 |> Map.put("muting_user", user)
889 |> Map.put("user", user)
890 |> Map.put("tag", tags)
891 |> Map.put("tag_all", tag_all)
892 |> Map.put("tag_reject", tag_reject)
893 |> ActivityPub.fetch_public_activities()
894 |> Enum.reverse()
895
896 conn
897 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
898 |> put_view(StatusView)
899 |> render("index.json", %{activities: activities, for: user, as: :activity})
900 end
901
902 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
903 with %User{} = user <- User.get_cached_by_id(id),
904 followers <- MastodonAPI.get_followers(user, params) do
905 followers =
906 cond do
907 for_user && user.id == for_user.id -> followers
908 user.info.hide_followers -> []
909 true -> followers
910 end
911
912 conn
913 |> add_link_headers(:followers, followers, user)
914 |> put_view(AccountView)
915 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
916 end
917 end
918
919 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
920 with %User{} = user <- User.get_cached_by_id(id),
921 followers <- MastodonAPI.get_friends(user, params) do
922 followers =
923 cond do
924 for_user && user.id == for_user.id -> followers
925 user.info.hide_follows -> []
926 true -> followers
927 end
928
929 conn
930 |> add_link_headers(:following, followers, user)
931 |> put_view(AccountView)
932 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
933 end
934 end
935
936 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
937 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
938 conn
939 |> put_view(AccountView)
940 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
941 end
942 end
943
944 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
945 with %User{} = follower <- User.get_cached_by_id(id),
946 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
947 conn
948 |> put_view(AccountView)
949 |> render("relationship.json", %{user: followed, target: follower})
950 else
951 {:error, message} ->
952 conn
953 |> put_status(:forbidden)
954 |> json(%{error: message})
955 end
956 end
957
958 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
959 with %User{} = follower <- User.get_cached_by_id(id),
960 {:ok, follower} <- CommonAPI.reject_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 follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
973 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
974 {_, true} <- {:followed, follower.id != followed.id},
975 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
976 conn
977 |> put_view(AccountView)
978 |> render("relationship.json", %{user: follower, target: followed})
979 else
980 {:followed, _} ->
981 {:error, :not_found}
982
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, %{"uri" => uri}) do
991 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
992 {_, true} <- {:followed, follower.id != followed.id},
993 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
994 conn
995 |> put_view(AccountView)
996 |> render("account.json", %{user: followed, for: follower})
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 unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1009 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1010 {_, true} <- {:followed, follower.id != followed.id},
1011 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1012 conn
1013 |> put_view(AccountView)
1014 |> render("relationship.json", %{user: follower, target: followed})
1015 else
1016 {:followed, _} ->
1017 {:error, :not_found}
1018
1019 error ->
1020 error
1021 end
1022 end
1023
1024 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1025 notifications =
1026 if Map.has_key?(params, "notifications"),
1027 do: params["notifications"] in [true, "True", "true", "1"],
1028 else: true
1029
1030 with %User{} = muted <- User.get_cached_by_id(id),
1031 {:ok, muter} <- User.mute(muter, muted, notifications) do
1032 conn
1033 |> put_view(AccountView)
1034 |> render("relationship.json", %{user: muter, target: muted})
1035 else
1036 {:error, message} ->
1037 conn
1038 |> put_status(:forbidden)
1039 |> json(%{error: message})
1040 end
1041 end
1042
1043 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1044 with %User{} = muted <- User.get_cached_by_id(id),
1045 {:ok, muter} <- User.unmute(muter, muted) 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 mutes(%{assigns: %{user: user}} = conn, _) do
1058 with muted_accounts <- User.muted_users(user) do
1059 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1060 json(conn, res)
1061 end
1062 end
1063
1064 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1065 with %User{} = blocked <- User.get_cached_by_id(id),
1066 {:ok, blocker} <- User.block(blocker, blocked),
1067 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1068 conn
1069 |> put_view(AccountView)
1070 |> render("relationship.json", %{user: blocker, target: blocked})
1071 else
1072 {:error, message} ->
1073 conn
1074 |> put_status(:forbidden)
1075 |> json(%{error: message})
1076 end
1077 end
1078
1079 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1080 with %User{} = blocked <- User.get_cached_by_id(id),
1081 {:ok, blocker} <- User.unblock(blocker, blocked),
1082 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1083 conn
1084 |> put_view(AccountView)
1085 |> render("relationship.json", %{user: blocker, target: blocked})
1086 else
1087 {:error, message} ->
1088 conn
1089 |> put_status(:forbidden)
1090 |> json(%{error: message})
1091 end
1092 end
1093
1094 def blocks(%{assigns: %{user: user}} = conn, _) do
1095 with blocked_accounts <- User.blocked_users(user) do
1096 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1097 json(conn, res)
1098 end
1099 end
1100
1101 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1102 json(conn, info.domain_blocks || [])
1103 end
1104
1105 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1106 User.block_domain(blocker, domain)
1107 json(conn, %{})
1108 end
1109
1110 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1111 User.unblock_domain(blocker, domain)
1112 json(conn, %{})
1113 end
1114
1115 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1116 with %User{} = subscription_target <- User.get_cached_by_id(id),
1117 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1118 conn
1119 |> put_view(AccountView)
1120 |> render("relationship.json", %{user: user, target: subscription_target})
1121 else
1122 {:error, message} ->
1123 conn
1124 |> put_status(:forbidden)
1125 |> json(%{error: message})
1126 end
1127 end
1128
1129 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1130 with %User{} = subscription_target <- User.get_cached_by_id(id),
1131 {:ok, subscription_target} = User.unsubscribe(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 favourites(%{assigns: %{user: user}} = conn, params) do
1144 params =
1145 params
1146 |> Map.put("type", "Create")
1147 |> Map.put("favorited_by", user.ap_id)
1148 |> Map.put("blocking_user", user)
1149
1150 activities =
1151 ActivityPub.fetch_activities([], params)
1152 |> Enum.reverse()
1153
1154 conn
1155 |> add_link_headers(:favourites, activities)
1156 |> put_view(StatusView)
1157 |> render("index.json", %{activities: activities, for: user, as: :activity})
1158 end
1159
1160 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1161 with %User{} = user <- User.get_by_id(id),
1162 false <- user.info.hide_favorites do
1163 params =
1164 params
1165 |> Map.put("type", "Create")
1166 |> Map.put("favorited_by", user.ap_id)
1167 |> Map.put("blocking_user", for_user)
1168
1169 recipients =
1170 if for_user do
1171 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1172 else
1173 [Pleroma.Constants.as_public()]
1174 end
1175
1176 activities =
1177 recipients
1178 |> ActivityPub.fetch_activities(params)
1179 |> Enum.reverse()
1180
1181 conn
1182 |> add_link_headers(:favourites, activities)
1183 |> put_view(StatusView)
1184 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1185 else
1186 nil -> {:error, :not_found}
1187 true -> render_error(conn, :forbidden, "Can't get favorites")
1188 end
1189 end
1190
1191 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1192 user = User.get_cached_by_id(user.id)
1193
1194 bookmarks =
1195 Bookmark.for_user_query(user.id)
1196 |> Pagination.fetch_paginated(params)
1197
1198 activities =
1199 bookmarks
1200 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1201
1202 conn
1203 |> add_link_headers(:bookmarks, bookmarks)
1204 |> put_view(StatusView)
1205 |> render("index.json", %{activities: activities, for: user, as: :activity})
1206 end
1207
1208 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1209 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1210 res = ListView.render("lists.json", lists: lists)
1211 json(conn, res)
1212 end
1213
1214 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1215 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1216 params =
1217 params
1218 |> Map.put("type", "Create")
1219 |> Map.put("blocking_user", user)
1220 |> Map.put("user", user)
1221 |> Map.put("muting_user", user)
1222
1223 # we must filter the following list for the user to avoid leaking statuses the user
1224 # does not actually have permission to see (for more info, peruse security issue #270).
1225 activities =
1226 following
1227 |> Enum.filter(fn x -> x in user.following end)
1228 |> ActivityPub.fetch_activities_bounded(following, params)
1229 |> Enum.reverse()
1230
1231 conn
1232 |> put_view(StatusView)
1233 |> render("index.json", %{activities: activities, for: user, as: :activity})
1234 else
1235 _e -> render_error(conn, :forbidden, "Error.")
1236 end
1237 end
1238
1239 def index(%{assigns: %{user: user}} = conn, _params) do
1240 token = get_session(conn, :oauth_token)
1241
1242 if user && token do
1243 mastodon_emoji = mastodonized_emoji()
1244
1245 limit = Config.get([:instance, :limit])
1246
1247 accounts =
1248 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1249
1250 initial_state =
1251 %{
1252 meta: %{
1253 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1254 access_token: token,
1255 locale: "en",
1256 domain: Pleroma.Web.Endpoint.host(),
1257 admin: "1",
1258 me: "#{user.id}",
1259 unfollow_modal: false,
1260 boost_modal: false,
1261 delete_modal: true,
1262 auto_play_gif: false,
1263 display_sensitive_media: false,
1264 reduce_motion: false,
1265 max_toot_chars: limit,
1266 mascot: User.get_mascot(user)["url"]
1267 },
1268 poll_limits: Config.get([:instance, :poll_limits]),
1269 rights: %{
1270 delete_others_notice: present?(user.info.is_moderator),
1271 admin: present?(user.info.is_admin)
1272 },
1273 compose: %{
1274 me: "#{user.id}",
1275 default_privacy: user.info.default_scope,
1276 default_sensitive: false,
1277 allow_content_types: Config.get([:instance, :allowed_post_formats])
1278 },
1279 media_attachments: %{
1280 accept_content_types: [
1281 ".jpg",
1282 ".jpeg",
1283 ".png",
1284 ".gif",
1285 ".webm",
1286 ".mp4",
1287 ".m4v",
1288 "image\/jpeg",
1289 "image\/png",
1290 "image\/gif",
1291 "video\/webm",
1292 "video\/mp4"
1293 ]
1294 },
1295 settings:
1296 user.info.settings ||
1297 %{
1298 onboarded: true,
1299 home: %{
1300 shows: %{
1301 reblog: true,
1302 reply: true
1303 }
1304 },
1305 notifications: %{
1306 alerts: %{
1307 follow: true,
1308 favourite: true,
1309 reblog: true,
1310 mention: true
1311 },
1312 shows: %{
1313 follow: true,
1314 favourite: true,
1315 reblog: true,
1316 mention: true
1317 },
1318 sounds: %{
1319 follow: true,
1320 favourite: true,
1321 reblog: true,
1322 mention: true
1323 }
1324 }
1325 },
1326 push_subscription: nil,
1327 accounts: accounts,
1328 custom_emojis: mastodon_emoji,
1329 char_limit: limit
1330 }
1331 |> Jason.encode!()
1332
1333 conn
1334 |> put_layout(false)
1335 |> put_view(MastodonView)
1336 |> render("index.html", %{initial_state: initial_state})
1337 else
1338 conn
1339 |> put_session(:return_to, conn.request_path)
1340 |> redirect(to: "/web/login")
1341 end
1342 end
1343
1344 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1345 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1346
1347 with changeset <- Changeset.change(user),
1348 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1349 {:ok, _user} <- User.update_and_set_cache(changeset) do
1350 json(conn, %{})
1351 else
1352 e ->
1353 conn
1354 |> put_status(:internal_server_error)
1355 |> json(%{error: inspect(e)})
1356 end
1357 end
1358
1359 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1360 redirect(conn, to: local_mastodon_root_path(conn))
1361 end
1362
1363 @doc "Local Mastodon FE login init action"
1364 def login(conn, %{"code" => auth_token}) do
1365 with {:ok, app} <- get_or_make_app(),
1366 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1367 {:ok, token} <- Token.exchange_token(app, auth) do
1368 conn
1369 |> put_session(:oauth_token, token.token)
1370 |> redirect(to: local_mastodon_root_path(conn))
1371 end
1372 end
1373
1374 @doc "Local Mastodon FE callback action"
1375 def login(conn, _) do
1376 with {:ok, app} <- get_or_make_app() do
1377 path =
1378 o_auth_path(
1379 conn,
1380 :authorize,
1381 response_type: "code",
1382 client_id: app.client_id,
1383 redirect_uri: ".",
1384 scope: Enum.join(app.scopes, " ")
1385 )
1386
1387 redirect(conn, to: path)
1388 end
1389 end
1390
1391 defp local_mastodon_root_path(conn) do
1392 case get_session(conn, :return_to) do
1393 nil ->
1394 mastodon_api_path(conn, :index, ["getting-started"])
1395
1396 return_to ->
1397 delete_session(conn, :return_to)
1398 return_to
1399 end
1400 end
1401
1402 defp get_or_make_app do
1403 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1404 scopes = ["read", "write", "follow", "push"]
1405
1406 with %App{} = app <- Repo.get_by(App, find_attrs) do
1407 {:ok, app} =
1408 if app.scopes == scopes do
1409 {:ok, app}
1410 else
1411 app
1412 |> Changeset.change(%{scopes: scopes})
1413 |> Repo.update()
1414 end
1415
1416 {:ok, app}
1417 else
1418 _e ->
1419 cs =
1420 App.register_changeset(
1421 %App{},
1422 Map.put(find_attrs, :scopes, scopes)
1423 )
1424
1425 Repo.insert(cs)
1426 end
1427 end
1428
1429 def logout(conn, _) do
1430 conn
1431 |> clear_session
1432 |> redirect(to: "/")
1433 end
1434
1435 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1436 Logger.debug("Unimplemented, returning unmodified relationship")
1437
1438 with %User{} = target <- User.get_cached_by_id(id) do
1439 conn
1440 |> put_view(AccountView)
1441 |> render("relationship.json", %{user: user, target: target})
1442 end
1443 end
1444
1445 def empty_array(conn, _) do
1446 Logger.debug("Unimplemented, returning an empty array")
1447 json(conn, [])
1448 end
1449
1450 def empty_object(conn, _) do
1451 Logger.debug("Unimplemented, returning an empty object")
1452 json(conn, %{})
1453 end
1454
1455 def get_filters(%{assigns: %{user: user}} = conn, _) do
1456 filters = Filter.get_filters(user)
1457 res = FilterView.render("filters.json", filters: filters)
1458 json(conn, res)
1459 end
1460
1461 def create_filter(
1462 %{assigns: %{user: user}} = conn,
1463 %{"phrase" => phrase, "context" => context} = params
1464 ) do
1465 query = %Filter{
1466 user_id: user.id,
1467 phrase: phrase,
1468 context: context,
1469 hide: Map.get(params, "irreversible", false),
1470 whole_word: Map.get(params, "boolean", true)
1471 # expires_at
1472 }
1473
1474 {:ok, response} = Filter.create(query)
1475 res = FilterView.render("filter.json", filter: response)
1476 json(conn, res)
1477 end
1478
1479 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1480 filter = Filter.get(filter_id, user)
1481 res = FilterView.render("filter.json", filter: filter)
1482 json(conn, res)
1483 end
1484
1485 def update_filter(
1486 %{assigns: %{user: user}} = conn,
1487 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1488 ) do
1489 query = %Filter{
1490 user_id: user.id,
1491 filter_id: filter_id,
1492 phrase: phrase,
1493 context: context,
1494 hide: Map.get(params, "irreversible", nil),
1495 whole_word: Map.get(params, "boolean", true)
1496 # expires_at
1497 }
1498
1499 {:ok, response} = Filter.update(query)
1500 res = FilterView.render("filter.json", filter: response)
1501 json(conn, res)
1502 end
1503
1504 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1505 query = %Filter{
1506 user_id: user.id,
1507 filter_id: filter_id
1508 }
1509
1510 {:ok, _} = Filter.delete(query)
1511 json(conn, %{})
1512 end
1513
1514 def suggestions(%{assigns: %{user: user}} = conn, _) do
1515 suggestions = Config.get(:suggestions)
1516
1517 if Keyword.get(suggestions, :enabled, false) do
1518 api = Keyword.get(suggestions, :third_party_engine, "")
1519 timeout = Keyword.get(suggestions, :timeout, 5000)
1520 limit = Keyword.get(suggestions, :limit, 23)
1521
1522 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1523
1524 user = user.nickname
1525
1526 url =
1527 api
1528 |> String.replace("{{host}}", host)
1529 |> String.replace("{{user}}", user)
1530
1531 with {:ok, %{status: 200, body: body}} <-
1532 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1533 {:ok, data} <- Jason.decode(body) do
1534 data =
1535 data
1536 |> Enum.slice(0, limit)
1537 |> Enum.map(fn x ->
1538 x
1539 |> Map.put("id", fetch_suggestion_id(x))
1540 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1541 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1542 end)
1543
1544 json(conn, data)
1545 else
1546 e ->
1547 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1548 end
1549 else
1550 json(conn, [])
1551 end
1552 end
1553
1554 defp fetch_suggestion_id(attrs) do
1555 case User.get_or_fetch(attrs["acct"]) do
1556 {:ok, %User{id: id}} -> id
1557 _ -> 0
1558 end
1559 end
1560
1561 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1562 with %Activity{} = activity <- Activity.get_by_id(status_id),
1563 true <- Visibility.visible_for_user?(activity, user) do
1564 data =
1565 StatusView.render(
1566 "card.json",
1567 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1568 )
1569
1570 json(conn, data)
1571 else
1572 _e ->
1573 %{}
1574 end
1575 end
1576
1577 def reports(%{assigns: %{user: user}} = conn, params) do
1578 case CommonAPI.report(user, params) do
1579 {:ok, activity} ->
1580 conn
1581 |> put_view(ReportView)
1582 |> try_render("report.json", %{activity: activity})
1583
1584 {:error, err} ->
1585 conn
1586 |> put_status(:bad_request)
1587 |> json(%{error: err})
1588 end
1589 end
1590
1591 def account_register(
1592 %{assigns: %{app: app}} = conn,
1593 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1594 ) do
1595 params =
1596 params
1597 |> Map.take([
1598 "email",
1599 "captcha_solution",
1600 "captcha_token",
1601 "captcha_answer_data",
1602 "token",
1603 "password"
1604 ])
1605 |> Map.put("nickname", nickname)
1606 |> Map.put("fullname", params["fullname"] || nickname)
1607 |> Map.put("bio", params["bio"] || "")
1608 |> Map.put("confirm", params["password"])
1609
1610 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1611 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1612 json(conn, %{
1613 token_type: "Bearer",
1614 access_token: token.token,
1615 scope: app.scopes,
1616 created_at: Token.Utils.format_created_at(token)
1617 })
1618 else
1619 {:error, errors} ->
1620 conn
1621 |> put_status(:bad_request)
1622 |> json(errors)
1623 end
1624 end
1625
1626 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1627 render_error(conn, :bad_request, "Missing parameters")
1628 end
1629
1630 def account_register(conn, _) do
1631 render_error(conn, :forbidden, "Invalid credentials")
1632 end
1633
1634 def conversations(%{assigns: %{user: user}} = conn, params) do
1635 participations = Participation.for_user_with_last_activity_id(user, params)
1636
1637 conversations =
1638 Enum.map(participations, fn participation ->
1639 ConversationView.render("participation.json", %{participation: participation, for: user})
1640 end)
1641
1642 conn
1643 |> add_link_headers(:conversations, participations)
1644 |> json(conversations)
1645 end
1646
1647 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1648 with %Participation{} = participation <-
1649 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1650 {:ok, participation} <- Participation.mark_as_read(participation) do
1651 participation_view =
1652 ConversationView.render("participation.json", %{participation: participation, for: user})
1653
1654 conn
1655 |> json(participation_view)
1656 end
1657 end
1658
1659 def password_reset(conn, params) do
1660 nickname_or_email = params["email"] || params["nickname"]
1661
1662 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1663 conn
1664 |> put_status(:no_content)
1665 |> json("")
1666 else
1667 {:error, "unknown user"} ->
1668 send_resp(conn, :not_found, "")
1669
1670 {:error, _} ->
1671 send_resp(conn, :bad_request, "")
1672 end
1673 end
1674
1675 def account_confirmation_resend(conn, params) do
1676 nickname_or_email = params["email"] || params["nickname"]
1677
1678 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1679 {:ok, _} <- User.try_send_confirmation_email(user) do
1680 conn
1681 |> json_response(:no_content, "")
1682 end
1683 end
1684
1685 def try_render(conn, target, params)
1686 when is_binary(target) do
1687 case render(conn, target, params) do
1688 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1689 res -> res
1690 end
1691 end
1692
1693 def try_render(conn, _, _) do
1694 render_error(conn, :not_implemented, "Can't display this activity")
1695 end
1696
1697 defp present?(nil), do: false
1698 defp present?(false), do: false
1699 defp present?(_), do: true
1700 end