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