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