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