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