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