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