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