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