Merge branch 'develop' into feature/database-compaction
[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(id),
547 %User{} = user <- User.get_by_nickname(user.nickname),
548 true <- Visibility.visible_for_user?(activity, user),
549 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
550 conn
551 |> put_view(StatusView)
552 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
553 end
554 end
555
556 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
557 with %Activity{} = activity <- Activity.get_by_id(id),
558 %User{} = user <- User.get_by_nickname(user.nickname),
559 true <- Visibility.visible_for_user?(activity, user),
560 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
561 conn
562 |> put_view(StatusView)
563 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
564 end
565 end
566
567 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
568 activity = Activity.get_by_id(id)
569
570 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
571 conn
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
574 else
575 {:error, reason} ->
576 conn
577 |> put_resp_content_type("application/json")
578 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
579 end
580 end
581
582 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
583 activity = Activity.get_by_id(id)
584
585 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
586 conn
587 |> put_view(StatusView)
588 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
589 end
590 end
591
592 def notifications(%{assigns: %{user: user}} = conn, params) do
593 notifications = MastodonAPI.get_notifications(user, params)
594
595 conn
596 |> add_link_headers(:notifications, notifications)
597 |> put_view(NotificationView)
598 |> render("index.json", %{notifications: notifications, for: user})
599 end
600
601 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
602 with {:ok, notification} <- Notification.get(user, id) do
603 conn
604 |> put_view(NotificationView)
605 |> render("show.json", %{notification: notification, for: user})
606 else
607 {:error, reason} ->
608 conn
609 |> put_resp_content_type("application/json")
610 |> send_resp(403, Jason.encode!(%{"error" => reason}))
611 end
612 end
613
614 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
615 Notification.clear(user)
616 json(conn, %{})
617 end
618
619 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
620 with {:ok, _notif} <- Notification.dismiss(user, id) do
621 json(conn, %{})
622 else
623 {:error, reason} ->
624 conn
625 |> put_resp_content_type("application/json")
626 |> send_resp(403, Jason.encode!(%{"error" => reason}))
627 end
628 end
629
630 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
631 Notification.destroy_multiple(user, ids)
632 json(conn, %{})
633 end
634
635 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
636 id = List.wrap(id)
637 q = from(u in User, where: u.id in ^id)
638 targets = Repo.all(q)
639
640 conn
641 |> put_view(AccountView)
642 |> render("relationships.json", %{user: user, targets: targets})
643 end
644
645 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
646 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
647
648 def update_media(%{assigns: %{user: user}} = conn, data) do
649 with %Object{} = object <- Repo.get(Object, data["id"]),
650 true <- Object.authorize_mutation(object, user),
651 true <- is_binary(data["description"]),
652 description <- data["description"] do
653 new_data = %{object.data | "name" => description}
654
655 {:ok, _} =
656 object
657 |> Object.change(%{data: new_data})
658 |> Repo.update()
659
660 attachment_data = Map.put(new_data, "id", object.id)
661
662 conn
663 |> put_view(StatusView)
664 |> render("attachment.json", %{attachment: attachment_data})
665 end
666 end
667
668 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
669 with {:ok, object} <-
670 ActivityPub.upload(
671 file,
672 actor: User.ap_id(user),
673 description: Map.get(data, "description")
674 ) do
675 attachment_data = Map.put(object.data, "id", object.id)
676
677 conn
678 |> put_view(StatusView)
679 |> render("attachment.json", %{attachment: attachment_data})
680 end
681 end
682
683 def favourited_by(conn, %{"id" => id}) do
684 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
685 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
686 q = from(u in User, where: u.ap_id in ^likes)
687 users = Repo.all(q)
688
689 conn
690 |> put_view(AccountView)
691 |> render(AccountView, "accounts.json", %{users: users, as: :user})
692 else
693 _ -> json(conn, [])
694 end
695 end
696
697 def reblogged_by(conn, %{"id" => id}) do
698 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
699 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
700 q = from(u in User, where: u.ap_id in ^announces)
701 users = Repo.all(q)
702
703 conn
704 |> put_view(AccountView)
705 |> render("accounts.json", %{users: users, as: :user})
706 else
707 _ -> json(conn, [])
708 end
709 end
710
711 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
712 local_only = params["local"] in [true, "True", "true", "1"]
713
714 tags =
715 [params["tag"], params["any"]]
716 |> List.flatten()
717 |> Enum.uniq()
718 |> Enum.filter(& &1)
719 |> Enum.map(&String.downcase(&1))
720
721 tag_all =
722 params["all"] ||
723 []
724 |> Enum.map(&String.downcase(&1))
725
726 tag_reject =
727 params["none"] ||
728 []
729 |> Enum.map(&String.downcase(&1))
730
731 activities =
732 params
733 |> Map.put("type", "Create")
734 |> Map.put("local_only", local_only)
735 |> Map.put("blocking_user", user)
736 |> Map.put("muting_user", user)
737 |> Map.put("tag", tags)
738 |> Map.put("tag_all", tag_all)
739 |> Map.put("tag_reject", tag_reject)
740 |> ActivityPub.fetch_public_activities()
741 |> Enum.reverse()
742
743 conn
744 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
745 |> put_view(StatusView)
746 |> render("index.json", %{activities: activities, for: user, as: :activity})
747 end
748
749 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
750 with %User{} = user <- User.get_by_id(id),
751 followers <- MastodonAPI.get_followers(user, params) do
752 followers =
753 cond do
754 for_user && user.id == for_user.id -> followers
755 user.info.hide_followers -> []
756 true -> followers
757 end
758
759 conn
760 |> add_link_headers(:followers, followers, user)
761 |> put_view(AccountView)
762 |> render("accounts.json", %{users: followers, as: :user})
763 end
764 end
765
766 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
767 with %User{} = user <- User.get_by_id(id),
768 followers <- MastodonAPI.get_friends(user, params) do
769 followers =
770 cond do
771 for_user && user.id == for_user.id -> followers
772 user.info.hide_follows -> []
773 true -> followers
774 end
775
776 conn
777 |> add_link_headers(:following, followers, user)
778 |> put_view(AccountView)
779 |> render("accounts.json", %{users: followers, as: :user})
780 end
781 end
782
783 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
784 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
785 conn
786 |> put_view(AccountView)
787 |> render("accounts.json", %{users: follow_requests, as: :user})
788 end
789 end
790
791 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
792 with %User{} = follower <- User.get_by_id(id),
793 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
794 conn
795 |> put_view(AccountView)
796 |> render("relationship.json", %{user: followed, target: follower})
797 else
798 {:error, message} ->
799 conn
800 |> put_resp_content_type("application/json")
801 |> send_resp(403, Jason.encode!(%{"error" => message}))
802 end
803 end
804
805 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
806 with %User{} = follower <- User.get_by_id(id),
807 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
808 conn
809 |> put_view(AccountView)
810 |> render("relationship.json", %{user: followed, target: follower})
811 else
812 {:error, message} ->
813 conn
814 |> put_resp_content_type("application/json")
815 |> send_resp(403, Jason.encode!(%{"error" => message}))
816 end
817 end
818
819 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
820 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
821 {_, true} <- {:followed, follower.id != followed.id},
822 false <- User.following?(follower, followed),
823 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
824 conn
825 |> put_view(AccountView)
826 |> render("relationship.json", %{user: follower, target: followed})
827 else
828 {:followed, _} ->
829 {:error, :not_found}
830
831 true ->
832 followed = User.get_cached_by_id(id)
833
834 {:ok, follower} =
835 case conn.params["reblogs"] do
836 true -> CommonAPI.show_reblogs(follower, followed)
837 false -> CommonAPI.hide_reblogs(follower, followed)
838 end
839
840 conn
841 |> put_view(AccountView)
842 |> render("relationship.json", %{user: follower, target: followed})
843
844 {:error, message} ->
845 conn
846 |> put_resp_content_type("application/json")
847 |> send_resp(403, Jason.encode!(%{"error" => message}))
848 end
849 end
850
851 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
852 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
853 {_, true} <- {:followed, follower.id != followed.id},
854 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
855 conn
856 |> put_view(AccountView)
857 |> render("account.json", %{user: followed, for: follower})
858 else
859 {:followed, _} ->
860 {:error, :not_found}
861
862 {:error, message} ->
863 conn
864 |> put_resp_content_type("application/json")
865 |> send_resp(403, Jason.encode!(%{"error" => message}))
866 end
867 end
868
869 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
870 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
871 {_, true} <- {:followed, follower.id != followed.id},
872 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
873 conn
874 |> put_view(AccountView)
875 |> render("relationship.json", %{user: follower, target: followed})
876 else
877 {:followed, _} ->
878 {:error, :not_found}
879
880 error ->
881 error
882 end
883 end
884
885 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
886 with %User{} = muted <- User.get_by_id(id),
887 {:ok, muter} <- User.mute(muter, muted) do
888 conn
889 |> put_view(AccountView)
890 |> render("relationship.json", %{user: muter, target: muted})
891 else
892 {:error, message} ->
893 conn
894 |> put_resp_content_type("application/json")
895 |> send_resp(403, Jason.encode!(%{"error" => message}))
896 end
897 end
898
899 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
900 with %User{} = muted <- User.get_by_id(id),
901 {:ok, muter} <- User.unmute(muter, muted) do
902 conn
903 |> put_view(AccountView)
904 |> render("relationship.json", %{user: muter, target: muted})
905 else
906 {:error, message} ->
907 conn
908 |> put_resp_content_type("application/json")
909 |> send_resp(403, Jason.encode!(%{"error" => message}))
910 end
911 end
912
913 def mutes(%{assigns: %{user: user}} = conn, _) do
914 with muted_accounts <- User.muted_users(user) do
915 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
916 json(conn, res)
917 end
918 end
919
920 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
921 with %User{} = blocked <- User.get_by_id(id),
922 {:ok, blocker} <- User.block(blocker, blocked),
923 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
924 conn
925 |> put_view(AccountView)
926 |> render("relationship.json", %{user: blocker, target: blocked})
927 else
928 {:error, message} ->
929 conn
930 |> put_resp_content_type("application/json")
931 |> send_resp(403, Jason.encode!(%{"error" => message}))
932 end
933 end
934
935 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
936 with %User{} = blocked <- User.get_by_id(id),
937 {:ok, blocker} <- User.unblock(blocker, blocked),
938 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
939 conn
940 |> put_view(AccountView)
941 |> render("relationship.json", %{user: blocker, target: blocked})
942 else
943 {:error, message} ->
944 conn
945 |> put_resp_content_type("application/json")
946 |> send_resp(403, Jason.encode!(%{"error" => message}))
947 end
948 end
949
950 def blocks(%{assigns: %{user: user}} = conn, _) do
951 with blocked_accounts <- User.blocked_users(user) do
952 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
953 json(conn, res)
954 end
955 end
956
957 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
958 json(conn, info.domain_blocks || [])
959 end
960
961 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
962 User.block_domain(blocker, domain)
963 json(conn, %{})
964 end
965
966 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
967 User.unblock_domain(blocker, domain)
968 json(conn, %{})
969 end
970
971 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
972 with %User{} = subscription_target <- User.get_cached_by_id(id),
973 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
974 conn
975 |> put_view(AccountView)
976 |> render("relationship.json", %{user: user, target: subscription_target})
977 else
978 {:error, message} ->
979 conn
980 |> put_resp_content_type("application/json")
981 |> send_resp(403, Jason.encode!(%{"error" => message}))
982 end
983 end
984
985 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
986 with %User{} = subscription_target <- User.get_cached_by_id(id),
987 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
988 conn
989 |> put_view(AccountView)
990 |> render("relationship.json", %{user: user, target: subscription_target})
991 else
992 {:error, message} ->
993 conn
994 |> put_resp_content_type("application/json")
995 |> send_resp(403, Jason.encode!(%{"error" => message}))
996 end
997 end
998
999 def status_search(user, query) do
1000 fetched =
1001 if Regex.match?(~r/https?:/, query) do
1002 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1003 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1004 true <- Visibility.visible_for_user?(activity, user) do
1005 [activity]
1006 else
1007 _e -> []
1008 end
1009 end || []
1010
1011 q =
1012 from(
1013 a in Activity,
1014 where: fragment("?->>'type' = 'Create'", a.data),
1015 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1016 where:
1017 fragment(
1018 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
1019 a.data,
1020 ^query
1021 ),
1022 limit: 20,
1023 order_by: [desc: :id]
1024 )
1025
1026 Repo.all(q) ++ fetched
1027 end
1028
1029 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1030 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1031
1032 statuses = status_search(user, query)
1033
1034 tags_path = Web.base_url() <> "/tag/"
1035
1036 tags =
1037 query
1038 |> String.split()
1039 |> Enum.uniq()
1040 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1041 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1042 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1043
1044 res = %{
1045 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1046 "statuses" =>
1047 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1048 "hashtags" => tags
1049 }
1050
1051 json(conn, res)
1052 end
1053
1054 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1055 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1056
1057 statuses = status_search(user, query)
1058
1059 tags =
1060 query
1061 |> String.split()
1062 |> Enum.uniq()
1063 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1064 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1065
1066 res = %{
1067 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1068 "statuses" =>
1069 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1070 "hashtags" => tags
1071 }
1072
1073 json(conn, res)
1074 end
1075
1076 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1077 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1078
1079 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1080
1081 json(conn, res)
1082 end
1083
1084 def favourites(%{assigns: %{user: user}} = conn, params) do
1085 params =
1086 params
1087 |> Map.put("type", "Create")
1088 |> Map.put("favorited_by", user.ap_id)
1089 |> Map.put("blocking_user", user)
1090
1091 activities =
1092 ActivityPub.fetch_activities([], params)
1093 |> Enum.reverse()
1094
1095 conn
1096 |> add_link_headers(:favourites, activities)
1097 |> put_view(StatusView)
1098 |> render("index.json", %{activities: activities, for: user, as: :activity})
1099 end
1100
1101 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1102 user = User.get_by_id(user.id)
1103
1104 activities =
1105 user.bookmarks
1106 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1107 |> Enum.reverse()
1108
1109 conn
1110 |> put_view(StatusView)
1111 |> render("index.json", %{activities: activities, for: user, as: :activity})
1112 end
1113
1114 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1115 lists = Pleroma.List.for_user(user, opts)
1116 res = ListView.render("lists.json", lists: lists)
1117 json(conn, res)
1118 end
1119
1120 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1121 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1122 res = ListView.render("list.json", list: list)
1123 json(conn, res)
1124 else
1125 _e ->
1126 conn
1127 |> put_status(404)
1128 |> json(%{error: "Record not found"})
1129 end
1130 end
1131
1132 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1133 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1134 res = ListView.render("lists.json", lists: lists)
1135 json(conn, res)
1136 end
1137
1138 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1139 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1140 {:ok, _list} <- Pleroma.List.delete(list) do
1141 json(conn, %{})
1142 else
1143 _e ->
1144 json(conn, "error")
1145 end
1146 end
1147
1148 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1149 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1150 res = ListView.render("list.json", list: list)
1151 json(conn, res)
1152 end
1153 end
1154
1155 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1156 accounts
1157 |> Enum.each(fn account_id ->
1158 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1159 %User{} = followed <- User.get_by_id(account_id) do
1160 Pleroma.List.follow(list, followed)
1161 end
1162 end)
1163
1164 json(conn, %{})
1165 end
1166
1167 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1168 accounts
1169 |> Enum.each(fn account_id ->
1170 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1171 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1172 Pleroma.List.unfollow(list, followed)
1173 end
1174 end)
1175
1176 json(conn, %{})
1177 end
1178
1179 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1180 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1181 {:ok, users} = Pleroma.List.get_following(list) do
1182 conn
1183 |> put_view(AccountView)
1184 |> render("accounts.json", %{users: users, as: :user})
1185 end
1186 end
1187
1188 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1189 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1190 {:ok, list} <- Pleroma.List.rename(list, title) do
1191 res = ListView.render("list.json", list: list)
1192 json(conn, res)
1193 else
1194 _e ->
1195 json(conn, "error")
1196 end
1197 end
1198
1199 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1200 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1201 params =
1202 params
1203 |> Map.put("type", "Create")
1204 |> Map.put("blocking_user", user)
1205 |> Map.put("muting_user", user)
1206
1207 # we must filter the following list for the user to avoid leaking statuses the user
1208 # does not actually have permission to see (for more info, peruse security issue #270).
1209 activities =
1210 following
1211 |> Enum.filter(fn x -> x in user.following end)
1212 |> ActivityPub.fetch_activities_bounded(following, params)
1213 |> Enum.reverse()
1214
1215 conn
1216 |> put_view(StatusView)
1217 |> render("index.json", %{activities: activities, for: user, as: :activity})
1218 else
1219 _e ->
1220 conn
1221 |> put_status(403)
1222 |> json(%{error: "Error."})
1223 end
1224 end
1225
1226 def index(%{assigns: %{user: user}} = conn, _params) do
1227 token = get_session(conn, :oauth_token)
1228
1229 if user && token do
1230 mastodon_emoji = mastodonized_emoji()
1231
1232 limit = Config.get([:instance, :limit])
1233
1234 accounts =
1235 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1236
1237 flavour = get_user_flavour(user)
1238
1239 initial_state =
1240 %{
1241 meta: %{
1242 streaming_api_base_url:
1243 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1244 access_token: token,
1245 locale: "en",
1246 domain: Pleroma.Web.Endpoint.host(),
1247 admin: "1",
1248 me: "#{user.id}",
1249 unfollow_modal: false,
1250 boost_modal: false,
1251 delete_modal: true,
1252 auto_play_gif: false,
1253 display_sensitive_media: false,
1254 reduce_motion: false,
1255 max_toot_chars: limit,
1256 mascot: "/images/pleroma-fox-tan-smol.png"
1257 },
1258 rights: %{
1259 delete_others_notice: present?(user.info.is_moderator),
1260 admin: present?(user.info.is_admin)
1261 },
1262 compose: %{
1263 me: "#{user.id}",
1264 default_privacy: user.info.default_scope,
1265 default_sensitive: false,
1266 allow_content_types: Config.get([:instance, :allowed_post_formats])
1267 },
1268 media_attachments: %{
1269 accept_content_types: [
1270 ".jpg",
1271 ".jpeg",
1272 ".png",
1273 ".gif",
1274 ".webm",
1275 ".mp4",
1276 ".m4v",
1277 "image\/jpeg",
1278 "image\/png",
1279 "image\/gif",
1280 "video\/webm",
1281 "video\/mp4"
1282 ]
1283 },
1284 settings:
1285 user.info.settings ||
1286 %{
1287 onboarded: true,
1288 home: %{
1289 shows: %{
1290 reblog: true,
1291 reply: true
1292 }
1293 },
1294 notifications: %{
1295 alerts: %{
1296 follow: true,
1297 favourite: true,
1298 reblog: true,
1299 mention: true
1300 },
1301 shows: %{
1302 follow: true,
1303 favourite: true,
1304 reblog: true,
1305 mention: true
1306 },
1307 sounds: %{
1308 follow: true,
1309 favourite: true,
1310 reblog: true,
1311 mention: true
1312 }
1313 }
1314 },
1315 push_subscription: nil,
1316 accounts: accounts,
1317 custom_emojis: mastodon_emoji,
1318 char_limit: limit
1319 }
1320 |> Jason.encode!()
1321
1322 conn
1323 |> put_layout(false)
1324 |> put_view(MastodonView)
1325 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1326 else
1327 conn
1328 |> put_session(:return_to, conn.request_path)
1329 |> redirect(to: "/web/login")
1330 end
1331 end
1332
1333 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1334 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1335
1336 with changeset <- Ecto.Changeset.change(user),
1337 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1338 {:ok, _user} <- User.update_and_set_cache(changeset) do
1339 json(conn, %{})
1340 else
1341 e ->
1342 conn
1343 |> put_resp_content_type("application/json")
1344 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1345 end
1346 end
1347
1348 @supported_flavours ["glitch", "vanilla"]
1349
1350 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1351 when flavour in @supported_flavours do
1352 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1353
1354 with changeset <- Ecto.Changeset.change(user),
1355 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1356 {:ok, user} <- User.update_and_set_cache(changeset),
1357 flavour <- user.info.flavour do
1358 json(conn, flavour)
1359 else
1360 e ->
1361 conn
1362 |> put_resp_content_type("application/json")
1363 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1364 end
1365 end
1366
1367 def set_flavour(conn, _params) do
1368 conn
1369 |> put_status(400)
1370 |> json(%{error: "Unsupported flavour"})
1371 end
1372
1373 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1374 json(conn, get_user_flavour(user))
1375 end
1376
1377 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1378 flavour
1379 end
1380
1381 defp get_user_flavour(_) do
1382 "glitch"
1383 end
1384
1385 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1386 redirect(conn, to: local_mastodon_root_path(conn))
1387 end
1388
1389 @doc "Local Mastodon FE login init action"
1390 def login(conn, %{"code" => auth_token}) do
1391 with {:ok, app} <- get_or_make_app(),
1392 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1393 {:ok, token} <- Token.exchange_token(app, auth) do
1394 conn
1395 |> put_session(:oauth_token, token.token)
1396 |> redirect(to: local_mastodon_root_path(conn))
1397 end
1398 end
1399
1400 @doc "Local Mastodon FE callback action"
1401 def login(conn, _) do
1402 with {:ok, app} <- get_or_make_app() do
1403 path =
1404 o_auth_path(
1405 conn,
1406 :authorize,
1407 response_type: "code",
1408 client_id: app.client_id,
1409 redirect_uri: ".",
1410 scope: Enum.join(app.scopes, " ")
1411 )
1412
1413 redirect(conn, to: path)
1414 end
1415 end
1416
1417 defp local_mastodon_root_path(conn) do
1418 case get_session(conn, :return_to) do
1419 nil ->
1420 mastodon_api_path(conn, :index, ["getting-started"])
1421
1422 return_to ->
1423 delete_session(conn, :return_to)
1424 return_to
1425 end
1426 end
1427
1428 defp get_or_make_app do
1429 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1430 scopes = ["read", "write", "follow", "push"]
1431
1432 with %App{} = app <- Repo.get_by(App, find_attrs) do
1433 {:ok, app} =
1434 if app.scopes == scopes do
1435 {:ok, app}
1436 else
1437 app
1438 |> Ecto.Changeset.change(%{scopes: scopes})
1439 |> Repo.update()
1440 end
1441
1442 {:ok, app}
1443 else
1444 _e ->
1445 cs =
1446 App.register_changeset(
1447 %App{},
1448 Map.put(find_attrs, :scopes, scopes)
1449 )
1450
1451 Repo.insert(cs)
1452 end
1453 end
1454
1455 def logout(conn, _) do
1456 conn
1457 |> clear_session
1458 |> redirect(to: "/")
1459 end
1460
1461 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1462 Logger.debug("Unimplemented, returning unmodified relationship")
1463
1464 with %User{} = target <- User.get_by_id(id) do
1465 conn
1466 |> put_view(AccountView)
1467 |> render("relationship.json", %{user: user, target: target})
1468 end
1469 end
1470
1471 def empty_array(conn, _) do
1472 Logger.debug("Unimplemented, returning an empty array")
1473 json(conn, [])
1474 end
1475
1476 def empty_object(conn, _) do
1477 Logger.debug("Unimplemented, returning an empty object")
1478 json(conn, %{})
1479 end
1480
1481 def get_filters(%{assigns: %{user: user}} = conn, _) do
1482 filters = Filter.get_filters(user)
1483 res = FilterView.render("filters.json", filters: filters)
1484 json(conn, res)
1485 end
1486
1487 def create_filter(
1488 %{assigns: %{user: user}} = conn,
1489 %{"phrase" => phrase, "context" => context} = params
1490 ) do
1491 query = %Filter{
1492 user_id: user.id,
1493 phrase: phrase,
1494 context: context,
1495 hide: Map.get(params, "irreversible", nil),
1496 whole_word: Map.get(params, "boolean", true)
1497 # expires_at
1498 }
1499
1500 {:ok, response} = Filter.create(query)
1501 res = FilterView.render("filter.json", filter: response)
1502 json(conn, res)
1503 end
1504
1505 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1506 filter = Filter.get(filter_id, user)
1507 res = FilterView.render("filter.json", filter: filter)
1508 json(conn, res)
1509 end
1510
1511 def update_filter(
1512 %{assigns: %{user: user}} = conn,
1513 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1514 ) do
1515 query = %Filter{
1516 user_id: user.id,
1517 filter_id: filter_id,
1518 phrase: phrase,
1519 context: context,
1520 hide: Map.get(params, "irreversible", nil),
1521 whole_word: Map.get(params, "boolean", true)
1522 # expires_at
1523 }
1524
1525 {:ok, response} = Filter.update(query)
1526 res = FilterView.render("filter.json", filter: response)
1527 json(conn, res)
1528 end
1529
1530 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1531 query = %Filter{
1532 user_id: user.id,
1533 filter_id: filter_id
1534 }
1535
1536 {:ok, _} = Filter.delete(query)
1537 json(conn, %{})
1538 end
1539
1540 # fallback action
1541 #
1542 def errors(conn, {:error, %Changeset{} = changeset}) do
1543 error_message =
1544 changeset
1545 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1546 |> Enum.map_join(", ", fn {_k, v} -> v end)
1547
1548 conn
1549 |> put_status(422)
1550 |> json(%{error: error_message})
1551 end
1552
1553 def errors(conn, {:error, :not_found}) do
1554 conn
1555 |> put_status(404)
1556 |> json(%{error: "Record not found"})
1557 end
1558
1559 def errors(conn, _) do
1560 conn
1561 |> put_status(500)
1562 |> json("Something went wrong")
1563 end
1564
1565 def suggestions(%{assigns: %{user: user}} = conn, _) do
1566 suggestions = Config.get(:suggestions)
1567
1568 if Keyword.get(suggestions, :enabled, false) do
1569 api = Keyword.get(suggestions, :third_party_engine, "")
1570 timeout = Keyword.get(suggestions, :timeout, 5000)
1571 limit = Keyword.get(suggestions, :limit, 23)
1572
1573 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1574
1575 user = user.nickname
1576
1577 url =
1578 api
1579 |> String.replace("{{host}}", host)
1580 |> String.replace("{{user}}", user)
1581
1582 with {:ok, %{status: 200, body: body}} <-
1583 @httpoison.get(
1584 url,
1585 [],
1586 adapter: [
1587 recv_timeout: timeout,
1588 pool: :default
1589 ]
1590 ),
1591 {:ok, data} <- Jason.decode(body) do
1592 data =
1593 data
1594 |> Enum.slice(0, limit)
1595 |> Enum.map(fn x ->
1596 Map.put(
1597 x,
1598 "id",
1599 case User.get_or_fetch(x["acct"]) do
1600 %{id: id} -> id
1601 _ -> 0
1602 end
1603 )
1604 end)
1605 |> Enum.map(fn x ->
1606 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1607 end)
1608 |> Enum.map(fn x ->
1609 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1610 end)
1611
1612 conn
1613 |> json(data)
1614 else
1615 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1616 end
1617 else
1618 json(conn, [])
1619 end
1620 end
1621
1622 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1623 with %Activity{} = activity <- Activity.get_by_id(status_id),
1624 true <- Visibility.visible_for_user?(activity, user) do
1625 data =
1626 StatusView.render(
1627 "card.json",
1628 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1629 )
1630
1631 json(conn, data)
1632 else
1633 _e ->
1634 %{}
1635 end
1636 end
1637
1638 def reports(%{assigns: %{user: user}} = conn, params) do
1639 case CommonAPI.report(user, params) do
1640 {:ok, activity} ->
1641 conn
1642 |> put_view(ReportView)
1643 |> try_render("report.json", %{activity: activity})
1644
1645 {:error, err} ->
1646 conn
1647 |> put_status(:bad_request)
1648 |> json(%{error: err})
1649 end
1650 end
1651
1652 def try_render(conn, target, params)
1653 when is_binary(target) do
1654 res = render(conn, target, params)
1655
1656 if res == nil do
1657 conn
1658 |> put_status(501)
1659 |> json(%{error: "Can't display this activity"})
1660 else
1661 res
1662 end
1663 end
1664
1665 def try_render(conn, _, _) do
1666 conn
1667 |> put_status(501)
1668 |> json(%{error: "Can't display this activity"})
1669 end
1670
1671 defp present?(nil), do: false
1672 defp present?(false), do: false
1673 defp present?(_), do: true
1674 end