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