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