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