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))
339 :skip_thread_containment
341 |> Enum.reduce(%{}, fn key, acc ->
342 add_if_present(acc, params, to_string(key), key, fn value ->
343 {:ok, ControllerHelper.truthy_param?(value)}
346 |> add_if_present(params, "default_scope", :default_scope)
347 |> add_if_present(params, "fields", :fields, fn fields ->
348 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
352 |> add_if_present(params, "fields", :raw_fields)
353 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
354 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
356 |> add_if_present(params, "header", :banner, fn value ->
357 with %Plug.Upload{} <- value,
358 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
364 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
365 with %Plug.Upload{} <- value,
366 {:ok, object} <- ActivityPub.upload(value, type: :background) do
372 |> Map.put(:emoji, user_info_emojis)
374 info_cng = User.Info.profile_update(user.info, info_params)
376 with changeset <- User.update_changeset(user, user_params),
377 changeset <- Changeset.put_embed(changeset, :info, info_cng),
378 {:ok, user} <- User.update_and_set_cache(changeset) do
379 if original_user != user do
380 CommonAPI.update(user)
385 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
388 _e -> render_error(conn, :forbidden, "Invalid request")
392 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
393 change = Changeset.change(user, %{avatar: nil})
394 {:ok, user} = User.update_and_set_cache(change)
395 CommonAPI.update(user)
397 json(conn, %{url: nil})
400 def update_avatar(%{assigns: %{user: user}} = conn, params) do
401 {:ok, object} = ActivityPub.upload(params, type: :avatar)
402 change = Changeset.change(user, %{avatar: object.data})
403 {:ok, user} = User.update_and_set_cache(change)
404 CommonAPI.update(user)
405 %{"url" => [%{"href" => href} | _]} = object.data
407 json(conn, %{url: href})
410 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
411 with new_info <- %{"banner" => %{}},
412 info_cng <- User.Info.profile_update(user.info, new_info),
413 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
414 {:ok, user} <- User.update_and_set_cache(changeset) do
415 CommonAPI.update(user)
417 json(conn, %{url: nil})
421 def update_banner(%{assigns: %{user: user}} = conn, params) do
422 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
423 new_info <- %{"banner" => object.data},
424 info_cng <- User.Info.profile_update(user.info, new_info),
425 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
426 {:ok, user} <- User.update_and_set_cache(changeset) do
427 CommonAPI.update(user)
428 %{"url" => [%{"href" => href} | _]} = object.data
430 json(conn, %{url: href})
434 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
435 with new_info <- %{"background" => %{}},
436 info_cng <- User.Info.profile_update(user.info, new_info),
437 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
438 {:ok, _user} <- User.update_and_set_cache(changeset) do
439 json(conn, %{url: nil})
443 def update_background(%{assigns: %{user: user}} = conn, params) do
444 with {:ok, object} <- ActivityPub.upload(params, type: :background),
445 new_info <- %{"background" => object.data},
446 info_cng <- User.Info.profile_update(user.info, new_info),
447 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
448 {:ok, _user} <- User.update_and_set_cache(changeset) do
449 %{"url" => [%{"href" => href} | _]} = object.data
451 json(conn, %{url: href})
455 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
456 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
459 AccountView.render("account.json", %{
462 with_pleroma_settings: true,
463 with_chat_token: chat_token
469 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
470 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
473 |> render("short.json", %{app: app})
477 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
478 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
479 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
480 account = AccountView.render("account.json", %{user: user, for: for_user})
483 _e -> render_error(conn, :not_found, "Can't find user")
487 @mastodon_api_level "2.7.2"
489 def masto_instance(conn, _params) do
490 instance = Config.get(:instance)
494 title: Keyword.get(instance, :name),
495 description: Keyword.get(instance, :description),
496 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
497 email: Keyword.get(instance, :email),
499 streaming_api: Pleroma.Web.Endpoint.websocket_url()
501 stats: Stats.get_stats(),
502 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
504 registrations: Pleroma.Config.get([:instance, :registrations_open]),
505 # Extra (not present in Mastodon):
506 max_toot_chars: Keyword.get(instance, :limit),
507 poll_limits: Keyword.get(instance, :poll_limits)
513 def peers(conn, _params) do
514 json(conn, Stats.get_peers())
517 defp mastodonized_emoji do
518 Pleroma.Emoji.get_all()
519 |> Enum.map(fn {shortcode, relative_url, tags} ->
520 url = to_string(URI.merge(Web.base_url(), relative_url))
523 "shortcode" => shortcode,
525 "visible_in_picker" => true,
528 # Assuming that a comma is authorized in the category name
529 "category" => (tags -- ["Custom"]) |> Enum.join(",")
534 def custom_emojis(conn, _params) do
535 mastodon_emoji = mastodonized_emoji()
536 json(conn, mastodon_emoji)
539 def home_timeline(%{assigns: %{user: user}} = conn, params) do
542 |> Map.put("type", ["Create", "Announce"])
543 |> Map.put("blocking_user", user)
544 |> Map.put("muting_user", user)
545 |> Map.put("user", user)
548 [user.ap_id | user.following]
549 |> ActivityPub.fetch_activities(params)
553 |> add_link_headers(activities)
554 |> put_view(StatusView)
555 |> render("index.json", %{activities: activities, for: user, as: :activity})
558 def public_timeline(%{assigns: %{user: user}} = conn, params) do
559 local_only = params["local"] in [true, "True", "true", "1"]
563 |> Map.put("type", ["Create", "Announce"])
564 |> Map.put("local_only", local_only)
565 |> Map.put("blocking_user", user)
566 |> Map.put("muting_user", user)
567 |> Map.put("user", user)
568 |> ActivityPub.fetch_public_activities()
572 |> add_link_headers(activities, %{"local" => local_only})
573 |> put_view(StatusView)
574 |> render("index.json", %{activities: activities, for: user, as: :activity})
577 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
578 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
581 |> Map.put("tag", params["tagged"])
583 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
586 |> add_link_headers(activities)
587 |> put_view(StatusView)
588 |> render("index.json", %{
589 activities: activities,
596 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
599 |> Map.put("type", "Create")
600 |> Map.put("blocking_user", user)
601 |> Map.put("user", user)
602 |> Map.put(:visibility, "direct")
606 |> ActivityPub.fetch_activities_query(params)
607 |> Pagination.fetch_paginated(params)
610 |> add_link_headers(activities)
611 |> put_view(StatusView)
612 |> render("index.json", %{activities: activities, for: user, as: :activity})
615 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
621 |> Activity.all_by_ids_with_object()
622 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
625 |> put_view(StatusView)
626 |> render("index.json", activities: activities, for: user, as: :activity)
629 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
630 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
631 true <- Visibility.visible_for_user?(activity, user) do
633 |> put_view(StatusView)
634 |> try_render("status.json", %{activity: activity, for: user})
638 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
639 with %Activity{} = activity <- Activity.get_by_id(id),
641 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
642 "blocking_user" => user,
644 "exclude_id" => activity.id
646 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
652 activities: grouped_activities[true] || [],
656 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
661 activities: grouped_activities[false] || [],
665 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
672 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
673 with %Object{} = object <- Object.get_by_id(id),
674 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
675 true <- Visibility.visible_for_user?(activity, user) do
677 |> put_view(StatusView)
678 |> try_render("poll.json", %{object: object, for: user})
680 error when is_nil(error) or error == false ->
681 render_error(conn, :not_found, "Record not found")
685 defp get_cached_vote_or_vote(user, object, choices) do
686 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
689 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
690 case CommonAPI.vote(user, object, choices) do
691 {:error, _message} = res -> {:ignore, res}
692 res -> {:commit, res}
699 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
700 with %Object{} = object <- Object.get_by_id(id),
701 true <- object.data["type"] == "Question",
702 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
703 true <- Visibility.visible_for_user?(activity, user),
704 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
706 |> put_view(StatusView)
707 |> try_render("poll.json", %{object: object, for: user})
710 render_error(conn, :not_found, "Record not found")
713 render_error(conn, :not_found, "Record not found")
717 |> put_status(:unprocessable_entity)
718 |> json(%{error: message})
722 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
723 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
725 |> add_link_headers(scheduled_activities)
726 |> put_view(ScheduledActivityView)
727 |> render("index.json", %{scheduled_activities: scheduled_activities})
731 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
732 with %ScheduledActivity{} = scheduled_activity <-
733 ScheduledActivity.get(user, scheduled_activity_id) do
735 |> put_view(ScheduledActivityView)
736 |> render("show.json", %{scheduled_activity: scheduled_activity})
738 _ -> {:error, :not_found}
742 def update_scheduled_status(
743 %{assigns: %{user: user}} = conn,
744 %{"id" => scheduled_activity_id} = params
746 with %ScheduledActivity{} = scheduled_activity <-
747 ScheduledActivity.get(user, scheduled_activity_id),
748 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
750 |> put_view(ScheduledActivityView)
751 |> render("show.json", %{scheduled_activity: scheduled_activity})
753 nil -> {:error, :not_found}
758 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
759 with %ScheduledActivity{} = scheduled_activity <-
760 ScheduledActivity.get(user, scheduled_activity_id),
761 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
763 |> put_view(ScheduledActivityView)
764 |> render("show.json", %{scheduled_activity: scheduled_activity})
766 nil -> {:error, :not_found}
771 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
774 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
776 scheduled_at = params["scheduled_at"]
778 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
779 with {:ok, scheduled_activity} <-
780 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
782 |> put_view(ScheduledActivityView)
783 |> render("show.json", %{scheduled_activity: scheduled_activity})
786 params = Map.drop(params, ["scheduled_at"])
788 case CommonAPI.post(user, params) do
791 |> put_status(:unprocessable_entity)
792 |> json(%{error: message})
796 |> put_view(StatusView)
797 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
802 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
803 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
806 _e -> render_error(conn, :forbidden, "Can't delete this post")
810 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
811 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
812 %Activity{} = announce <- Activity.normalize(announce.data) do
814 |> put_view(StatusView)
815 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
819 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
820 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
821 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
823 |> put_view(StatusView)
824 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
828 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
829 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
830 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
832 |> put_view(StatusView)
833 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
837 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
838 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
839 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
841 |> put_view(StatusView)
842 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
846 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
847 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
849 |> put_view(StatusView)
850 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
854 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
855 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
857 |> put_view(StatusView)
858 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
862 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
863 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
864 %User{} = user <- User.get_cached_by_nickname(user.nickname),
865 true <- Visibility.visible_for_user?(activity, user),
866 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
868 |> put_view(StatusView)
869 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
873 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
874 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
875 %User{} = user <- User.get_cached_by_nickname(user.nickname),
876 true <- Visibility.visible_for_user?(activity, user),
877 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
879 |> put_view(StatusView)
880 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
884 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
885 activity = Activity.get_by_id(id)
887 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
889 |> put_view(StatusView)
890 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
894 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
895 activity = Activity.get_by_id(id)
897 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
899 |> put_view(StatusView)
900 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
904 def notifications(%{assigns: %{user: user}} = conn, params) do
905 notifications = MastodonAPI.get_notifications(user, params)
908 |> add_link_headers(notifications)
909 |> put_view(NotificationView)
910 |> render("index.json", %{notifications: notifications, for: user})
913 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
914 with {:ok, notification} <- Notification.get(user, id) do
916 |> put_view(NotificationView)
917 |> render("show.json", %{notification: notification, for: user})
921 |> put_status(:forbidden)
922 |> json(%{"error" => reason})
926 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
927 Notification.clear(user)
931 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
932 with {:ok, _notif} <- Notification.dismiss(user, id) do
937 |> put_status(:forbidden)
938 |> json(%{"error" => reason})
942 def destroy_multiple_notifications(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
943 Notification.destroy_multiple(user, ids)
947 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
949 q = from(u in User, where: u.id in ^id)
950 targets = Repo.all(q)
953 |> put_view(AccountView)
954 |> render("relationships.json", %{user: user, targets: targets})
957 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
958 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
960 def update_media(%{assigns: %{user: user}} = conn, data) do
961 with %Object{} = object <- Repo.get(Object, data["id"]),
962 true <- Object.authorize_mutation(object, user),
963 true <- is_binary(data["description"]),
964 description <- data["description"] do
965 new_data = %{object.data | "name" => description}
969 |> Object.change(%{data: new_data})
972 attachment_data = Map.put(new_data, "id", object.id)
975 |> put_view(StatusView)
976 |> render("attachment.json", %{attachment: attachment_data})
980 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
981 with {:ok, object} <-
984 actor: User.ap_id(user),
985 description: Map.get(data, "description")
987 attachment_data = Map.put(object.data, "id", object.id)
990 |> put_view(StatusView)
991 |> render("attachment.json", %{attachment: attachment_data})
995 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
996 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
997 %{} = attachment_data <- Map.put(object.data, "id", object.id),
998 %{type: type} = rendered <-
999 StatusView.render("attachment.json", %{attachment: attachment_data}) do
1000 # Reject if not an image
1001 if type == "image" do
1003 # Save to the user's info
1004 info_changeset = User.Info.mascot_update(user.info, rendered)
1008 |> Changeset.change()
1009 |> Changeset.put_embed(:info, info_changeset)
1011 {:ok, _user} = User.update_and_set_cache(user_changeset)
1016 render_error(conn, :unsupported_media_type, "mascots can only be images")
1021 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
1022 mascot = User.get_mascot(user)
1028 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1029 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
1030 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
1031 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
1032 q = from(u in User, where: u.ap_id in ^likes)
1036 |> Enum.filter(&(not User.blocks?(user, &1)))
1039 |> put_view(AccountView)
1040 |> render("accounts.json", %{for: user, users: users, as: :user})
1042 {:visible, false} -> {:error, :not_found}
1047 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1048 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
1049 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
1050 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
1051 q = from(u in User, where: u.ap_id in ^announces)
1055 |> Enum.filter(&(not User.blocks?(user, &1)))
1058 |> put_view(AccountView)
1059 |> render("accounts.json", %{for: user, users: users, as: :user})
1061 {:visible, false} -> {:error, :not_found}
1066 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
1067 local_only = params["local"] in [true, "True", "true", "1"]
1070 [params["tag"], params["any"]]
1073 |> Enum.filter(& &1)
1074 |> Enum.map(&String.downcase(&1))
1079 |> Enum.map(&String.downcase(&1))
1084 |> Enum.map(&String.downcase(&1))
1088 |> Map.put("type", "Create")
1089 |> Map.put("local_only", local_only)
1090 |> Map.put("blocking_user", user)
1091 |> Map.put("muting_user", user)
1092 |> Map.put("user", user)
1093 |> Map.put("tag", tags)
1094 |> Map.put("tag_all", tag_all)
1095 |> Map.put("tag_reject", tag_reject)
1096 |> ActivityPub.fetch_public_activities()
1100 |> add_link_headers(activities, %{"local" => local_only})
1101 |> put_view(StatusView)
1102 |> render("index.json", %{activities: activities, for: user, as: :activity})
1105 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1106 with %User{} = user <- User.get_cached_by_id(id),
1107 followers <- MastodonAPI.get_followers(user, params) do
1110 for_user && user.id == for_user.id -> followers
1111 user.info.hide_followers -> []
1116 |> add_link_headers(followers)
1117 |> put_view(AccountView)
1118 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1122 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1123 with %User{} = user <- User.get_cached_by_id(id),
1124 followers <- MastodonAPI.get_friends(user, params) do
1127 for_user && user.id == for_user.id -> followers
1128 user.info.hide_follows -> []
1133 |> add_link_headers(followers)
1134 |> put_view(AccountView)
1135 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1139 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
1140 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
1142 |> put_view(AccountView)
1143 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1147 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1148 with %User{} = follower <- User.get_cached_by_id(id),
1149 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1151 |> put_view(AccountView)
1152 |> render("relationship.json", %{user: followed, target: follower})
1154 {:error, message} ->
1156 |> put_status(:forbidden)
1157 |> json(%{error: message})
1161 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1162 with %User{} = follower <- User.get_cached_by_id(id),
1163 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1165 |> put_view(AccountView)
1166 |> render("relationship.json", %{user: followed, target: follower})
1168 {:error, message} ->
1170 |> put_status(:forbidden)
1171 |> json(%{error: message})
1175 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1176 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1177 {_, true} <- {:followed, follower.id != followed.id},
1178 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1180 |> put_view(AccountView)
1181 |> render("relationship.json", %{user: follower, target: followed})
1184 {:error, :not_found}
1186 {:error, message} ->
1188 |> put_status(:forbidden)
1189 |> json(%{error: message})
1193 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1194 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1195 {_, true} <- {:followed, follower.id != followed.id},
1196 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1198 |> put_view(AccountView)
1199 |> render("account.json", %{user: followed, for: follower})
1202 {:error, :not_found}
1204 {:error, message} ->
1206 |> put_status(:forbidden)
1207 |> json(%{error: message})
1211 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1212 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1213 {_, true} <- {:followed, follower.id != followed.id},
1214 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1216 |> put_view(AccountView)
1217 |> render("relationship.json", %{user: follower, target: followed})
1220 {:error, :not_found}
1227 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1229 if Map.has_key?(params, "notifications"),
1230 do: params["notifications"] in [true, "True", "true", "1"],
1233 with %User{} = muted <- User.get_cached_by_id(id),
1234 {:ok, muter} <- User.mute(muter, muted, notifications) do
1236 |> put_view(AccountView)
1237 |> render("relationship.json", %{user: muter, target: muted})
1239 {:error, message} ->
1241 |> put_status(:forbidden)
1242 |> json(%{error: message})
1246 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1247 with %User{} = muted <- User.get_cached_by_id(id),
1248 {:ok, muter} <- User.unmute(muter, muted) do
1250 |> put_view(AccountView)
1251 |> render("relationship.json", %{user: muter, target: muted})
1253 {:error, message} ->
1255 |> put_status(:forbidden)
1256 |> json(%{error: message})
1260 def mutes(%{assigns: %{user: user}} = conn, _) do
1261 with muted_accounts <- User.muted_users(user) do
1262 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1267 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1268 with %User{} = blocked <- User.get_cached_by_id(id),
1269 {:ok, blocker} <- User.block(blocker, blocked),
1270 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1272 |> put_view(AccountView)
1273 |> render("relationship.json", %{user: blocker, target: blocked})
1275 {:error, message} ->
1277 |> put_status(:forbidden)
1278 |> json(%{error: message})
1282 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1283 with %User{} = blocked <- User.get_cached_by_id(id),
1284 {:ok, blocker} <- User.unblock(blocker, blocked),
1285 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1287 |> put_view(AccountView)
1288 |> render("relationship.json", %{user: blocker, target: blocked})
1290 {:error, message} ->
1292 |> put_status(:forbidden)
1293 |> json(%{error: message})
1297 def blocks(%{assigns: %{user: user}} = conn, _) do
1298 with blocked_accounts <- User.blocked_users(user) do
1299 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1304 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1305 json(conn, info.domain_blocks || [])
1308 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1309 User.block_domain(blocker, domain)
1313 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1314 User.unblock_domain(blocker, domain)
1318 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1319 with %User{} = subscription_target <- User.get_cached_by_id(id),
1320 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1322 |> put_view(AccountView)
1323 |> render("relationship.json", %{user: user, target: subscription_target})
1325 {:error, message} ->
1327 |> put_status(:forbidden)
1328 |> json(%{error: message})
1332 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1333 with %User{} = subscription_target <- User.get_cached_by_id(id),
1334 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1336 |> put_view(AccountView)
1337 |> render("relationship.json", %{user: user, target: subscription_target})
1339 {:error, message} ->
1341 |> put_status(:forbidden)
1342 |> json(%{error: message})
1346 def favourites(%{assigns: %{user: user}} = conn, params) do
1349 |> Map.put("type", "Create")
1350 |> Map.put("favorited_by", user.ap_id)
1351 |> Map.put("blocking_user", user)
1354 ActivityPub.fetch_activities([], params)
1358 |> add_link_headers(activities)
1359 |> put_view(StatusView)
1360 |> render("index.json", %{activities: activities, for: user, as: :activity})
1363 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1364 with %User{} = user <- User.get_by_id(id),
1365 false <- user.info.hide_favorites do
1368 |> Map.put("type", "Create")
1369 |> Map.put("favorited_by", user.ap_id)
1370 |> Map.put("blocking_user", for_user)
1374 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1376 [Pleroma.Constants.as_public()]
1381 |> ActivityPub.fetch_activities(params)
1385 |> add_link_headers(activities)
1386 |> put_view(StatusView)
1387 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1389 nil -> {:error, :not_found}
1390 true -> render_error(conn, :forbidden, "Can't get favorites")
1394 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1395 user = User.get_cached_by_id(user.id)
1398 Bookmark.for_user_query(user.id)
1399 |> Pagination.fetch_paginated(params)
1403 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1406 |> add_link_headers(bookmarks)
1407 |> put_view(StatusView)
1408 |> render("index.json", %{activities: activities, for: user, as: :activity})
1411 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1412 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1413 res = ListView.render("lists.json", lists: lists)
1417 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1418 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1421 |> Map.put("type", "Create")
1422 |> Map.put("blocking_user", user)
1423 |> Map.put("user", user)
1424 |> Map.put("muting_user", user)
1426 # we must filter the following list for the user to avoid leaking statuses the user
1427 # does not actually have permission to see (for more info, peruse security issue #270).
1430 |> Enum.filter(fn x -> x in user.following end)
1431 |> ActivityPub.fetch_activities_bounded(following, params)
1435 |> put_view(StatusView)
1436 |> render("index.json", %{activities: activities, for: user, as: :activity})
1438 _e -> render_error(conn, :forbidden, "Error.")
1442 def index(%{assigns: %{user: user}} = conn, _params) do
1443 token = get_session(conn, :oauth_token)
1446 mastodon_emoji = mastodonized_emoji()
1448 limit = Config.get([:instance, :limit])
1451 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1456 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1457 access_token: token,
1459 domain: Pleroma.Web.Endpoint.host(),
1462 unfollow_modal: false,
1465 auto_play_gif: false,
1466 display_sensitive_media: false,
1467 reduce_motion: false,
1468 max_toot_chars: limit,
1469 mascot: User.get_mascot(user)["url"]
1471 poll_limits: Config.get([:instance, :poll_limits]),
1473 delete_others_notice: present?(user.info.is_moderator),
1474 admin: present?(user.info.is_admin)
1478 default_privacy: user.info.default_scope,
1479 default_sensitive: false,
1480 allow_content_types: Config.get([:instance, :allowed_post_formats])
1482 media_attachments: %{
1483 accept_content_types: [
1499 user.info.settings ||
1529 push_subscription: nil,
1531 custom_emojis: mastodon_emoji,
1537 |> put_layout(false)
1538 |> put_view(MastodonView)
1539 |> render("index.html", %{initial_state: initial_state})
1542 |> put_session(:return_to, conn.request_path)
1543 |> redirect(to: "/web/login")
1547 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1548 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1550 with changeset <- Changeset.change(user),
1551 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1552 {:ok, _user} <- User.update_and_set_cache(changeset) do
1557 |> put_status(:internal_server_error)
1558 |> json(%{error: inspect(e)})
1562 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1563 redirect(conn, to: local_mastodon_root_path(conn))
1566 @doc "Local Mastodon FE login init action"
1567 def login(conn, %{"code" => auth_token}) do
1568 with {:ok, app} <- get_or_make_app(),
1569 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1570 {:ok, token} <- Token.exchange_token(app, auth) do
1572 |> put_session(:oauth_token, token.token)
1573 |> redirect(to: local_mastodon_root_path(conn))
1577 @doc "Local Mastodon FE callback action"
1578 def login(conn, _) do
1579 with {:ok, app} <- get_or_make_app() do
1584 response_type: "code",
1585 client_id: app.client_id,
1587 scope: Enum.join(app.scopes, " ")
1590 redirect(conn, to: path)
1594 defp local_mastodon_root_path(conn) do
1595 case get_session(conn, :return_to) do
1597 mastodon_api_path(conn, :index, ["getting-started"])
1600 delete_session(conn, :return_to)
1605 defp get_or_make_app do
1606 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1607 scopes = ["read", "write", "follow", "push"]
1609 with %App{} = app <- Repo.get_by(App, find_attrs) do
1611 if app.scopes == scopes do
1615 |> Changeset.change(%{scopes: scopes})
1623 App.register_changeset(
1625 Map.put(find_attrs, :scopes, scopes)
1632 def logout(conn, _) do
1635 |> redirect(to: "/")
1638 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1639 Logger.debug("Unimplemented, returning unmodified relationship")
1641 with %User{} = target <- User.get_cached_by_id(id) do
1643 |> put_view(AccountView)
1644 |> render("relationship.json", %{user: user, target: target})
1648 def empty_array(conn, _) do
1649 Logger.debug("Unimplemented, returning an empty array")
1653 def empty_object(conn, _) do
1654 Logger.debug("Unimplemented, returning an empty object")
1658 def endorsements(conn, params), do: empty_array(conn, params)
1660 def get_filters(%{assigns: %{user: user}} = conn, _) do
1661 filters = Filter.get_filters(user)
1662 res = FilterView.render("filters.json", filters: filters)
1667 %{assigns: %{user: user}} = conn,
1668 %{"phrase" => phrase, "context" => context} = params
1674 hide: Map.get(params, "irreversible", false),
1675 whole_word: Map.get(params, "boolean", true)
1679 {:ok, response} = Filter.create(query)
1680 res = FilterView.render("filter.json", filter: response)
1684 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1685 filter = Filter.get(filter_id, user)
1686 res = FilterView.render("filter.json", filter: filter)
1691 %{assigns: %{user: user}} = conn,
1692 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1696 filter_id: filter_id,
1699 hide: Map.get(params, "irreversible", nil),
1700 whole_word: Map.get(params, "boolean", true)
1704 {:ok, response} = Filter.update(query)
1705 res = FilterView.render("filter.json", filter: response)
1709 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1712 filter_id: filter_id
1715 {:ok, _} = Filter.delete(query)
1719 def suggestions(%{assigns: %{user: user}} = conn, _) do
1720 suggestions = Config.get(:suggestions)
1722 if Keyword.get(suggestions, :enabled, false) do
1723 api = Keyword.get(suggestions, :third_party_engine, "")
1724 timeout = Keyword.get(suggestions, :timeout, 5000)
1725 limit = Keyword.get(suggestions, :limit, 23)
1727 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1729 user = user.nickname
1733 |> String.replace("{{host}}", host)
1734 |> String.replace("{{user}}", user)
1736 with {:ok, %{status: 200, body: body}} <-
1737 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1738 {:ok, data} <- Jason.decode(body) do
1741 |> Enum.slice(0, limit)
1744 |> Map.put("id", fetch_suggestion_id(x))
1745 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1746 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1752 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1759 defp fetch_suggestion_id(attrs) do
1760 case User.get_or_fetch(attrs["acct"]) do
1761 {:ok, %User{id: id}} -> id
1766 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1767 with %Activity{} = activity <- Activity.get_by_id(status_id),
1768 true <- Visibility.visible_for_user?(activity, user) do
1772 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1782 def create_report(%{assigns: %{user: user}} = conn, params) do
1783 case CommonAPI.report(user, params) do
1786 |> put_view(ReportView)
1787 |> try_render("report.json", %{activity: activity})
1791 |> put_status(:bad_request)
1792 |> json(%{error: err})
1796 def account_register(
1797 %{assigns: %{app: app}} = conn,
1798 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1806 "captcha_answer_data",
1810 |> Map.put("nickname", nickname)
1811 |> Map.put("fullname", params["fullname"] || nickname)
1812 |> Map.put("bio", params["bio"] || "")
1813 |> Map.put("confirm", params["password"])
1815 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1816 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1818 token_type: "Bearer",
1819 access_token: token.token,
1821 created_at: Token.Utils.format_created_at(token)
1826 |> put_status(:bad_request)
1831 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1832 render_error(conn, :bad_request, "Missing parameters")
1835 def account_register(conn, _) do
1836 render_error(conn, :forbidden, "Invalid credentials")
1839 def conversations(%{assigns: %{user: user}} = conn, params) do
1840 participations = Participation.for_user_with_last_activity_id(user, params)
1843 Enum.map(participations, fn participation ->
1844 ConversationView.render("participation.json", %{participation: participation, for: user})
1848 |> add_link_headers(participations)
1849 |> json(conversations)
1852 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1853 with %Participation{} = participation <-
1854 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1855 {:ok, participation} <- Participation.mark_as_read(participation) do
1856 participation_view =
1857 ConversationView.render("participation.json", %{participation: participation, for: user})
1860 |> json(participation_view)
1864 def password_reset(conn, params) do
1865 nickname_or_email = params["email"] || params["nickname"]
1867 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1869 |> put_status(:no_content)
1872 {:error, "unknown user"} ->
1873 send_resp(conn, :not_found, "")
1876 send_resp(conn, :bad_request, "")
1880 def account_confirmation_resend(conn, params) do
1881 nickname_or_email = params["email"] || params["nickname"]
1883 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1884 {:ok, _} <- User.try_send_confirmation_email(user) do
1886 |> json_response(:no_content, "")
1890 def try_render(conn, target, params)
1891 when is_binary(target) do
1892 case render(conn, target, params) do
1893 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1898 def try_render(conn, _, _) do
1899 render_error(conn, :not_implemented, "Can't display this activity")
1902 defp present?(nil), do: false
1903 defp present?(false), do: false
1904 defp present?(_), do: true