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