Add CommonAPI.ActivityDraft
[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(
579 %{assigns: %{user: user}} = conn,
580 %{"status" => _, "scheduled_at" => scheduled_at} = params
581 ) do
582 if ScheduledActivity.far_enough?(scheduled_at) do
583 with {:ok, scheduled_activity} <-
584 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
585 conn
586 |> put_view(ScheduledActivityView)
587 |> render("show.json", %{scheduled_activity: scheduled_activity})
588 end
589 else
590 post_status(conn, Map.drop(params, ["scheduled_at"]))
591 end
592 end
593
594 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
595 case CommonAPI.post(user, params) do
596 {:ok, activity} ->
597 conn
598 |> put_view(StatusView)
599 |> try_render("status.json", %{
600 activity: activity,
601 for: user,
602 as: :activity,
603 with_direct_conversation_id: true
604 })
605
606 {:error, message} ->
607 conn
608 |> put_status(:unprocessable_entity)
609 |> json(%{error: message})
610 end
611 end
612
613 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
614 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
615 json(conn, %{})
616 else
617 _e -> render_error(conn, :forbidden, "Can't delete this post")
618 end
619 end
620
621 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
622 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
623 %Activity{} = announce <- Activity.normalize(announce.data) do
624 conn
625 |> put_view(StatusView)
626 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
627 end
628 end
629
630 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
631 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
632 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
633 conn
634 |> put_view(StatusView)
635 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
636 end
637 end
638
639 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
640 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
641 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
642 conn
643 |> put_view(StatusView)
644 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
645 end
646 end
647
648 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
649 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
650 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
651 conn
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
654 end
655 end
656
657 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
658 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
659 conn
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
662 end
663 end
664
665 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
666 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
667 conn
668 |> put_view(StatusView)
669 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
670 end
671 end
672
673 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
674 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
675 %User{} = user <- User.get_cached_by_nickname(user.nickname),
676 true <- Visibility.visible_for_user?(activity, user),
677 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
678 conn
679 |> put_view(StatusView)
680 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
681 end
682 end
683
684 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
685 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
686 %User{} = user <- User.get_cached_by_nickname(user.nickname),
687 true <- Visibility.visible_for_user?(activity, user),
688 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
689 conn
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
692 end
693 end
694
695 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 activity = Activity.get_by_id(id)
697
698 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
699 conn
700 |> put_view(StatusView)
701 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
702 end
703 end
704
705 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
706 activity = Activity.get_by_id(id)
707
708 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
709 conn
710 |> put_view(StatusView)
711 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
712 end
713 end
714
715 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
716 id = List.wrap(id)
717 q = from(u in User, where: u.id in ^id)
718 targets = Repo.all(q)
719
720 conn
721 |> put_view(AccountView)
722 |> render("relationships.json", %{user: user, targets: targets})
723 end
724
725 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
726 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
727
728 def update_media(%{assigns: %{user: user}} = conn, data) do
729 with %Object{} = object <- Repo.get(Object, data["id"]),
730 true <- Object.authorize_mutation(object, user),
731 true <- is_binary(data["description"]),
732 description <- data["description"] do
733 new_data = %{object.data | "name" => description}
734
735 {:ok, _} =
736 object
737 |> Object.change(%{data: new_data})
738 |> Repo.update()
739
740 attachment_data = Map.put(new_data, "id", object.id)
741
742 conn
743 |> put_view(StatusView)
744 |> render("attachment.json", %{attachment: attachment_data})
745 end
746 end
747
748 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
749 with {:ok, object} <-
750 ActivityPub.upload(
751 file,
752 actor: User.ap_id(user),
753 description: Map.get(data, "description")
754 ) do
755 attachment_data = Map.put(object.data, "id", object.id)
756
757 conn
758 |> put_view(StatusView)
759 |> render("attachment.json", %{attachment: attachment_data})
760 end
761 end
762
763 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
764 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
765 %{} = attachment_data <- Map.put(object.data, "id", object.id),
766 # Reject if not an image
767 %{type: "image"} = rendered <-
768 StatusView.render("attachment.json", %{attachment: attachment_data}) do
769 # Sure!
770 # Save to the user's info
771 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
772
773 json(conn, rendered)
774 else
775 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
776 end
777 end
778
779 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
780 mascot = User.get_mascot(user)
781
782 conn
783 |> json(mascot)
784 end
785
786 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
787 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
788 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
789 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
790 q = from(u in User, where: u.ap_id in ^likes)
791
792 users =
793 Repo.all(q)
794 |> Enum.filter(&(not User.blocks?(user, &1)))
795
796 conn
797 |> put_view(AccountView)
798 |> render("accounts.json", %{for: user, users: users, as: :user})
799 else
800 {:visible, false} -> {:error, :not_found}
801 _ -> json(conn, [])
802 end
803 end
804
805 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
806 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
807 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
808 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
809 q = from(u in User, where: u.ap_id in ^announces)
810
811 users =
812 Repo.all(q)
813 |> Enum.filter(&(not User.blocks?(user, &1)))
814
815 conn
816 |> put_view(AccountView)
817 |> render("accounts.json", %{for: user, users: users, as: :user})
818 else
819 {:visible, false} -> {:error, :not_found}
820 _ -> json(conn, [])
821 end
822 end
823
824 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
825 local_only = params["local"] in [true, "True", "true", "1"]
826
827 tags =
828 [params["tag"], params["any"]]
829 |> List.flatten()
830 |> Enum.uniq()
831 |> Enum.filter(& &1)
832 |> Enum.map(&String.downcase(&1))
833
834 tag_all =
835 params["all"] ||
836 []
837 |> Enum.map(&String.downcase(&1))
838
839 tag_reject =
840 params["none"] ||
841 []
842 |> Enum.map(&String.downcase(&1))
843
844 activities =
845 params
846 |> Map.put("type", "Create")
847 |> Map.put("local_only", local_only)
848 |> Map.put("blocking_user", user)
849 |> Map.put("muting_user", user)
850 |> Map.put("user", user)
851 |> Map.put("tag", tags)
852 |> Map.put("tag_all", tag_all)
853 |> Map.put("tag_reject", tag_reject)
854 |> ActivityPub.fetch_public_activities()
855 |> Enum.reverse()
856
857 conn
858 |> add_link_headers(activities, %{"local" => local_only})
859 |> put_view(StatusView)
860 |> render("index.json", %{activities: activities, for: user, as: :activity})
861 end
862
863 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
864 with %User{} = user <- User.get_cached_by_id(id),
865 followers <- MastodonAPI.get_followers(user, params) do
866 followers =
867 cond do
868 for_user && user.id == for_user.id -> followers
869 user.info.hide_followers -> []
870 true -> followers
871 end
872
873 conn
874 |> add_link_headers(followers)
875 |> put_view(AccountView)
876 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
877 end
878 end
879
880 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
881 with %User{} = user <- User.get_cached_by_id(id),
882 followers <- MastodonAPI.get_friends(user, params) do
883 followers =
884 cond do
885 for_user && user.id == for_user.id -> followers
886 user.info.hide_follows -> []
887 true -> followers
888 end
889
890 conn
891 |> add_link_headers(followers)
892 |> put_view(AccountView)
893 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
894 end
895 end
896
897 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
898 follow_requests = User.get_follow_requests(followed)
899
900 conn
901 |> put_view(AccountView)
902 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
903 end
904
905 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
906 with %User{} = follower <- User.get_cached_by_id(id),
907 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
908 conn
909 |> put_view(AccountView)
910 |> render("relationship.json", %{user: followed, target: follower})
911 else
912 {:error, message} ->
913 conn
914 |> put_status(:forbidden)
915 |> json(%{error: message})
916 end
917 end
918
919 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
920 with %User{} = follower <- User.get_cached_by_id(id),
921 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
922 conn
923 |> put_view(AccountView)
924 |> render("relationship.json", %{user: followed, target: follower})
925 else
926 {:error, message} ->
927 conn
928 |> put_status(:forbidden)
929 |> json(%{error: message})
930 end
931 end
932
933 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
934 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
935 {_, true} <- {:followed, follower.id != followed.id},
936 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
937 conn
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: follower, target: followed})
940 else
941 {:followed, _} ->
942 {:error, :not_found}
943
944 {:error, message} ->
945 conn
946 |> put_status(:forbidden)
947 |> json(%{error: message})
948 end
949 end
950
951 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
952 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
953 {_, true} <- {:followed, follower.id != followed.id},
954 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
955 conn
956 |> put_view(AccountView)
957 |> render("account.json", %{user: followed, for: follower})
958 else
959 {:followed, _} ->
960 {:error, :not_found}
961
962 {:error, message} ->
963 conn
964 |> put_status(:forbidden)
965 |> json(%{error: message})
966 end
967 end
968
969 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
970 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
971 {_, true} <- {:followed, follower.id != followed.id},
972 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
973 conn
974 |> put_view(AccountView)
975 |> render("relationship.json", %{user: follower, target: followed})
976 else
977 {:followed, _} ->
978 {:error, :not_found}
979
980 error ->
981 error
982 end
983 end
984
985 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
986 notifications =
987 if Map.has_key?(params, "notifications"),
988 do: params["notifications"] in [true, "True", "true", "1"],
989 else: true
990
991 with %User{} = muted <- User.get_cached_by_id(id),
992 {:ok, muter} <- User.mute(muter, muted, notifications) do
993 conn
994 |> put_view(AccountView)
995 |> render("relationship.json", %{user: muter, target: muted})
996 else
997 {:error, message} ->
998 conn
999 |> put_status(:forbidden)
1000 |> json(%{error: message})
1001 end
1002 end
1003
1004 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1005 with %User{} = muted <- User.get_cached_by_id(id),
1006 {:ok, muter} <- User.unmute(muter, muted) do
1007 conn
1008 |> put_view(AccountView)
1009 |> render("relationship.json", %{user: muter, target: muted})
1010 else
1011 {:error, message} ->
1012 conn
1013 |> put_status(:forbidden)
1014 |> json(%{error: message})
1015 end
1016 end
1017
1018 def mutes(%{assigns: %{user: user}} = conn, _) do
1019 with muted_accounts <- User.muted_users(user) do
1020 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1021 json(conn, res)
1022 end
1023 end
1024
1025 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1026 with %User{} = blocked <- User.get_cached_by_id(id),
1027 {:ok, blocker} <- User.block(blocker, blocked),
1028 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1029 conn
1030 |> put_view(AccountView)
1031 |> render("relationship.json", %{user: blocker, target: blocked})
1032 else
1033 {:error, message} ->
1034 conn
1035 |> put_status(:forbidden)
1036 |> json(%{error: message})
1037 end
1038 end
1039
1040 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1041 with %User{} = blocked <- User.get_cached_by_id(id),
1042 {:ok, blocker} <- User.unblock(blocker, blocked),
1043 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1044 conn
1045 |> put_view(AccountView)
1046 |> render("relationship.json", %{user: blocker, target: blocked})
1047 else
1048 {:error, message} ->
1049 conn
1050 |> put_status(:forbidden)
1051 |> json(%{error: message})
1052 end
1053 end
1054
1055 def blocks(%{assigns: %{user: user}} = conn, _) do
1056 with blocked_accounts <- User.blocked_users(user) do
1057 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1058 json(conn, res)
1059 end
1060 end
1061
1062 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1063 json(conn, info.domain_blocks || [])
1064 end
1065
1066 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1067 User.block_domain(blocker, domain)
1068 json(conn, %{})
1069 end
1070
1071 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1072 User.unblock_domain(blocker, domain)
1073 json(conn, %{})
1074 end
1075
1076 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1077 with %User{} = subscription_target <- User.get_cached_by_id(id),
1078 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1079 conn
1080 |> put_view(AccountView)
1081 |> render("relationship.json", %{user: user, target: subscription_target})
1082 else
1083 {:error, message} ->
1084 conn
1085 |> put_status(:forbidden)
1086 |> json(%{error: message})
1087 end
1088 end
1089
1090 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1091 with %User{} = subscription_target <- User.get_cached_by_id(id),
1092 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1093 conn
1094 |> put_view(AccountView)
1095 |> render("relationship.json", %{user: user, target: subscription_target})
1096 else
1097 {:error, message} ->
1098 conn
1099 |> put_status(:forbidden)
1100 |> json(%{error: message})
1101 end
1102 end
1103
1104 def favourites(%{assigns: %{user: user}} = conn, params) do
1105 params =
1106 params
1107 |> Map.put("type", "Create")
1108 |> Map.put("favorited_by", user.ap_id)
1109 |> Map.put("blocking_user", user)
1110
1111 activities =
1112 ActivityPub.fetch_activities([], params)
1113 |> Enum.reverse()
1114
1115 conn
1116 |> add_link_headers(activities)
1117 |> put_view(StatusView)
1118 |> render("index.json", %{activities: activities, for: user, as: :activity})
1119 end
1120
1121 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1122 with %User{} = user <- User.get_by_id(id),
1123 false <- user.info.hide_favorites do
1124 params =
1125 params
1126 |> Map.put("type", "Create")
1127 |> Map.put("favorited_by", user.ap_id)
1128 |> Map.put("blocking_user", for_user)
1129
1130 recipients =
1131 if for_user do
1132 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1133 else
1134 [Pleroma.Constants.as_public()]
1135 end
1136
1137 activities =
1138 recipients
1139 |> ActivityPub.fetch_activities(params)
1140 |> Enum.reverse()
1141
1142 conn
1143 |> add_link_headers(activities)
1144 |> put_view(StatusView)
1145 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1146 else
1147 nil -> {:error, :not_found}
1148 true -> render_error(conn, :forbidden, "Can't get favorites")
1149 end
1150 end
1151
1152 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1153 user = User.get_cached_by_id(user.id)
1154
1155 bookmarks =
1156 Bookmark.for_user_query(user.id)
1157 |> Pagination.fetch_paginated(params)
1158
1159 activities =
1160 bookmarks
1161 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1162
1163 conn
1164 |> add_link_headers(bookmarks)
1165 |> put_view(StatusView)
1166 |> render("index.json", %{activities: activities, for: user, as: :activity})
1167 end
1168
1169 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1170 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1171 res = ListView.render("lists.json", lists: lists)
1172 json(conn, res)
1173 end
1174
1175 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1176 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1177 params =
1178 params
1179 |> Map.put("type", "Create")
1180 |> Map.put("blocking_user", user)
1181 |> Map.put("user", user)
1182 |> Map.put("muting_user", user)
1183
1184 # we must filter the following list for the user to avoid leaking statuses the user
1185 # does not actually have permission to see (for more info, peruse security issue #270).
1186 activities =
1187 following
1188 |> Enum.filter(fn x -> x in user.following end)
1189 |> ActivityPub.fetch_activities_bounded(following, params)
1190 |> Enum.reverse()
1191
1192 conn
1193 |> put_view(StatusView)
1194 |> render("index.json", %{activities: activities, for: user, as: :activity})
1195 else
1196 _e -> render_error(conn, :forbidden, "Error.")
1197 end
1198 end
1199
1200 def index(%{assigns: %{user: user}} = conn, _params) do
1201 token = get_session(conn, :oauth_token)
1202
1203 if user && token do
1204 mastodon_emoji = mastodonized_emoji()
1205
1206 limit = Config.get([:instance, :limit])
1207
1208 accounts =
1209 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1210
1211 initial_state =
1212 %{
1213 meta: %{
1214 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1215 access_token: token,
1216 locale: "en",
1217 domain: Pleroma.Web.Endpoint.host(),
1218 admin: "1",
1219 me: "#{user.id}",
1220 unfollow_modal: false,
1221 boost_modal: false,
1222 delete_modal: true,
1223 auto_play_gif: false,
1224 display_sensitive_media: false,
1225 reduce_motion: false,
1226 max_toot_chars: limit,
1227 mascot: User.get_mascot(user)["url"]
1228 },
1229 poll_limits: Config.get([:instance, :poll_limits]),
1230 rights: %{
1231 delete_others_notice: present?(user.info.is_moderator),
1232 admin: present?(user.info.is_admin)
1233 },
1234 compose: %{
1235 me: "#{user.id}",
1236 default_privacy: user.info.default_scope,
1237 default_sensitive: false,
1238 allow_content_types: Config.get([:instance, :allowed_post_formats])
1239 },
1240 media_attachments: %{
1241 accept_content_types: [
1242 ".jpg",
1243 ".jpeg",
1244 ".png",
1245 ".gif",
1246 ".webm",
1247 ".mp4",
1248 ".m4v",
1249 "image\/jpeg",
1250 "image\/png",
1251 "image\/gif",
1252 "video\/webm",
1253 "video\/mp4"
1254 ]
1255 },
1256 settings:
1257 user.info.settings ||
1258 %{
1259 onboarded: true,
1260 home: %{
1261 shows: %{
1262 reblog: true,
1263 reply: true
1264 }
1265 },
1266 notifications: %{
1267 alerts: %{
1268 follow: true,
1269 favourite: true,
1270 reblog: true,
1271 mention: true
1272 },
1273 shows: %{
1274 follow: true,
1275 favourite: true,
1276 reblog: true,
1277 mention: true
1278 },
1279 sounds: %{
1280 follow: true,
1281 favourite: true,
1282 reblog: true,
1283 mention: true
1284 }
1285 }
1286 },
1287 push_subscription: nil,
1288 accounts: accounts,
1289 custom_emojis: mastodon_emoji,
1290 char_limit: limit
1291 }
1292 |> Jason.encode!()
1293
1294 conn
1295 |> put_layout(false)
1296 |> put_view(MastodonView)
1297 |> render("index.html", %{initial_state: initial_state})
1298 else
1299 conn
1300 |> put_session(:return_to, conn.request_path)
1301 |> redirect(to: "/web/login")
1302 end
1303 end
1304
1305 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1306 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
1307 json(conn, %{})
1308 else
1309 e ->
1310 conn
1311 |> put_status(:internal_server_error)
1312 |> json(%{error: inspect(e)})
1313 end
1314 end
1315
1316 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1317 redirect(conn, to: local_mastodon_root_path(conn))
1318 end
1319
1320 @doc "Local Mastodon FE login init action"
1321 def login(conn, %{"code" => auth_token}) do
1322 with {:ok, app} <- get_or_make_app(),
1323 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1324 {:ok, token} <- Token.exchange_token(app, auth) do
1325 conn
1326 |> put_session(:oauth_token, token.token)
1327 |> redirect(to: local_mastodon_root_path(conn))
1328 end
1329 end
1330
1331 @doc "Local Mastodon FE callback action"
1332 def login(conn, _) do
1333 with {:ok, app} <- get_or_make_app() do
1334 path =
1335 o_auth_path(
1336 conn,
1337 :authorize,
1338 response_type: "code",
1339 client_id: app.client_id,
1340 redirect_uri: ".",
1341 scope: Enum.join(app.scopes, " ")
1342 )
1343
1344 redirect(conn, to: path)
1345 end
1346 end
1347
1348 defp local_mastodon_root_path(conn) do
1349 case get_session(conn, :return_to) do
1350 nil ->
1351 mastodon_api_path(conn, :index, ["getting-started"])
1352
1353 return_to ->
1354 delete_session(conn, :return_to)
1355 return_to
1356 end
1357 end
1358
1359 defp get_or_make_app do
1360 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1361 scopes = ["read", "write", "follow", "push"]
1362
1363 with %App{} = app <- Repo.get_by(App, find_attrs) do
1364 {:ok, app} =
1365 if app.scopes == scopes do
1366 {:ok, app}
1367 else
1368 app
1369 |> Changeset.change(%{scopes: scopes})
1370 |> Repo.update()
1371 end
1372
1373 {:ok, app}
1374 else
1375 _e ->
1376 cs =
1377 App.register_changeset(
1378 %App{},
1379 Map.put(find_attrs, :scopes, scopes)
1380 )
1381
1382 Repo.insert(cs)
1383 end
1384 end
1385
1386 def logout(conn, _) do
1387 conn
1388 |> clear_session
1389 |> redirect(to: "/")
1390 end
1391
1392 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1393 Logger.debug("Unimplemented, returning unmodified relationship")
1394
1395 with %User{} = target <- User.get_cached_by_id(id) do
1396 conn
1397 |> put_view(AccountView)
1398 |> render("relationship.json", %{user: user, target: target})
1399 end
1400 end
1401
1402 def empty_array(conn, _) do
1403 Logger.debug("Unimplemented, returning an empty array")
1404 json(conn, [])
1405 end
1406
1407 def empty_object(conn, _) do
1408 Logger.debug("Unimplemented, returning an empty object")
1409 json(conn, %{})
1410 end
1411
1412 def get_filters(%{assigns: %{user: user}} = conn, _) do
1413 filters = Filter.get_filters(user)
1414 res = FilterView.render("filters.json", filters: filters)
1415 json(conn, res)
1416 end
1417
1418 def create_filter(
1419 %{assigns: %{user: user}} = conn,
1420 %{"phrase" => phrase, "context" => context} = params
1421 ) do
1422 query = %Filter{
1423 user_id: user.id,
1424 phrase: phrase,
1425 context: context,
1426 hide: Map.get(params, "irreversible", false),
1427 whole_word: Map.get(params, "boolean", true)
1428 # expires_at
1429 }
1430
1431 {:ok, response} = Filter.create(query)
1432 res = FilterView.render("filter.json", filter: response)
1433 json(conn, res)
1434 end
1435
1436 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1437 filter = Filter.get(filter_id, user)
1438 res = FilterView.render("filter.json", filter: filter)
1439 json(conn, res)
1440 end
1441
1442 def update_filter(
1443 %{assigns: %{user: user}} = conn,
1444 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1445 ) do
1446 query = %Filter{
1447 user_id: user.id,
1448 filter_id: filter_id,
1449 phrase: phrase,
1450 context: context,
1451 hide: Map.get(params, "irreversible", nil),
1452 whole_word: Map.get(params, "boolean", true)
1453 # expires_at
1454 }
1455
1456 {:ok, response} = Filter.update(query)
1457 res = FilterView.render("filter.json", filter: response)
1458 json(conn, res)
1459 end
1460
1461 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1462 query = %Filter{
1463 user_id: user.id,
1464 filter_id: filter_id
1465 }
1466
1467 {:ok, _} = Filter.delete(query)
1468 json(conn, %{})
1469 end
1470
1471 def suggestions(%{assigns: %{user: user}} = conn, _) do
1472 suggestions = Config.get(:suggestions)
1473
1474 if Keyword.get(suggestions, :enabled, false) do
1475 api = Keyword.get(suggestions, :third_party_engine, "")
1476 timeout = Keyword.get(suggestions, :timeout, 5000)
1477 limit = Keyword.get(suggestions, :limit, 23)
1478
1479 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1480
1481 user = user.nickname
1482
1483 url =
1484 api
1485 |> String.replace("{{host}}", host)
1486 |> String.replace("{{user}}", user)
1487
1488 with {:ok, %{status: 200, body: body}} <-
1489 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1490 {:ok, data} <- Jason.decode(body) do
1491 data =
1492 data
1493 |> Enum.slice(0, limit)
1494 |> Enum.map(fn x ->
1495 x
1496 |> Map.put("id", fetch_suggestion_id(x))
1497 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1498 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1499 end)
1500
1501 json(conn, data)
1502 else
1503 e ->
1504 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1505 end
1506 else
1507 json(conn, [])
1508 end
1509 end
1510
1511 defp fetch_suggestion_id(attrs) do
1512 case User.get_or_fetch(attrs["acct"]) do
1513 {:ok, %User{id: id}} -> id
1514 _ -> 0
1515 end
1516 end
1517
1518 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1519 with %Activity{} = activity <- Activity.get_by_id(status_id),
1520 true <- Visibility.visible_for_user?(activity, user) do
1521 data =
1522 StatusView.render(
1523 "card.json",
1524 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1525 )
1526
1527 json(conn, data)
1528 else
1529 _e ->
1530 %{}
1531 end
1532 end
1533
1534 def reports(%{assigns: %{user: user}} = conn, params) do
1535 case CommonAPI.report(user, params) do
1536 {:ok, activity} ->
1537 conn
1538 |> put_view(ReportView)
1539 |> try_render("report.json", %{activity: activity})
1540
1541 {:error, err} ->
1542 conn
1543 |> put_status(:bad_request)
1544 |> json(%{error: err})
1545 end
1546 end
1547
1548 def account_register(
1549 %{assigns: %{app: app}} = conn,
1550 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1551 ) do
1552 params =
1553 params
1554 |> Map.take([
1555 "email",
1556 "captcha_solution",
1557 "captcha_token",
1558 "captcha_answer_data",
1559 "token",
1560 "password"
1561 ])
1562 |> Map.put("nickname", nickname)
1563 |> Map.put("fullname", params["fullname"] || nickname)
1564 |> Map.put("bio", params["bio"] || "")
1565 |> Map.put("confirm", params["password"])
1566
1567 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1568 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1569 json(conn, %{
1570 token_type: "Bearer",
1571 access_token: token.token,
1572 scope: app.scopes,
1573 created_at: Token.Utils.format_created_at(token)
1574 })
1575 else
1576 {:error, errors} ->
1577 conn
1578 |> put_status(:bad_request)
1579 |> json(errors)
1580 end
1581 end
1582
1583 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1584 render_error(conn, :bad_request, "Missing parameters")
1585 end
1586
1587 def account_register(conn, _) do
1588 render_error(conn, :forbidden, "Invalid credentials")
1589 end
1590
1591 def conversations(%{assigns: %{user: user}} = conn, params) do
1592 participations = Participation.for_user_with_last_activity_id(user, params)
1593
1594 conversations =
1595 Enum.map(participations, fn participation ->
1596 ConversationView.render("participation.json", %{participation: participation, for: user})
1597 end)
1598
1599 conn
1600 |> add_link_headers(participations)
1601 |> json(conversations)
1602 end
1603
1604 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1605 with %Participation{} = participation <-
1606 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1607 {:ok, participation} <- Participation.mark_as_read(participation) do
1608 participation_view =
1609 ConversationView.render("participation.json", %{participation: participation, for: user})
1610
1611 conn
1612 |> json(participation_view)
1613 end
1614 end
1615
1616 def password_reset(conn, params) do
1617 nickname_or_email = params["email"] || params["nickname"]
1618
1619 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1620 conn
1621 |> put_status(:no_content)
1622 |> json("")
1623 else
1624 {:error, "unknown user"} ->
1625 send_resp(conn, :not_found, "")
1626
1627 {:error, _} ->
1628 send_resp(conn, :bad_request, "")
1629 end
1630 end
1631
1632 def account_confirmation_resend(conn, params) do
1633 nickname_or_email = params["email"] || params["nickname"]
1634
1635 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1636 {:ok, _} <- User.try_send_confirmation_email(user) do
1637 conn
1638 |> json_response(:no_content, "")
1639 end
1640 end
1641
1642 def try_render(conn, target, params)
1643 when is_binary(target) do
1644 case render(conn, target, params) do
1645 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1646 res -> res
1647 end
1648 end
1649
1650 def try_render(conn, _, _) do
1651 render_error(conn, :not_implemented, "Can't display this activity")
1652 end
1653
1654 defp present?(nil), do: false
1655 defp present?(false), do: false
1656 defp present?(_), do: true
1657 end