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