1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
17 alias Pleroma.Formatter
19 alias Pleroma.Notification
21 alias Pleroma.Pagination
22 alias Pleroma.Plugs.OAuthScopesPlug
23 alias Pleroma.Plugs.RateLimiter
25 alias Pleroma.ScheduledActivity
29 alias Pleroma.Web.ActivityPub.ActivityPub
30 alias Pleroma.Web.ActivityPub.Visibility
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.MastodonAPI.AccountView
33 alias Pleroma.Web.MastodonAPI.AppView
34 alias Pleroma.Web.MastodonAPI.ConversationView
35 alias Pleroma.Web.MastodonAPI.FilterView
36 alias Pleroma.Web.MastodonAPI.ListView
37 alias Pleroma.Web.MastodonAPI.MastodonAPI
38 alias Pleroma.Web.MastodonAPI.MastodonView
39 alias Pleroma.Web.MastodonAPI.NotificationView
40 alias Pleroma.Web.MastodonAPI.ReportView
41 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
42 alias Pleroma.Web.MastodonAPI.StatusView
43 alias Pleroma.Web.MediaProxy
44 alias Pleroma.Web.OAuth.App
45 alias Pleroma.Web.OAuth.Authorization
46 alias Pleroma.Web.OAuth.Scopes
47 alias Pleroma.Web.OAuth.Token
48 alias Pleroma.Web.TwitterAPI.TwitterAPI
50 alias Pleroma.Web.ControllerHelper
54 require Pleroma.Constants
56 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
58 # Note: :index action handles attempt of unauthenticated access to private instance with redirect
61 Map.merge(@unauthenticated_access, %{scopes: ["read"], skip_instance_privacy_check: true})
67 %{scopes: ["read"]} when action in [:suggestions, :verify_app_credentials]
72 %{scopes: ["write:accounts"]}
73 # Note: the following actions are not permission-secured in Mastodon:
85 %{scopes: ["write:accounts"]}
86 when action in [:pin_status, :unpin_status, :update_credentials]
91 %{scopes: ["read:statuses"]}
95 :show_scheduled_status,
103 %{@unauthenticated_access | scopes: ["read:statuses"]}
116 %{scopes: ["write:statuses"]}
118 :update_scheduled_status,
119 :delete_scheduled_status,
128 plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :conversation_read)
132 %{scopes: ["read:accounts"]}
133 when action in [:endorsements, :verify_credentials, :followers, :following, :get_mascot]
138 %{@unauthenticated_access | scopes: ["read:accounts"]}
139 when action in [:user, :favourited_by, :reblogged_by]
144 %{scopes: ["read:favourites"]} when action in [:favourites, :user_favourites]
149 %{scopes: ["write:favourites"]} when action in [:fav_status, :unfav_status]
152 plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in [:get_filters, :get_filter])
156 %{scopes: ["write:filters"]} when action in [:create_filter, :update_filter, :delete_filter]
159 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:account_lists, :list_timeline])
161 plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action in [:upload, :update_media])
165 %{scopes: ["read:notifications"]} when action in [:notifications, :get_notification]
170 %{scopes: ["write:notifications"]}
171 when action in [:clear_notifications, :dismiss_notification, :destroy_multiple_notifications]
176 %{scopes: ["write:reports"]}
177 when action in [:create_report, :report_update_state, :report_respond]
182 %{scopes: ["follow", "read:blocks"]} when action in [:blocks, :domain_blocks]
187 %{scopes: ["follow", "write:blocks"]}
188 when action in [:block, :unblock, :block_domain, :unblock_domain]
191 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
192 plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :follow_requests)
196 %{scopes: ["follow", "write:follows"]}
202 :authorize_follow_request,
203 :reject_follow_request
207 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
208 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
212 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
215 # Note: scopes not present in Mastodon: read:bookmarks, write:bookmarks
216 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
220 %{scopes: ["write:bookmarks"]} when action in [:bookmark_status, :unbookmark_status]
223 # An extra safety measure for possible actions not guarded by OAuth permissions specification
225 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
233 :account_confirmation_resend,
240 @rate_limited_relations_actions ~w(follow unfollow)a
242 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
243 post_status delete_status)a
247 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
248 when action in ~w(reblog_status unreblog_status)a
253 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
254 when action in ~w(fav_status unfav_status)a
259 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
262 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
263 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
264 plug(RateLimiter, :app_account_creation when action == :account_register)
265 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
266 plug(RateLimiter, :password_reset when action == :password_reset)
267 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
269 @local_mastodon_name "Mastodon-Local"
271 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
273 def create_app(conn, params) do
274 scopes = Scopes.fetch_scopes(params, ["read"])
278 |> Map.drop(["scope", "scopes"])
279 |> Map.put("scopes", scopes)
281 with cs <- App.register_changeset(%App{}, app_attrs),
282 false <- cs.changes[:client_name] == @local_mastodon_name,
283 {:ok, app} <- Repo.insert(cs) do
286 |> render("show.json", %{app: app})
295 value_function \\ fn x -> {:ok, x} end
297 if Map.has_key?(params, params_field) do
298 case value_function.(params[params_field]) do
299 {:ok, new_value} -> Map.put(map, map_field, new_value)
307 def update_credentials(%{assigns: %{user: user}} = conn, params) do
312 |> add_if_present(params, "display_name", :name)
313 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
314 |> add_if_present(params, "avatar", :avatar, fn value ->
315 with %Plug.Upload{} <- value,
316 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
323 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
327 |> Map.get(:emoji, [])
328 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
335 :hide_followers_count,
341 :skip_thread_containment
343 |> Enum.reduce(%{}, fn key, acc ->
344 add_if_present(acc, params, to_string(key), key, fn value ->
345 {:ok, ControllerHelper.truthy_param?(value)}
348 |> add_if_present(params, "default_scope", :default_scope)
349 |> add_if_present(params, "fields", :fields, fn fields ->
350 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
354 |> add_if_present(params, "fields", :raw_fields)
355 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
356 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
358 |> add_if_present(params, "header", :banner, fn value ->
359 with %Plug.Upload{} <- value,
360 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
366 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
367 with %Plug.Upload{} <- value,
368 {:ok, object} <- ActivityPub.upload(value, type: :background) do
374 |> Map.put(:emoji, user_info_emojis)
376 info_cng = User.Info.profile_update(user.info, info_params)
378 with changeset <- User.update_changeset(user, user_params),
379 changeset <- Changeset.put_embed(changeset, :info, info_cng),
380 {:ok, user} <- User.update_and_set_cache(changeset) do
381 if original_user != user do
382 CommonAPI.update(user)
387 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
390 _e -> render_error(conn, :forbidden, "Invalid request")
394 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
395 change = Changeset.change(user, %{avatar: nil})
396 {:ok, user} = User.update_and_set_cache(change)
397 CommonAPI.update(user)
399 json(conn, %{url: nil})
402 def update_avatar(%{assigns: %{user: user}} = conn, params) do
403 {:ok, object} = ActivityPub.upload(params, type: :avatar)
404 change = Changeset.change(user, %{avatar: object.data})
405 {:ok, user} = User.update_and_set_cache(change)
406 CommonAPI.update(user)
407 %{"url" => [%{"href" => href} | _]} = object.data
409 json(conn, %{url: href})
412 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
413 with new_info <- %{"banner" => %{}},
414 info_cng <- User.Info.profile_update(user.info, new_info),
415 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
416 {:ok, user} <- User.update_and_set_cache(changeset) do
417 CommonAPI.update(user)
419 json(conn, %{url: nil})
423 def update_banner(%{assigns: %{user: user}} = conn, params) do
424 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
425 new_info <- %{"banner" => object.data},
426 info_cng <- User.Info.profile_update(user.info, new_info),
427 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
428 {:ok, user} <- User.update_and_set_cache(changeset) do
429 CommonAPI.update(user)
430 %{"url" => [%{"href" => href} | _]} = object.data
432 json(conn, %{url: href})
436 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
437 with new_info <- %{"background" => %{}},
438 info_cng <- User.Info.profile_update(user.info, new_info),
439 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
440 {:ok, _user} <- User.update_and_set_cache(changeset) do
441 json(conn, %{url: nil})
445 def update_background(%{assigns: %{user: user}} = conn, params) do
446 with {:ok, object} <- ActivityPub.upload(params, type: :background),
447 new_info <- %{"background" => object.data},
448 info_cng <- User.Info.profile_update(user.info, new_info),
449 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
450 {:ok, _user} <- User.update_and_set_cache(changeset) do
451 %{"url" => [%{"href" => href} | _]} = object.data
453 json(conn, %{url: href})
457 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
458 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
461 AccountView.render("account.json", %{
464 with_pleroma_settings: true,
465 with_chat_token: chat_token
471 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
472 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
475 |> render("short.json", %{app: app})
479 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
480 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
481 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
482 account = AccountView.render("account.json", %{user: user, for: for_user})
485 _e -> render_error(conn, :not_found, "Can't find user")
489 @mastodon_api_level "2.7.2"
491 def masto_instance(conn, _params) do
492 instance = Config.get(:instance)
496 title: Keyword.get(instance, :name),
497 description: Keyword.get(instance, :description),
498 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
499 email: Keyword.get(instance, :email),
501 streaming_api: Pleroma.Web.Endpoint.websocket_url()
503 stats: Stats.get_stats(),
504 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
506 registrations: Pleroma.Config.get([:instance, :registrations_open]),
507 # Extra (not present in Mastodon):
508 max_toot_chars: Keyword.get(instance, :limit),
509 poll_limits: Keyword.get(instance, :poll_limits)
515 def peers(conn, _params) do
516 json(conn, Stats.get_peers())
519 defp mastodonized_emoji do
520 Pleroma.Emoji.get_all()
521 |> Enum.map(fn {shortcode, relative_url, tags} ->
522 url = to_string(URI.merge(Web.base_url(), relative_url))
525 "shortcode" => shortcode,
527 "visible_in_picker" => true,
530 # Assuming that a comma is authorized in the category name
531 "category" => (tags -- ["Custom"]) |> Enum.join(",")
536 def custom_emojis(conn, _params) do
537 mastodon_emoji = mastodonized_emoji()
538 json(conn, mastodon_emoji)
541 def home_timeline(%{assigns: %{user: user}} = conn, params) do
544 |> Map.put("type", ["Create", "Announce"])
545 |> Map.put("blocking_user", user)
546 |> Map.put("muting_user", user)
547 |> Map.put("user", user)
550 [user.ap_id | user.following]
551 |> ActivityPub.fetch_activities(params)
555 |> add_link_headers(activities)
556 |> put_view(StatusView)
557 |> render("index.json", %{activities: activities, for: user, as: :activity})
560 def public_timeline(%{assigns: %{user: user}} = conn, params) do
561 local_only = params["local"] in [true, "True", "true", "1"]
565 |> Map.put("type", ["Create", "Announce"])
566 |> Map.put("local_only", local_only)
567 |> Map.put("blocking_user", user)
568 |> Map.put("muting_user", user)
569 |> Map.put("user", user)
570 |> ActivityPub.fetch_public_activities()
574 |> add_link_headers(activities, %{"local" => local_only})
575 |> put_view(StatusView)
576 |> render("index.json", %{activities: activities, for: user, as: :activity})
579 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
580 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
583 |> Map.put("tag", params["tagged"])
585 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
588 |> add_link_headers(activities)
589 |> put_view(StatusView)
590 |> render("index.json", %{
591 activities: activities,
598 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
601 |> Map.put("type", "Create")
602 |> Map.put("blocking_user", user)
603 |> Map.put("user", user)
604 |> Map.put(:visibility, "direct")
608 |> ActivityPub.fetch_activities_query(params)
609 |> Pagination.fetch_paginated(params)
612 |> add_link_headers(activities)
613 |> put_view(StatusView)
614 |> render("index.json", %{activities: activities, for: user, as: :activity})
617 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
623 |> Activity.all_by_ids_with_object()
624 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
627 |> put_view(StatusView)
628 |> render("index.json", activities: activities, for: user, as: :activity)
631 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
632 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
633 true <- Visibility.visible_for_user?(activity, user) do
635 |> put_view(StatusView)
636 |> try_render("status.json", %{activity: activity, for: user})
640 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
641 with %Activity{} = activity <- Activity.get_by_id(id),
643 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
644 "blocking_user" => user,
646 "exclude_id" => activity.id
648 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
654 activities: grouped_activities[true] || [],
658 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
663 activities: grouped_activities[false] || [],
667 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
674 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
675 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
676 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
677 true <- Visibility.visible_for_user?(activity, user) do
679 |> put_view(StatusView)
680 |> try_render("poll.json", %{object: object, for: user})
682 error when is_nil(error) or error == false ->
683 render_error(conn, :not_found, "Record not found")
687 defp get_cached_vote_or_vote(user, object, choices) do
688 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
691 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
692 case CommonAPI.vote(user, object, choices) do
693 {:error, _message} = res -> {:ignore, res}
694 res -> {:commit, res}
701 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
702 with %Object{} = object <- Object.get_by_id(id),
703 true <- object.data["type"] == "Question",
704 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
705 true <- Visibility.visible_for_user?(activity, user),
706 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
708 |> put_view(StatusView)
709 |> try_render("poll.json", %{object: object, for: user})
712 render_error(conn, :not_found, "Record not found")
715 render_error(conn, :not_found, "Record not found")
719 |> put_status(:unprocessable_entity)
720 |> json(%{error: message})
724 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
725 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
727 |> add_link_headers(scheduled_activities)
728 |> put_view(ScheduledActivityView)
729 |> render("index.json", %{scheduled_activities: scheduled_activities})
733 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
734 with %ScheduledActivity{} = scheduled_activity <-
735 ScheduledActivity.get(user, scheduled_activity_id) do
737 |> put_view(ScheduledActivityView)
738 |> render("show.json", %{scheduled_activity: scheduled_activity})
740 _ -> {:error, :not_found}
744 def update_scheduled_status(
745 %{assigns: %{user: user}} = conn,
746 %{"id" => scheduled_activity_id} = params
748 with %ScheduledActivity{} = scheduled_activity <-
749 ScheduledActivity.get(user, scheduled_activity_id),
750 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
752 |> put_view(ScheduledActivityView)
753 |> render("show.json", %{scheduled_activity: scheduled_activity})
755 nil -> {:error, :not_found}
760 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
761 with %ScheduledActivity{} = scheduled_activity <-
762 ScheduledActivity.get(user, scheduled_activity_id),
763 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
765 |> put_view(ScheduledActivityView)
766 |> render("show.json", %{scheduled_activity: scheduled_activity})
768 nil -> {:error, :not_found}
773 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
776 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
778 scheduled_at = params["scheduled_at"]
780 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
781 with {:ok, scheduled_activity} <-
782 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
784 |> put_view(ScheduledActivityView)
785 |> render("show.json", %{scheduled_activity: scheduled_activity})
788 params = Map.drop(params, ["scheduled_at"])
790 case CommonAPI.post(user, params) do
793 |> put_status(:unprocessable_entity)
794 |> json(%{error: message})
798 |> put_view(StatusView)
799 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
804 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
805 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
808 _e -> render_error(conn, :forbidden, "Can't delete this post")
812 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
813 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
814 %Activity{} = announce <- Activity.normalize(announce.data) do
816 |> put_view(StatusView)
817 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
821 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
822 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
823 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
825 |> put_view(StatusView)
826 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
830 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
831 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
832 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
834 |> put_view(StatusView)
835 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
839 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
840 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
841 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
843 |> put_view(StatusView)
844 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
848 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
849 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
851 |> put_view(StatusView)
852 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
856 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
857 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
859 |> put_view(StatusView)
860 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
864 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
865 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
866 %User{} = user <- User.get_cached_by_nickname(user.nickname),
867 true <- Visibility.visible_for_user?(activity, user),
868 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
870 |> put_view(StatusView)
871 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
875 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
876 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
877 %User{} = user <- User.get_cached_by_nickname(user.nickname),
878 true <- Visibility.visible_for_user?(activity, user),
879 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
881 |> put_view(StatusView)
882 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
886 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
887 activity = Activity.get_by_id(id)
889 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
891 |> put_view(StatusView)
892 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
896 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
897 activity = Activity.get_by_id(id)
899 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
901 |> put_view(StatusView)
902 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
906 def notifications(%{assigns: %{user: user}} = conn, params) do
907 notifications = MastodonAPI.get_notifications(user, params)
910 |> add_link_headers(notifications)
911 |> put_view(NotificationView)
912 |> render("index.json", %{notifications: notifications, for: user})
915 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
916 with {:ok, notification} <- Notification.get(user, id) do
918 |> put_view(NotificationView)
919 |> render("show.json", %{notification: notification, for: user})
923 |> put_status(:forbidden)
924 |> json(%{"error" => reason})
928 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
929 Notification.clear(user)
933 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
934 with {:ok, _notif} <- Notification.dismiss(user, id) do
939 |> put_status(:forbidden)
940 |> json(%{"error" => reason})
944 def destroy_multiple_notifications(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
945 Notification.destroy_multiple(user, ids)
949 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
951 q = from(u in User, where: u.id in ^id)
952 targets = Repo.all(q)
955 |> put_view(AccountView)
956 |> render("relationships.json", %{user: user, targets: targets})
959 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
960 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
962 def update_media(%{assigns: %{user: user}} = conn, data) do
963 with %Object{} = object <- Repo.get(Object, data["id"]),
964 true <- Object.authorize_mutation(object, user),
965 true <- is_binary(data["description"]),
966 description <- data["description"] do
967 new_data = %{object.data | "name" => description}
971 |> Object.change(%{data: new_data})
974 attachment_data = Map.put(new_data, "id", object.id)
977 |> put_view(StatusView)
978 |> render("attachment.json", %{attachment: attachment_data})
982 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
983 with {:ok, object} <-
986 actor: User.ap_id(user),
987 description: Map.get(data, "description")
989 attachment_data = Map.put(object.data, "id", object.id)
992 |> put_view(StatusView)
993 |> render("attachment.json", %{attachment: attachment_data})
997 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
998 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
999 %{} = attachment_data <- Map.put(object.data, "id", object.id),
1000 %{type: type} = rendered <-
1001 StatusView.render("attachment.json", %{attachment: attachment_data}) do
1002 # Reject if not an image
1003 if type == "image" do
1005 # Save to the user's info
1006 info_changeset = User.Info.mascot_update(user.info, rendered)
1010 |> Changeset.change()
1011 |> Changeset.put_embed(:info, info_changeset)
1013 {:ok, _user} = User.update_and_set_cache(user_changeset)
1018 render_error(conn, :unsupported_media_type, "mascots can only be images")
1023 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
1024 mascot = User.get_mascot(user)
1030 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1031 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
1032 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
1033 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
1034 q = from(u in User, where: u.ap_id in ^likes)
1038 |> Enum.filter(&(not User.blocks?(user, &1)))
1041 |> put_view(AccountView)
1042 |> render("accounts.json", %{for: user, users: users, as: :user})
1044 {:visible, false} -> {:error, :not_found}
1049 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1050 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
1051 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
1052 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
1053 q = from(u in User, where: u.ap_id in ^announces)
1057 |> Enum.filter(&(not User.blocks?(user, &1)))
1060 |> put_view(AccountView)
1061 |> render("accounts.json", %{for: user, users: users, as: :user})
1063 {:visible, false} -> {:error, :not_found}
1068 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
1069 local_only = params["local"] in [true, "True", "true", "1"]
1072 [params["tag"], params["any"]]
1075 |> Enum.filter(& &1)
1076 |> Enum.map(&String.downcase(&1))
1081 |> Enum.map(&String.downcase(&1))
1086 |> Enum.map(&String.downcase(&1))
1090 |> Map.put("type", "Create")
1091 |> Map.put("local_only", local_only)
1092 |> Map.put("blocking_user", user)
1093 |> Map.put("muting_user", user)
1094 |> Map.put("user", user)
1095 |> Map.put("tag", tags)
1096 |> Map.put("tag_all", tag_all)
1097 |> Map.put("tag_reject", tag_reject)
1098 |> ActivityPub.fetch_public_activities()
1102 |> add_link_headers(activities, %{"local" => local_only})
1103 |> put_view(StatusView)
1104 |> render("index.json", %{activities: activities, for: user, as: :activity})
1107 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1108 with %User{} = user <- User.get_cached_by_id(id),
1109 followers <- MastodonAPI.get_followers(user, params) do
1112 for_user && user.id == for_user.id -> followers
1113 user.info.hide_followers -> []
1118 |> add_link_headers(followers)
1119 |> put_view(AccountView)
1120 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1124 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1125 with %User{} = user <- User.get_cached_by_id(id),
1126 followers <- MastodonAPI.get_friends(user, params) do
1129 for_user && user.id == for_user.id -> followers
1130 user.info.hide_follows -> []
1135 |> add_link_headers(followers)
1136 |> put_view(AccountView)
1137 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1141 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
1142 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
1144 |> put_view(AccountView)
1145 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1149 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1150 with %User{} = follower <- User.get_cached_by_id(id),
1151 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1153 |> put_view(AccountView)
1154 |> render("relationship.json", %{user: followed, target: follower})
1156 {:error, message} ->
1158 |> put_status(:forbidden)
1159 |> json(%{error: message})
1163 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1164 with %User{} = follower <- User.get_cached_by_id(id),
1165 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1167 |> put_view(AccountView)
1168 |> render("relationship.json", %{user: followed, target: follower})
1170 {:error, message} ->
1172 |> put_status(:forbidden)
1173 |> json(%{error: message})
1177 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1178 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1179 {_, true} <- {:followed, follower.id != followed.id},
1180 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1182 |> put_view(AccountView)
1183 |> render("relationship.json", %{user: follower, target: followed})
1186 {:error, :not_found}
1188 {:error, message} ->
1190 |> put_status(:forbidden)
1191 |> json(%{error: message})
1195 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1196 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1197 {_, true} <- {:followed, follower.id != followed.id},
1198 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1200 |> put_view(AccountView)
1201 |> render("account.json", %{user: followed, for: follower})
1204 {:error, :not_found}
1206 {:error, message} ->
1208 |> put_status(:forbidden)
1209 |> json(%{error: message})
1213 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1214 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1215 {_, true} <- {:followed, follower.id != followed.id},
1216 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1218 |> put_view(AccountView)
1219 |> render("relationship.json", %{user: follower, target: followed})
1222 {:error, :not_found}
1229 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1231 if Map.has_key?(params, "notifications"),
1232 do: params["notifications"] in [true, "True", "true", "1"],
1235 with %User{} = muted <- User.get_cached_by_id(id),
1236 {:ok, muter} <- User.mute(muter, muted, notifications) do
1238 |> put_view(AccountView)
1239 |> render("relationship.json", %{user: muter, target: muted})
1241 {:error, message} ->
1243 |> put_status(:forbidden)
1244 |> json(%{error: message})
1248 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1249 with %User{} = muted <- User.get_cached_by_id(id),
1250 {:ok, muter} <- User.unmute(muter, muted) do
1252 |> put_view(AccountView)
1253 |> render("relationship.json", %{user: muter, target: muted})
1255 {:error, message} ->
1257 |> put_status(:forbidden)
1258 |> json(%{error: message})
1262 def mutes(%{assigns: %{user: user}} = conn, _) do
1263 with muted_accounts <- User.muted_users(user) do
1264 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1269 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1270 with %User{} = blocked <- User.get_cached_by_id(id),
1271 {:ok, blocker} <- User.block(blocker, blocked),
1272 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1274 |> put_view(AccountView)
1275 |> render("relationship.json", %{user: blocker, target: blocked})
1277 {:error, message} ->
1279 |> put_status(:forbidden)
1280 |> json(%{error: message})
1284 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1285 with %User{} = blocked <- User.get_cached_by_id(id),
1286 {:ok, blocker} <- User.unblock(blocker, blocked),
1287 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1289 |> put_view(AccountView)
1290 |> render("relationship.json", %{user: blocker, target: blocked})
1292 {:error, message} ->
1294 |> put_status(:forbidden)
1295 |> json(%{error: message})
1299 def blocks(%{assigns: %{user: user}} = conn, _) do
1300 with blocked_accounts <- User.blocked_users(user) do
1301 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1306 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1307 json(conn, info.domain_blocks || [])
1310 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1311 User.block_domain(blocker, domain)
1315 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1316 User.unblock_domain(blocker, domain)
1320 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1321 with %User{} = subscription_target <- User.get_cached_by_id(id),
1322 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1324 |> put_view(AccountView)
1325 |> render("relationship.json", %{user: user, target: subscription_target})
1327 {:error, message} ->
1329 |> put_status(:forbidden)
1330 |> json(%{error: message})
1334 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1335 with %User{} = subscription_target <- User.get_cached_by_id(id),
1336 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1338 |> put_view(AccountView)
1339 |> render("relationship.json", %{user: user, target: subscription_target})
1341 {:error, message} ->
1343 |> put_status(:forbidden)
1344 |> json(%{error: message})
1348 def favourites(%{assigns: %{user: user}} = conn, params) do
1351 |> Map.put("type", "Create")
1352 |> Map.put("favorited_by", user.ap_id)
1353 |> Map.put("blocking_user", user)
1356 ActivityPub.fetch_activities([], params)
1360 |> add_link_headers(activities)
1361 |> put_view(StatusView)
1362 |> render("index.json", %{activities: activities, for: user, as: :activity})
1365 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1366 with %User{} = user <- User.get_by_id(id),
1367 false <- user.info.hide_favorites do
1370 |> Map.put("type", "Create")
1371 |> Map.put("favorited_by", user.ap_id)
1372 |> Map.put("blocking_user", for_user)
1376 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1378 [Pleroma.Constants.as_public()]
1383 |> ActivityPub.fetch_activities(params)
1387 |> add_link_headers(activities)
1388 |> put_view(StatusView)
1389 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1391 nil -> {:error, :not_found}
1392 true -> render_error(conn, :forbidden, "Can't get favorites")
1396 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1397 user = User.get_cached_by_id(user.id)
1400 Bookmark.for_user_query(user.id)
1401 |> Pagination.fetch_paginated(params)
1405 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1408 |> add_link_headers(bookmarks)
1409 |> put_view(StatusView)
1410 |> render("index.json", %{activities: activities, for: user, as: :activity})
1413 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1414 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1415 res = ListView.render("lists.json", lists: lists)
1419 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1420 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1423 |> Map.put("type", "Create")
1424 |> Map.put("blocking_user", user)
1425 |> Map.put("user", user)
1426 |> Map.put("muting_user", user)
1428 # we must filter the following list for the user to avoid leaking statuses the user
1429 # does not actually have permission to see (for more info, peruse security issue #270).
1432 |> Enum.filter(fn x -> x in user.following end)
1433 |> ActivityPub.fetch_activities_bounded(following, params)
1437 |> put_view(StatusView)
1438 |> render("index.json", %{activities: activities, for: user, as: :activity})
1440 _e -> render_error(conn, :forbidden, "Error.")
1444 def index(%{assigns: %{user: user}} = conn, _params) do
1445 token = get_session(conn, :oauth_token)
1448 mastodon_emoji = mastodonized_emoji()
1450 limit = Config.get([:instance, :limit])
1453 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1458 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1459 access_token: token,
1461 domain: Pleroma.Web.Endpoint.host(),
1464 unfollow_modal: false,
1467 auto_play_gif: false,
1468 display_sensitive_media: false,
1469 reduce_motion: false,
1470 max_toot_chars: limit,
1471 mascot: User.get_mascot(user)["url"]
1473 poll_limits: Config.get([:instance, :poll_limits]),
1475 delete_others_notice: present?(user.info.is_moderator),
1476 admin: present?(user.info.is_admin)
1480 default_privacy: user.info.default_scope,
1481 default_sensitive: false,
1482 allow_content_types: Config.get([:instance, :allowed_post_formats])
1484 media_attachments: %{
1485 accept_content_types: [
1501 user.info.settings ||
1531 push_subscription: nil,
1533 custom_emojis: mastodon_emoji,
1539 |> put_layout(false)
1540 |> put_view(MastodonView)
1541 |> render("index.html", %{initial_state: initial_state})
1544 |> put_session(:return_to, conn.request_path)
1545 |> redirect(to: "/web/login")
1549 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1550 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1552 with changeset <- Changeset.change(user),
1553 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1554 {:ok, _user} <- User.update_and_set_cache(changeset) do
1559 |> put_status(:internal_server_error)
1560 |> json(%{error: inspect(e)})
1564 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1565 redirect(conn, to: local_mastodon_root_path(conn))
1568 @doc "Local Mastodon FE login init action"
1569 def login(conn, %{"code" => auth_token}) do
1570 with {:ok, app} <- get_or_make_app(),
1571 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1572 {:ok, token} <- Token.exchange_token(app, auth) do
1574 |> put_session(:oauth_token, token.token)
1575 |> redirect(to: local_mastodon_root_path(conn))
1579 @doc "Local Mastodon FE callback action"
1580 def login(conn, _) do
1581 with {:ok, app} <- get_or_make_app() do
1586 response_type: "code",
1587 client_id: app.client_id,
1589 scope: Enum.join(app.scopes, " ")
1592 redirect(conn, to: path)
1596 defp local_mastodon_root_path(conn) do
1597 case get_session(conn, :return_to) do
1599 mastodon_api_path(conn, :index, ["getting-started"])
1602 delete_session(conn, :return_to)
1607 defp get_or_make_app do
1608 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1609 scopes = ["read", "write", "follow", "push"]
1611 with %App{} = app <- Repo.get_by(App, find_attrs) do
1613 if app.scopes == scopes do
1617 |> Changeset.change(%{scopes: scopes})
1625 App.register_changeset(
1627 Map.put(find_attrs, :scopes, scopes)
1634 def logout(conn, _) do
1637 |> redirect(to: "/")
1640 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1641 Logger.debug("Unimplemented, returning unmodified relationship")
1643 with %User{} = target <- User.get_cached_by_id(id) do
1645 |> put_view(AccountView)
1646 |> render("relationship.json", %{user: user, target: target})
1650 def empty_array(conn, _) do
1651 Logger.debug("Unimplemented, returning an empty array")
1655 def empty_object(conn, _) do
1656 Logger.debug("Unimplemented, returning an empty object")
1660 def endorsements(conn, params), do: empty_array(conn, params)
1662 def get_filters(%{assigns: %{user: user}} = conn, _) do
1663 filters = Filter.get_filters(user)
1664 res = FilterView.render("filters.json", filters: filters)
1669 %{assigns: %{user: user}} = conn,
1670 %{"phrase" => phrase, "context" => context} = params
1676 hide: Map.get(params, "irreversible", false),
1677 whole_word: Map.get(params, "boolean", true)
1681 {:ok, response} = Filter.create(query)
1682 res = FilterView.render("filter.json", filter: response)
1686 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1687 filter = Filter.get(filter_id, user)
1688 res = FilterView.render("filter.json", filter: filter)
1693 %{assigns: %{user: user}} = conn,
1694 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1698 filter_id: filter_id,
1701 hide: Map.get(params, "irreversible", nil),
1702 whole_word: Map.get(params, "boolean", true)
1706 {:ok, response} = Filter.update(query)
1707 res = FilterView.render("filter.json", filter: response)
1711 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1714 filter_id: filter_id
1717 {:ok, _} = Filter.delete(query)
1721 def suggestions(%{assigns: %{user: user}} = conn, _) do
1722 suggestions = Config.get(:suggestions)
1724 if Keyword.get(suggestions, :enabled, false) do
1725 api = Keyword.get(suggestions, :third_party_engine, "")
1726 timeout = Keyword.get(suggestions, :timeout, 5000)
1727 limit = Keyword.get(suggestions, :limit, 23)
1729 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1731 user = user.nickname
1735 |> String.replace("{{host}}", host)
1736 |> String.replace("{{user}}", user)
1738 with {:ok, %{status: 200, body: body}} <-
1739 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1740 {:ok, data} <- Jason.decode(body) do
1743 |> Enum.slice(0, limit)
1746 |> Map.put("id", fetch_suggestion_id(x))
1747 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1748 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1754 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1761 defp fetch_suggestion_id(attrs) do
1762 case User.get_or_fetch(attrs["acct"]) do
1763 {:ok, %User{id: id}} -> id
1768 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1769 with %Activity{} = activity <- Activity.get_by_id(status_id),
1770 true <- Visibility.visible_for_user?(activity, user) do
1774 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1784 def create_report(%{assigns: %{user: user}} = conn, params) do
1785 case CommonAPI.report(user, params) do
1788 |> put_view(ReportView)
1789 |> try_render("report.json", %{activity: activity})
1793 |> put_status(:bad_request)
1794 |> json(%{error: err})
1798 def account_register(
1799 %{assigns: %{app: app}} = conn,
1800 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1808 "captcha_answer_data",
1812 |> Map.put("nickname", nickname)
1813 |> Map.put("fullname", params["fullname"] || nickname)
1814 |> Map.put("bio", params["bio"] || "")
1815 |> Map.put("confirm", params["password"])
1817 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1818 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1820 token_type: "Bearer",
1821 access_token: token.token,
1823 created_at: Token.Utils.format_created_at(token)
1828 |> put_status(:bad_request)
1833 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1834 render_error(conn, :bad_request, "Missing parameters")
1837 def account_register(conn, _) do
1838 render_error(conn, :forbidden, "Invalid credentials")
1841 def conversations(%{assigns: %{user: user}} = conn, params) do
1842 participations = Participation.for_user_with_last_activity_id(user, params)
1845 Enum.map(participations, fn participation ->
1846 ConversationView.render("participation.json", %{participation: participation, for: user})
1850 |> add_link_headers(participations)
1851 |> json(conversations)
1854 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1855 with %Participation{} = participation <-
1856 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1857 {:ok, participation} <- Participation.mark_as_read(participation) do
1858 participation_view =
1859 ConversationView.render("participation.json", %{participation: participation, for: user})
1862 |> json(participation_view)
1866 def password_reset(conn, params) do
1867 nickname_or_email = params["email"] || params["nickname"]
1869 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1871 |> put_status(:no_content)
1874 {:error, "unknown user"} ->
1875 send_resp(conn, :not_found, "")
1878 send_resp(conn, :bad_request, "")
1882 def account_confirmation_resend(conn, params) do
1883 nickname_or_email = params["email"] || params["nickname"]
1885 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1886 {:ok, _} <- User.try_send_confirmation_email(user) do
1888 |> json_response(:no_content, "")
1892 def try_render(conn, target, params)
1893 when is_binary(target) do
1894 case render(conn, target, params) do
1895 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1900 def try_render(conn, _, _) do
1901 render_error(conn, :not_implemented, "Can't display this activity")
1904 defp present?(nil), do: false
1905 defp present?(false), do: false
1906 defp present?(_), do: true