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