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 plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
58 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
62 %{scopes: ["read"], skip_instance_privacy_check: true} when action == :index
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 @rate_limited_relations_actions ~w(follow unfollow)a
225 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
226 post_status delete_status)a
230 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
231 when action in ~w(reblog_status unreblog_status)a
236 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
237 when action in ~w(fav_status unfav_status)a
242 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
245 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
246 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
247 plug(RateLimiter, :app_account_creation when action == :account_register)
248 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
249 plug(RateLimiter, :password_reset when action == :password_reset)
250 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
252 @local_mastodon_name "Mastodon-Local"
254 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
256 def create_app(conn, params) do
257 scopes = Scopes.fetch_scopes(params, ["read"])
261 |> Map.drop(["scope", "scopes"])
262 |> Map.put("scopes", scopes)
264 with cs <- App.register_changeset(%App{}, app_attrs),
265 false <- cs.changes[:client_name] == @local_mastodon_name,
266 {:ok, app} <- Repo.insert(cs) do
269 |> render("show.json", %{app: app})
278 value_function \\ fn x -> {:ok, x} end
280 if Map.has_key?(params, params_field) do
281 case value_function.(params[params_field]) do
282 {:ok, new_value} -> Map.put(map, map_field, new_value)
290 def update_credentials(%{assigns: %{user: user}} = conn, params) do
295 |> add_if_present(params, "display_name", :name)
296 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
297 |> add_if_present(params, "avatar", :avatar, fn value ->
298 with %Plug.Upload{} <- value,
299 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
306 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
310 |> Map.get(:emoji, [])
311 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
322 :skip_thread_containment
324 |> Enum.reduce(%{}, fn key, acc ->
325 add_if_present(acc, params, to_string(key), key, fn value ->
326 {:ok, ControllerHelper.truthy_param?(value)}
329 |> add_if_present(params, "default_scope", :default_scope)
330 |> add_if_present(params, "fields", :fields, fn fields ->
331 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
335 |> add_if_present(params, "fields", :raw_fields)
336 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
337 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
339 |> add_if_present(params, "header", :banner, fn value ->
340 with %Plug.Upload{} <- value,
341 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
347 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
348 with %Plug.Upload{} <- value,
349 {:ok, object} <- ActivityPub.upload(value, type: :background) do
355 |> Map.put(:emoji, user_info_emojis)
357 info_cng = User.Info.profile_update(user.info, info_params)
359 with changeset <- User.update_changeset(user, user_params),
360 changeset <- Changeset.put_embed(changeset, :info, info_cng),
361 {:ok, user} <- User.update_and_set_cache(changeset) do
362 if original_user != user do
363 CommonAPI.update(user)
368 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
371 _e -> render_error(conn, :forbidden, "Invalid request")
375 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
376 change = Changeset.change(user, %{avatar: nil})
377 {:ok, user} = User.update_and_set_cache(change)
378 CommonAPI.update(user)
380 json(conn, %{url: nil})
383 def update_avatar(%{assigns: %{user: user}} = conn, params) do
384 {:ok, object} = ActivityPub.upload(params, type: :avatar)
385 change = Changeset.change(user, %{avatar: object.data})
386 {:ok, user} = User.update_and_set_cache(change)
387 CommonAPI.update(user)
388 %{"url" => [%{"href" => href} | _]} = object.data
390 json(conn, %{url: href})
393 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
394 with new_info <- %{"banner" => %{}},
395 info_cng <- User.Info.profile_update(user.info, new_info),
396 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
397 {:ok, user} <- User.update_and_set_cache(changeset) do
398 CommonAPI.update(user)
400 json(conn, %{url: nil})
404 def update_banner(%{assigns: %{user: user}} = conn, params) do
405 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
406 new_info <- %{"banner" => object.data},
407 info_cng <- User.Info.profile_update(user.info, new_info),
408 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
409 {:ok, user} <- User.update_and_set_cache(changeset) do
410 CommonAPI.update(user)
411 %{"url" => [%{"href" => href} | _]} = object.data
413 json(conn, %{url: href})
417 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
418 with new_info <- %{"background" => %{}},
419 info_cng <- User.Info.profile_update(user.info, new_info),
420 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
421 {:ok, _user} <- User.update_and_set_cache(changeset) do
422 json(conn, %{url: nil})
426 def update_background(%{assigns: %{user: user}} = conn, params) do
427 with {:ok, object} <- ActivityPub.upload(params, type: :background),
428 new_info <- %{"background" => object.data},
429 info_cng <- User.Info.profile_update(user.info, new_info),
430 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
431 {:ok, _user} <- User.update_and_set_cache(changeset) do
432 %{"url" => [%{"href" => href} | _]} = object.data
434 json(conn, %{url: href})
438 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
439 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
442 AccountView.render("account.json", %{
445 with_pleroma_settings: true,
446 with_chat_token: chat_token
452 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
453 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
456 |> render("short.json", %{app: app})
460 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
461 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
462 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
463 account = AccountView.render("account.json", %{user: user, for: for_user})
466 _e -> render_error(conn, :not_found, "Can't find user")
470 @mastodon_api_level "2.7.2"
472 def masto_instance(conn, _params) do
473 instance = Config.get(:instance)
477 title: Keyword.get(instance, :name),
478 description: Keyword.get(instance, :description),
479 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
480 email: Keyword.get(instance, :email),
482 streaming_api: Pleroma.Web.Endpoint.websocket_url()
484 stats: Stats.get_stats(),
485 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
487 registrations: Pleroma.Config.get([:instance, :registrations_open]),
488 # Extra (not present in Mastodon):
489 max_toot_chars: Keyword.get(instance, :limit),
490 poll_limits: Keyword.get(instance, :poll_limits)
496 def peers(conn, _params) do
497 json(conn, Stats.get_peers())
500 defp mastodonized_emoji do
501 Pleroma.Emoji.get_all()
502 |> Enum.map(fn {shortcode, relative_url, tags} ->
503 url = to_string(URI.merge(Web.base_url(), relative_url))
506 "shortcode" => shortcode,
508 "visible_in_picker" => true,
511 # Assuming that a comma is authorized in the category name
512 "category" => (tags -- ["Custom"]) |> Enum.join(",")
517 def custom_emojis(conn, _params) do
518 mastodon_emoji = mastodonized_emoji()
519 json(conn, mastodon_emoji)
522 def home_timeline(%{assigns: %{user: user}} = conn, params) do
525 |> Map.put("type", ["Create", "Announce"])
526 |> Map.put("blocking_user", user)
527 |> Map.put("muting_user", user)
528 |> Map.put("user", user)
531 [user.ap_id | user.following]
532 |> ActivityPub.fetch_activities(params)
536 |> add_link_headers(activities)
537 |> put_view(StatusView)
538 |> render("index.json", %{activities: activities, for: user, as: :activity})
541 def public_timeline(%{assigns: %{user: user}} = conn, params) do
542 local_only = params["local"] in [true, "True", "true", "1"]
546 |> Map.put("type", ["Create", "Announce"])
547 |> Map.put("local_only", local_only)
548 |> Map.put("blocking_user", user)
549 |> Map.put("muting_user", user)
550 |> Map.put("user", user)
551 |> ActivityPub.fetch_public_activities()
555 |> add_link_headers(activities, %{"local" => local_only})
556 |> put_view(StatusView)
557 |> render("index.json", %{activities: activities, for: user, as: :activity})
560 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
561 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
564 |> Map.put("tag", params["tagged"])
566 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
569 |> add_link_headers(activities)
570 |> put_view(StatusView)
571 |> render("index.json", %{
572 activities: activities,
579 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
582 |> Map.put("type", "Create")
583 |> Map.put("blocking_user", user)
584 |> Map.put("user", user)
585 |> Map.put(:visibility, "direct")
589 |> ActivityPub.fetch_activities_query(params)
590 |> Pagination.fetch_paginated(params)
593 |> add_link_headers(activities)
594 |> put_view(StatusView)
595 |> render("index.json", %{activities: activities, for: user, as: :activity})
598 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
604 |> Activity.all_by_ids_with_object()
605 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
608 |> put_view(StatusView)
609 |> render("index.json", activities: activities, for: user, as: :activity)
612 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
613 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
614 true <- Visibility.visible_for_user?(activity, user) do
616 |> put_view(StatusView)
617 |> try_render("status.json", %{activity: activity, for: user})
621 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
622 with %Activity{} = activity <- Activity.get_by_id(id),
624 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
625 "blocking_user" => user,
627 "exclude_id" => activity.id
629 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
635 activities: grouped_activities[true] || [],
639 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
644 activities: grouped_activities[false] || [],
648 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
655 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
656 with %Object{} = object <- Object.get_by_id(id),
657 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
658 true <- Visibility.visible_for_user?(activity, user) do
660 |> put_view(StatusView)
661 |> try_render("poll.json", %{object: object, for: user})
663 error when is_nil(error) or error == false ->
664 render_error(conn, :not_found, "Record not found")
668 defp get_cached_vote_or_vote(user, object, choices) do
669 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
672 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
673 case CommonAPI.vote(user, object, choices) do
674 {:error, _message} = res -> {:ignore, res}
675 res -> {:commit, res}
682 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
683 with %Object{} = object <- Object.get_by_id(id),
684 true <- object.data["type"] == "Question",
685 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
686 true <- Visibility.visible_for_user?(activity, user),
687 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
689 |> put_view(StatusView)
690 |> try_render("poll.json", %{object: object, for: user})
693 render_error(conn, :not_found, "Record not found")
696 render_error(conn, :not_found, "Record not found")
700 |> put_status(:unprocessable_entity)
701 |> json(%{error: message})
705 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
706 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
708 |> add_link_headers(scheduled_activities)
709 |> put_view(ScheduledActivityView)
710 |> render("index.json", %{scheduled_activities: scheduled_activities})
714 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
715 with %ScheduledActivity{} = scheduled_activity <-
716 ScheduledActivity.get(user, scheduled_activity_id) do
718 |> put_view(ScheduledActivityView)
719 |> render("show.json", %{scheduled_activity: scheduled_activity})
721 _ -> {:error, :not_found}
725 def update_scheduled_status(
726 %{assigns: %{user: user}} = conn,
727 %{"id" => scheduled_activity_id} = params
729 with %ScheduledActivity{} = scheduled_activity <-
730 ScheduledActivity.get(user, scheduled_activity_id),
731 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
733 |> put_view(ScheduledActivityView)
734 |> render("show.json", %{scheduled_activity: scheduled_activity})
736 nil -> {:error, :not_found}
741 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
742 with %ScheduledActivity{} = scheduled_activity <-
743 ScheduledActivity.get(user, scheduled_activity_id),
744 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
746 |> put_view(ScheduledActivityView)
747 |> render("show.json", %{scheduled_activity: scheduled_activity})
749 nil -> {:error, :not_found}
754 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
757 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
759 scheduled_at = params["scheduled_at"]
761 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
762 with {:ok, scheduled_activity} <-
763 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
765 |> put_view(ScheduledActivityView)
766 |> render("show.json", %{scheduled_activity: scheduled_activity})
769 params = Map.drop(params, ["scheduled_at"])
771 case CommonAPI.post(user, params) do
774 |> put_status(:unprocessable_entity)
775 |> json(%{error: message})
779 |> put_view(StatusView)
780 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
785 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
786 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
789 _e -> render_error(conn, :forbidden, "Can't delete this post")
793 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
794 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
795 %Activity{} = announce <- Activity.normalize(announce.data) do
797 |> put_view(StatusView)
798 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
802 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
803 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
804 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
806 |> put_view(StatusView)
807 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
811 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
812 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
813 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
815 |> put_view(StatusView)
816 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
820 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
821 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
822 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
824 |> put_view(StatusView)
825 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
829 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
830 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
832 |> put_view(StatusView)
833 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
837 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
838 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
840 |> put_view(StatusView)
841 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
845 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
846 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
847 %User{} = user <- User.get_cached_by_nickname(user.nickname),
848 true <- Visibility.visible_for_user?(activity, user),
849 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
851 |> put_view(StatusView)
852 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
856 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
857 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
858 %User{} = user <- User.get_cached_by_nickname(user.nickname),
859 true <- Visibility.visible_for_user?(activity, user),
860 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
862 |> put_view(StatusView)
863 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
867 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
868 activity = Activity.get_by_id(id)
870 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
872 |> put_view(StatusView)
873 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
877 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
878 activity = Activity.get_by_id(id)
880 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
882 |> put_view(StatusView)
883 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
887 def notifications(%{assigns: %{user: user}} = conn, params) do
888 notifications = MastodonAPI.get_notifications(user, params)
891 |> add_link_headers(notifications)
892 |> put_view(NotificationView)
893 |> render("index.json", %{notifications: notifications, for: user})
896 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
897 with {:ok, notification} <- Notification.get(user, id) do
899 |> put_view(NotificationView)
900 |> render("show.json", %{notification: notification, for: user})
904 |> put_status(:forbidden)
905 |> json(%{"error" => reason})
909 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
910 Notification.clear(user)
914 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
915 with {:ok, _notif} <- Notification.dismiss(user, id) do
920 |> put_status(:forbidden)
921 |> json(%{"error" => reason})
925 def destroy_multiple_notifications(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
926 Notification.destroy_multiple(user, ids)
930 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
932 q = from(u in User, where: u.id in ^id)
933 targets = Repo.all(q)
936 |> put_view(AccountView)
937 |> render("relationships.json", %{user: user, targets: targets})
940 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
941 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
943 def update_media(%{assigns: %{user: user}} = conn, data) do
944 with %Object{} = object <- Repo.get(Object, data["id"]),
945 true <- Object.authorize_mutation(object, user),
946 true <- is_binary(data["description"]),
947 description <- data["description"] do
948 new_data = %{object.data | "name" => description}
952 |> Object.change(%{data: new_data})
955 attachment_data = Map.put(new_data, "id", object.id)
958 |> put_view(StatusView)
959 |> render("attachment.json", %{attachment: attachment_data})
963 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
964 with {:ok, object} <-
967 actor: User.ap_id(user),
968 description: Map.get(data, "description")
970 attachment_data = Map.put(object.data, "id", object.id)
973 |> put_view(StatusView)
974 |> render("attachment.json", %{attachment: attachment_data})
978 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
979 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
980 %{} = attachment_data <- Map.put(object.data, "id", object.id),
981 %{type: type} = rendered <-
982 StatusView.render("attachment.json", %{attachment: attachment_data}) do
983 # Reject if not an image
984 if type == "image" do
986 # Save to the user's info
987 info_changeset = User.Info.mascot_update(user.info, rendered)
991 |> Changeset.change()
992 |> Changeset.put_embed(:info, info_changeset)
994 {:ok, _user} = User.update_and_set_cache(user_changeset)
999 render_error(conn, :unsupported_media_type, "mascots can only be images")
1004 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
1005 mascot = User.get_mascot(user)
1011 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1012 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
1013 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
1014 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
1015 q = from(u in User, where: u.ap_id in ^likes)
1019 |> Enum.filter(&(not User.blocks?(user, &1)))
1022 |> put_view(AccountView)
1023 |> render("accounts.json", %{for: user, users: users, as: :user})
1025 {:visible, false} -> {:error, :not_found}
1030 def reblogged_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: %{"announcements" => announces}} <- Object.normalize(activity) do
1034 q = from(u in User, where: u.ap_id in ^announces)
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 hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
1050 local_only = params["local"] in [true, "True", "true", "1"]
1053 [params["tag"], params["any"]]
1056 |> Enum.filter(& &1)
1057 |> Enum.map(&String.downcase(&1))
1062 |> Enum.map(&String.downcase(&1))
1067 |> Enum.map(&String.downcase(&1))
1071 |> Map.put("type", "Create")
1072 |> Map.put("local_only", local_only)
1073 |> Map.put("blocking_user", user)
1074 |> Map.put("muting_user", user)
1075 |> Map.put("user", user)
1076 |> Map.put("tag", tags)
1077 |> Map.put("tag_all", tag_all)
1078 |> Map.put("tag_reject", tag_reject)
1079 |> ActivityPub.fetch_public_activities()
1083 |> add_link_headers(activities, %{"local" => local_only})
1084 |> put_view(StatusView)
1085 |> render("index.json", %{activities: activities, for: user, as: :activity})
1088 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1089 with %User{} = user <- User.get_cached_by_id(id),
1090 followers <- MastodonAPI.get_followers(user, params) do
1093 for_user && user.id == for_user.id -> followers
1094 user.info.hide_followers -> []
1099 |> add_link_headers(followers)
1100 |> put_view(AccountView)
1101 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1105 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1106 with %User{} = user <- User.get_cached_by_id(id),
1107 followers <- MastodonAPI.get_friends(user, params) do
1110 for_user && user.id == for_user.id -> followers
1111 user.info.hide_follows -> []
1116 |> add_link_headers(followers)
1117 |> put_view(AccountView)
1118 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1122 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
1123 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
1125 |> put_view(AccountView)
1126 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1130 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1131 with %User{} = follower <- User.get_cached_by_id(id),
1132 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1134 |> put_view(AccountView)
1135 |> render("relationship.json", %{user: followed, target: follower})
1137 {:error, message} ->
1139 |> put_status(:forbidden)
1140 |> json(%{error: message})
1144 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1145 with %User{} = follower <- User.get_cached_by_id(id),
1146 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1148 |> put_view(AccountView)
1149 |> render("relationship.json", %{user: followed, target: follower})
1151 {:error, message} ->
1153 |> put_status(:forbidden)
1154 |> json(%{error: message})
1158 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1159 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1160 {_, true} <- {:followed, follower.id != followed.id},
1161 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1163 |> put_view(AccountView)
1164 |> render("relationship.json", %{user: follower, target: followed})
1167 {:error, :not_found}
1169 {:error, message} ->
1171 |> put_status(:forbidden)
1172 |> json(%{error: message})
1176 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1177 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1178 {_, true} <- {:followed, follower.id != followed.id},
1179 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1181 |> put_view(AccountView)
1182 |> render("account.json", %{user: followed, for: follower})
1185 {:error, :not_found}
1187 {:error, message} ->
1189 |> put_status(:forbidden)
1190 |> json(%{error: message})
1194 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1195 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1196 {_, true} <- {:followed, follower.id != followed.id},
1197 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1199 |> put_view(AccountView)
1200 |> render("relationship.json", %{user: follower, target: followed})
1203 {:error, :not_found}
1210 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1212 if Map.has_key?(params, "notifications"),
1213 do: params["notifications"] in [true, "True", "true", "1"],
1216 with %User{} = muted <- User.get_cached_by_id(id),
1217 {:ok, muter} <- User.mute(muter, muted, notifications) do
1219 |> put_view(AccountView)
1220 |> render("relationship.json", %{user: muter, target: muted})
1222 {:error, message} ->
1224 |> put_status(:forbidden)
1225 |> json(%{error: message})
1229 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1230 with %User{} = muted <- User.get_cached_by_id(id),
1231 {:ok, muter} <- User.unmute(muter, muted) do
1233 |> put_view(AccountView)
1234 |> render("relationship.json", %{user: muter, target: muted})
1236 {:error, message} ->
1238 |> put_status(:forbidden)
1239 |> json(%{error: message})
1243 def mutes(%{assigns: %{user: user}} = conn, _) do
1244 with muted_accounts <- User.muted_users(user) do
1245 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1250 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1251 with %User{} = blocked <- User.get_cached_by_id(id),
1252 {:ok, blocker} <- User.block(blocker, blocked),
1253 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1255 |> put_view(AccountView)
1256 |> render("relationship.json", %{user: blocker, target: blocked})
1258 {:error, message} ->
1260 |> put_status(:forbidden)
1261 |> json(%{error: message})
1265 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1266 with %User{} = blocked <- User.get_cached_by_id(id),
1267 {:ok, blocker} <- User.unblock(blocker, blocked),
1268 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1270 |> put_view(AccountView)
1271 |> render("relationship.json", %{user: blocker, target: blocked})
1273 {:error, message} ->
1275 |> put_status(:forbidden)
1276 |> json(%{error: message})
1280 def blocks(%{assigns: %{user: user}} = conn, _) do
1281 with blocked_accounts <- User.blocked_users(user) do
1282 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1287 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1288 json(conn, info.domain_blocks || [])
1291 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1292 User.block_domain(blocker, domain)
1296 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1297 User.unblock_domain(blocker, domain)
1301 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1302 with %User{} = subscription_target <- User.get_cached_by_id(id),
1303 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1305 |> put_view(AccountView)
1306 |> render("relationship.json", %{user: user, target: subscription_target})
1308 {:error, message} ->
1310 |> put_status(:forbidden)
1311 |> json(%{error: message})
1315 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1316 with %User{} = subscription_target <- User.get_cached_by_id(id),
1317 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1319 |> put_view(AccountView)
1320 |> render("relationship.json", %{user: user, target: subscription_target})
1322 {:error, message} ->
1324 |> put_status(:forbidden)
1325 |> json(%{error: message})
1329 def favourites(%{assigns: %{user: user}} = conn, params) do
1332 |> Map.put("type", "Create")
1333 |> Map.put("favorited_by", user.ap_id)
1334 |> Map.put("blocking_user", user)
1337 ActivityPub.fetch_activities([], params)
1341 |> add_link_headers(activities)
1342 |> put_view(StatusView)
1343 |> render("index.json", %{activities: activities, for: user, as: :activity})
1346 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1347 with %User{} = user <- User.get_by_id(id),
1348 false <- user.info.hide_favorites do
1351 |> Map.put("type", "Create")
1352 |> Map.put("favorited_by", user.ap_id)
1353 |> Map.put("blocking_user", for_user)
1357 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1359 [Pleroma.Constants.as_public()]
1364 |> ActivityPub.fetch_activities(params)
1368 |> add_link_headers(activities)
1369 |> put_view(StatusView)
1370 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1372 nil -> {:error, :not_found}
1373 true -> render_error(conn, :forbidden, "Can't get favorites")
1377 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1378 user = User.get_cached_by_id(user.id)
1381 Bookmark.for_user_query(user.id)
1382 |> Pagination.fetch_paginated(params)
1386 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1389 |> add_link_headers(bookmarks)
1390 |> put_view(StatusView)
1391 |> render("index.json", %{activities: activities, for: user, as: :activity})
1394 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1395 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1396 res = ListView.render("lists.json", lists: lists)
1400 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1401 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1404 |> Map.put("type", "Create")
1405 |> Map.put("blocking_user", user)
1406 |> Map.put("user", user)
1407 |> Map.put("muting_user", user)
1409 # we must filter the following list for the user to avoid leaking statuses the user
1410 # does not actually have permission to see (for more info, peruse security issue #270).
1413 |> Enum.filter(fn x -> x in user.following end)
1414 |> ActivityPub.fetch_activities_bounded(following, params)
1418 |> put_view(StatusView)
1419 |> render("index.json", %{activities: activities, for: user, as: :activity})
1421 _e -> render_error(conn, :forbidden, "Error.")
1425 def index(%{assigns: %{user: user}} = conn, _params) do
1426 token = get_session(conn, :oauth_token)
1429 mastodon_emoji = mastodonized_emoji()
1431 limit = Config.get([:instance, :limit])
1434 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1439 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1440 access_token: token,
1442 domain: Pleroma.Web.Endpoint.host(),
1445 unfollow_modal: false,
1448 auto_play_gif: false,
1449 display_sensitive_media: false,
1450 reduce_motion: false,
1451 max_toot_chars: limit,
1452 mascot: User.get_mascot(user)["url"]
1454 poll_limits: Config.get([:instance, :poll_limits]),
1456 delete_others_notice: present?(user.info.is_moderator),
1457 admin: present?(user.info.is_admin)
1461 default_privacy: user.info.default_scope,
1462 default_sensitive: false,
1463 allow_content_types: Config.get([:instance, :allowed_post_formats])
1465 media_attachments: %{
1466 accept_content_types: [
1482 user.info.settings ||
1512 push_subscription: nil,
1514 custom_emojis: mastodon_emoji,
1520 |> put_layout(false)
1521 |> put_view(MastodonView)
1522 |> render("index.html", %{initial_state: initial_state})
1525 |> put_session(:return_to, conn.request_path)
1526 |> redirect(to: "/web/login")
1530 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1531 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1533 with changeset <- Changeset.change(user),
1534 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1535 {:ok, _user} <- User.update_and_set_cache(changeset) do
1540 |> put_status(:internal_server_error)
1541 |> json(%{error: inspect(e)})
1545 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1546 redirect(conn, to: local_mastodon_root_path(conn))
1549 @doc "Local Mastodon FE login init action"
1550 def login(conn, %{"code" => auth_token}) do
1551 with {:ok, app} <- get_or_make_app(),
1552 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1553 {:ok, token} <- Token.exchange_token(app, auth) do
1555 |> put_session(:oauth_token, token.token)
1556 |> redirect(to: local_mastodon_root_path(conn))
1560 @doc "Local Mastodon FE callback action"
1561 def login(conn, _) do
1562 with {:ok, app} <- get_or_make_app() do
1567 response_type: "code",
1568 client_id: app.client_id,
1570 scope: Enum.join(app.scopes, " ")
1573 redirect(conn, to: path)
1577 defp local_mastodon_root_path(conn) do
1578 case get_session(conn, :return_to) do
1580 mastodon_api_path(conn, :index, ["getting-started"])
1583 delete_session(conn, :return_to)
1588 defp get_or_make_app do
1589 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1590 scopes = ["read", "write", "follow", "push"]
1592 with %App{} = app <- Repo.get_by(App, find_attrs) do
1594 if app.scopes == scopes do
1598 |> Changeset.change(%{scopes: scopes})
1606 App.register_changeset(
1608 Map.put(find_attrs, :scopes, scopes)
1615 def logout(conn, _) do
1618 |> redirect(to: "/")
1621 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1622 Logger.debug("Unimplemented, returning unmodified relationship")
1624 with %User{} = target <- User.get_cached_by_id(id) do
1626 |> put_view(AccountView)
1627 |> render("relationship.json", %{user: user, target: target})
1631 def empty_array(conn, _) do
1632 Logger.debug("Unimplemented, returning an empty array")
1636 def empty_object(conn, _) do
1637 Logger.debug("Unimplemented, returning an empty object")
1641 def endorsements(conn, params), do: empty_array(conn, params)
1643 def get_filters(%{assigns: %{user: user}} = conn, _) do
1644 filters = Filter.get_filters(user)
1645 res = FilterView.render("filters.json", filters: filters)
1650 %{assigns: %{user: user}} = conn,
1651 %{"phrase" => phrase, "context" => context} = params
1657 hide: Map.get(params, "irreversible", false),
1658 whole_word: Map.get(params, "boolean", true)
1662 {:ok, response} = Filter.create(query)
1663 res = FilterView.render("filter.json", filter: response)
1667 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1668 filter = Filter.get(filter_id, user)
1669 res = FilterView.render("filter.json", filter: filter)
1674 %{assigns: %{user: user}} = conn,
1675 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1679 filter_id: filter_id,
1682 hide: Map.get(params, "irreversible", nil),
1683 whole_word: Map.get(params, "boolean", true)
1687 {:ok, response} = Filter.update(query)
1688 res = FilterView.render("filter.json", filter: response)
1692 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1695 filter_id: filter_id
1698 {:ok, _} = Filter.delete(query)
1702 def suggestions(%{assigns: %{user: user}} = conn, _) do
1703 suggestions = Config.get(:suggestions)
1705 if Keyword.get(suggestions, :enabled, false) do
1706 api = Keyword.get(suggestions, :third_party_engine, "")
1707 timeout = Keyword.get(suggestions, :timeout, 5000)
1708 limit = Keyword.get(suggestions, :limit, 23)
1710 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1712 user = user.nickname
1716 |> String.replace("{{host}}", host)
1717 |> String.replace("{{user}}", user)
1719 with {:ok, %{status: 200, body: body}} <-
1720 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1721 {:ok, data} <- Jason.decode(body) do
1724 |> Enum.slice(0, limit)
1727 |> Map.put("id", fetch_suggestion_id(x))
1728 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1729 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1735 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1742 defp fetch_suggestion_id(attrs) do
1743 case User.get_or_fetch(attrs["acct"]) do
1744 {:ok, %User{id: id}} -> id
1749 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1750 with %Activity{} = activity <- Activity.get_by_id(status_id),
1751 true <- Visibility.visible_for_user?(activity, user) do
1755 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1765 def create_report(%{assigns: %{user: user}} = conn, params) do
1766 case CommonAPI.report(user, params) do
1769 |> put_view(ReportView)
1770 |> try_render("report.json", %{activity: activity})
1774 |> put_status(:bad_request)
1775 |> json(%{error: err})
1779 def account_register(
1780 %{assigns: %{app: app}} = conn,
1781 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1789 "captcha_answer_data",
1793 |> Map.put("nickname", nickname)
1794 |> Map.put("fullname", params["fullname"] || nickname)
1795 |> Map.put("bio", params["bio"] || "")
1796 |> Map.put("confirm", params["password"])
1798 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1799 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1801 token_type: "Bearer",
1802 access_token: token.token,
1804 created_at: Token.Utils.format_created_at(token)
1809 |> put_status(:bad_request)
1814 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1815 render_error(conn, :bad_request, "Missing parameters")
1818 def account_register(conn, _) do
1819 render_error(conn, :forbidden, "Invalid credentials")
1822 def conversations(%{assigns: %{user: user}} = conn, params) do
1823 participations = Participation.for_user_with_last_activity_id(user, params)
1826 Enum.map(participations, fn participation ->
1827 ConversationView.render("participation.json", %{participation: participation, for: user})
1831 |> add_link_headers(participations)
1832 |> json(conversations)
1835 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1836 with %Participation{} = participation <-
1837 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1838 {:ok, participation} <- Participation.mark_as_read(participation) do
1839 participation_view =
1840 ConversationView.render("participation.json", %{participation: participation, for: user})
1843 |> json(participation_view)
1847 def password_reset(conn, params) do
1848 nickname_or_email = params["email"] || params["nickname"]
1850 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1852 |> put_status(:no_content)
1855 {:error, "unknown user"} ->
1856 send_resp(conn, :not_found, "")
1859 send_resp(conn, :bad_request, "")
1863 def account_confirmation_resend(conn, params) do
1864 nickname_or_email = params["email"] || params["nickname"]
1866 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1867 {:ok, _} <- User.try_send_confirmation_email(user) do
1869 |> json_response(:no_content, "")
1873 def try_render(conn, target, params)
1874 when is_binary(target) do
1875 case render(conn, target, params) do
1876 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1881 def try_render(conn, _, _) do
1882 render_error(conn, :not_implemented, "Can't display this activity")
1885 defp present?(nil), do: false
1886 defp present?(false), do: false
1887 defp present?(_), do: true