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