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