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