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