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