Merge branch 'develop' into feature/matstodon-statuses-by-name
authorMark Felder <feld@FreeBSD.org>
Mon, 15 Jul 2019 22:10:27 +0000 (17:10 -0500)
committerMark Felder <feld@FreeBSD.org>
Mon, 15 Jul 2019 22:10:27 +0000 (17:10 -0500)
1  2 
docs/api/differences_in_mastoapi_responses.md
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex

index f952696f7ea8e961a46360dc3bb6534282b9dd39,d2e9bcc4b83504dde87e45ee66d73875a2ef239f..f5a72543a20ac199996bc1d909abf2fa568b8073
@@@ -16,9 -16,11 +16,11 @@@ Adding the parameter `with_muted=true` 
  
  ## Statuses
  
+ - `visibility`: has an additional possible value `list`
  Has these additional fields under the `pleroma` object:
  
- - `local`: true if the post was made on the local instance.
+ - `local`: true if the post was made on the local instance
  - `conversation_id`: the ID of the conversation the status is associated with (if any)
  - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
  - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
@@@ -32,10 -34,7 +34,10 @@@ Has these additional fields under the `
  
  ## Accounts
  
 -- `/api/v1/accounts/:id`: The `id` parameter can also be the `nickname` of the user. This only works in this endpoint, not the deeper nested ones for following etc.
 +The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
 +
 +- `/api/v1/accounts/:id`
 +- `/api/v1/accounts/:id/statuses`
  
  Has these additional fields under the `pleroma` object:
  
@@@ -46,6 -45,8 +48,8 @@@
  - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
  - `hide_followers`: boolean, true when the user has follower hiding enabled
  - `hide_follows`: boolean, true when the user has follow hiding enabled
+ - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
+ - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
  
  ### Source
  
@@@ -72,6 -73,8 +76,8 @@@ Additional parameters can be added to t
  
  - `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
  - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
+ - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
+ - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
  
  ## PATCH `/api/v1/update_credentials`
  
@@@ -83,6 -86,16 +89,16 @@@ Additional parameters can be added to t
  - `hide_favorites` - if true, user's favorites timeline will be hidden
  - `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API
  - `default_scope` - the scope returned under `privacy` key in Source subentity
+ - `pleroma_settings_store` - Opaque user settings to be saved on the backend.
+ - `skip_thread_containment` - if true, skip filtering out broken threads
+ - `pleroma_background_image` - sets the background image of the user.
+ ### Pleroma Settings Store
+ Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
+ The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings.
+ This information is returned in the `verify_credentials` endpoint.
  
  ## Authentication
  
index bc75ab35a09f14ab47c76a0228ca4e5a31b88a77,f4aa576f73463482ccc35aea5c2bfa8acb9315c6..29b1391d360ebe7e5ec5826ca4e3a676983ad403
@@@ -14,8 -14,8 +14,8 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
    alias Pleroma.HTTP
    alias Pleroma.Notification
    alias Pleroma.Object
-   alias Pleroma.Object.Fetcher
    alias Pleroma.Pagination
+   alias Pleroma.Plugs.RateLimiter
    alias Pleroma.Repo
    alias Pleroma.ScheduledActivity
    alias Pleroma.Stats
  
    require Logger
  
+   @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
+     post_status delete_status)a
    plug(
-     Pleroma.Plugs.RateLimitPlug,
-     %{
-       max_requests: Config.get([:app_account_creation, :max_requests]),
-       interval: Config.get([:app_account_creation, :interval])
-     }
-     when action in [:account_register]
+     RateLimiter,
+     {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+     when action in ~w(reblog_status unreblog_status)a
+   )
+   plug(
+     RateLimiter,
+     {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+     when action in ~w(fav_status unfav_status)a
    )
  
+   plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+   plug(RateLimiter, :app_account_creation when action == :account_register)
+   plug(RateLimiter, :search when action in [:search, :search2, :account_search])
    @local_mastodon_name "Mastodon-Local"
  
    action_fallback(:errors)
        |> Enum.dedup()
  
      info_params =
-       [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
+       [
+         :no_rich_text,
+         :locked,
+         :hide_followers,
+         :hide_follows,
+         :hide_favorites,
+         :show_role,
+         :skip_thread_containment
+       ]
        |> Enum.reduce(%{}, fn key, acc ->
          add_if_present(acc, params, to_string(key), key, fn value ->
            {:ok, ControllerHelper.truthy_param?(value)}
          end)
        end)
        |> add_if_present(params, "default_scope", :default_scope)
+       |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
+         {:ok, Map.merge(user.info.pleroma_settings_store, value)}
+       end)
        |> add_if_present(params, "header", :banner, fn value ->
          with %Plug.Upload{} <- value,
               {:ok, object} <- ActivityPub.upload(value, type: :banner) do
            _ -> :error
          end
        end)
+       |> add_if_present(params, "pleroma_background_image", :background, fn value ->
+         with %Plug.Upload{} <- value,
+              {:ok, object} <- ActivityPub.upload(value, type: :background) do
+           {:ok, object.data}
+         else
+           _ -> :error
+         end
+       end)
        |> Map.put(:emoji, user_info_emojis)
  
      info_cng = User.Info.profile_update(user.info, info_params)
          CommonAPI.update(user)
        end
  
-       json(conn, AccountView.render("account.json", %{user: user, for: user}))
+       json(
+         conn,
+         AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
+       )
      else
-       _e ->
-         conn
-         |> put_status(403)
-         |> json(%{error: "Invalid request"})
+       _e -> render_error(conn, :forbidden, "Invalid request")
+     end
+   end
+   def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
+     change = Changeset.change(user, %{avatar: nil})
+     {:ok, user} = User.update_and_set_cache(change)
+     CommonAPI.update(user)
+     json(conn, %{url: nil})
+   end
+   def update_avatar(%{assigns: %{user: user}} = conn, params) do
+     {:ok, object} = ActivityPub.upload(params, type: :avatar)
+     change = Changeset.change(user, %{avatar: object.data})
+     {:ok, user} = User.update_and_set_cache(change)
+     CommonAPI.update(user)
+     %{"url" => [%{"href" => href} | _]} = object.data
+     json(conn, %{url: href})
+   end
+   def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
+     with new_info <- %{"banner" => %{}},
+          info_cng <- User.Info.profile_update(user.info, new_info),
+          changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+          {:ok, user} <- User.update_and_set_cache(changeset) do
+       CommonAPI.update(user)
+       json(conn, %{url: nil})
+     end
+   end
+   def update_banner(%{assigns: %{user: user}} = conn, params) do
+     with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
+          new_info <- %{"banner" => object.data},
+          info_cng <- User.Info.profile_update(user.info, new_info),
+          changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+          {:ok, user} <- User.update_and_set_cache(changeset) do
+       CommonAPI.update(user)
+       %{"url" => [%{"href" => href} | _]} = object.data
+       json(conn, %{url: href})
+     end
+   end
+   def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
+     with new_info <- %{"background" => %{}},
+          info_cng <- User.Info.profile_update(user.info, new_info),
+          changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+          {:ok, _user} <- User.update_and_set_cache(changeset) do
+       json(conn, %{url: nil})
+     end
+   end
+   def update_background(%{assigns: %{user: user}} = conn, params) do
+     with {:ok, object} <- ActivityPub.upload(params, type: :background),
+          new_info <- %{"background" => object.data},
+          info_cng <- User.Info.profile_update(user.info, new_info),
+          changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+          {:ok, _user} <- User.update_and_set_cache(changeset) do
+       %{"url" => [%{"href" => href} | _]} = object.data
+       json(conn, %{url: href})
      end
    end
  
    def verify_credentials(%{assigns: %{user: user}} = conn, _) do
-     account = AccountView.render("account.json", %{user: user, for: user})
+     chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
+     account =
+       AccountView.render("account.json", %{
+         user: user,
+         for: user,
+         with_pleroma_settings: true,
+         with_chat_token: chat_token
+       })
      json(conn, account)
    end
  
        account = AccountView.render("account.json", %{user: user, for: for_user})
        json(conn, account)
      else
-       _e ->
-         conn
-         |> put_status(404)
-         |> json(%{error: "Can't find user"})
+       _e -> render_error(conn, :not_found, "Can't find user")
      end
    end
  
        languages: ["en"],
        registrations: Pleroma.Config.get([:instance, :registrations_open]),
        # Extra (not present in Mastodon):
-       max_toot_chars: Keyword.get(instance, :limit)
+       max_toot_chars: Keyword.get(instance, :limit),
+       poll_limits: Keyword.get(instance, :poll_limits)
      }
  
      json(conn, response)
          "static_url" => url,
          "visible_in_picker" => true,
          "url" => url,
-         "tags" => tags
+         "tags" => tags,
+         # Assuming that a comma is authorized in the category name
+         "category" => (tags -- ["Custom"]) |> Enum.join(",")
        }
      end)
    end
    end
  
    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
 -    with %User{} = user <- User.get_cached_by_id(params["id"]) do
 +    with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
+       params =
+         params
+         |> Map.put("tag", params["tagged"])
        activities = ActivityPub.fetch_user_activities(user, reading_user, params)
  
        conn
      end
    end
  
+   def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Object{} = object <- Object.get_by_id(id),
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+          true <- Visibility.visible_for_user?(activity, user) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("poll.json", %{object: object, for: user})
+     else
+       nil -> render_error(conn, :not_found, "Record not found")
+       false -> render_error(conn, :not_found, "Record not found")
+     end
+   end
+   defp get_cached_vote_or_vote(user, object, choices) do
+     idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
+     {_, res} =
+       Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
+         case CommonAPI.vote(user, object, choices) do
+           {:error, _message} = res -> {:ignore, res}
+           res -> {:commit, res}
+         end
+       end)
+     res
+   end
+   def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
+     with %Object{} = object <- Object.get_by_id(id),
+          true <- object.data["type"] == "Question",
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+          true <- Visibility.visible_for_user?(activity, user),
+          {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("poll.json", %{object: object, for: user})
+     else
+       nil ->
+         render_error(conn, :not_found, "Record not found")
+       false ->
+         render_error(conn, :not_found, "Record not found")
+       {:error, message} ->
+         conn
+         |> put_status(:unprocessable_entity)
+         |> json(%{error: message})
+     end
+   end
    def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
      with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
        conn
      end
    end
  
-   def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
-       when length(media_ids) > 0 do
-     params =
-       params
-       |> Map.put("status", ".")
-     post_status(conn, params)
-   end
    def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
      params =
        params
        |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
  
-     idempotency_key =
-       case get_req_header(conn, "idempotency-key") do
-         [key] -> key
-         _ -> Ecto.UUID.generate()
-       end
      scheduled_at = params["scheduled_at"]
  
      if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
      else
        params = Map.drop(params, ["scheduled_at"])
  
-       {:ok, activity} =
-         Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
-           CommonAPI.post(user, params)
-         end)
+       case CommonAPI.post(user, params) do
+         {:error, message} ->
+           conn
+           |> put_status(:unprocessable_entity)
+           |> json(%{error: message})
  
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+         {:ok, activity} ->
+           conn
+           |> put_view(StatusView)
+           |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+       end
      end
    end
  
      with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
        json(conn, %{})
      else
-       _e ->
-         conn
-         |> put_status(403)
-         |> json(%{error: "Can't delete this post"})
+       _e -> render_error(conn, :forbidden, "Can't delete this post")
      end
    end
  
        conn
        |> put_view(StatusView)
        |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     else
-       {:error, reason} ->
-         conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
      end
    end
  
        conn
        |> put_view(StatusView)
        |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     else
-       {:error, reason} ->
-         conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
      end
    end
  
      else
        {:error, reason} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => reason}))
+         |> put_status(:forbidden)
+         |> json(%{"error" => reason})
      end
    end
  
      else
        {:error, reason} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => reason}))
+         |> put_status(:forbidden)
+         |> json(%{"error" => reason})
      end
    end
  
          conn
          |> json(rendered)
        else
-         conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
+         render_error(conn, :unsupported_media_type, "mascots can only be images")
        end
      end
    end
    end
  
    def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+     with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
           %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
        q = from(u in User, where: u.ap_id in ^likes)
        users = Repo.all(q)
  
        conn
        |> put_view(AccountView)
-       |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
+       |> render("accounts.json", %{for: user, users: users, as: :user})
      else
        _ -> json(conn, [])
      end
    end
  
    def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+     with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
           %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
        q = from(u in User, where: u.ap_id in ^announces)
        users = Repo.all(q)
      else
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
      else
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
  
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
  
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
      end
    end
  
-   def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
+   def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
+     notifications =
+       if Map.has_key?(params, "notifications"),
+         do: params["notifications"] in [true, "True", "true", "1"],
+         else: true
      with %User{} = muted <- User.get_cached_by_id(id),
-          {:ok, muter} <- User.mute(muter, muted) do
+          {:ok, muter} <- User.mute(muter, muted, notifications) do
        conn
        |> put_view(AccountView)
        |> render("relationship.json", %{user: muter, target: muted})
      else
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
      else
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
      else
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
      else
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
      else
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
+         |> put_status(:forbidden)
+         |> json(%{error: message})
      end
    end
  
      else
        {:error, message} ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(403, Jason.encode!(%{"error" => message}))
-     end
-   end
-   def status_search_query_with_gin(q, query) do
-     from([a, o] in q,
-       where:
-         fragment(
-           "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
-           o.data,
-           ^query
-         ),
-       order_by: [desc: :id]
-     )
-   end
-   def status_search_query_with_rum(q, query) do
-     from([a, o] in q,
-       where:
-         fragment(
-           "? @@ plainto_tsquery('english', ?)",
-           o.fts_content,
-           ^query
-         ),
-       order_by: [fragment("? <=> now()::date", o.inserted_at)]
-     )
-   end
-   def status_search(user, query) do
-     fetched =
-       if Regex.match?(~r/https?:/, query) do
-         with {:ok, object} <- Fetcher.fetch_object_from_id(query),
-              %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
-              true <- Visibility.visible_for_user?(activity, user) do
-           [activity]
-         else
-           _e -> []
-         end
-       end || []
-     q =
-       from([a, o] in Activity.with_preloaded_object(Activity),
-         where: fragment("?->>'type' = 'Create'", a.data),
-         where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
-         limit: 40
-       )
-     q =
-       if Pleroma.Config.get([:database, :rum_enabled]) do
-         status_search_query_with_rum(q, query)
-       else
-         status_search_query_with_gin(q, query)
-       end
-     Repo.all(q) ++ fetched
-   end
-   def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-     accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
-     statuses = status_search(user, query)
-     tags_path = Web.base_url() <> "/tag/"
-     tags =
-       query
-       |> String.split()
-       |> Enum.uniq()
-       |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
-       |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
-       |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
-     res = %{
-       "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
-       "statuses" =>
-         StatusView.render("index.json", activities: statuses, for: user, as: :activity),
-       "hashtags" => tags
-     }
-     json(conn, res)
-   end
-   def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-     accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
-     statuses = status_search(user, query)
-     tags =
-       query
-       |> String.split()
-       |> Enum.uniq()
-       |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
-       |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
-     res = %{
-       "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
-       "statuses" =>
-         StatusView.render("index.json", activities: statuses, for: user, as: :activity),
-       "hashtags" => tags
-     }
-     json(conn, res)
-   end
-   def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-     accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
-     res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
-     json(conn, res)
+         |> put_status(:forbidden)
+         |> json(%{error: message})
+     end
    end
  
    def favourites(%{assigns: %{user: user}} = conn, params) do
        |> put_view(StatusView)
        |> render("index.json", %{activities: activities, for: for_user, as: :activity})
      else
-       nil ->
-         {:error, :not_found}
-       true ->
-         conn
-         |> put_status(403)
-         |> json(%{error: "Can't get favorites"})
+       nil -> {:error, :not_found}
+       true -> render_error(conn, :forbidden, "Can't get favorites")
      end
    end
  
        res = ListView.render("list.json", list: list)
        json(conn, res)
      else
-       _e ->
-         conn
-         |> put_status(404)
-         |> json(%{error: "Record not found"})
+       _e -> render_error(conn, :not_found, "Record not found")
      end
    end
  
        json(conn, %{})
      else
        _e ->
-         json(conn, "error")
+         json(conn, dgettext("errors", "error"))
      end
    end
  
        json(conn, res)
      else
        _e ->
-         json(conn, "error")
+         json(conn, dgettext("errors", "error"))
      end
    end
  
        |> put_view(StatusView)
        |> render("index.json", %{activities: activities, for: user, as: :activity})
      else
-       _e ->
-         conn
-         |> put_status(403)
-         |> json(%{error: "Error."})
+       _e -> render_error(conn, :forbidden, "Error.")
      end
    end
  
        accounts =
          Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
  
-       flavour = get_user_flavour(user)
        initial_state =
          %{
            meta: %{
              max_toot_chars: limit,
              mascot: User.get_mascot(user)["url"]
            },
+           poll_limits: Config.get([:instance, :poll_limits]),
            rights: %{
              delete_others_notice: present?(user.info.is_moderator),
              admin: present?(user.info.is_admin)
        conn
        |> put_layout(false)
        |> put_view(MastodonView)
-       |> render("index.html", %{initial_state: initial_state, flavour: flavour})
+       |> render("index.html", %{initial_state: initial_state})
      else
        conn
        |> put_session(:return_to, conn.request_path)
      else
        e ->
          conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
+         |> put_status(:internal_server_error)
+         |> json(%{error: inspect(e)})
      end
    end
  
-   @supported_flavours ["glitch", "vanilla"]
-   def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
-       when flavour in @supported_flavours do
-     flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
-     with changeset <- Ecto.Changeset.change(user),
-          changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
-          {:ok, user} <- User.update_and_set_cache(changeset),
-          flavour <- user.info.flavour do
-       json(conn, flavour)
-     else
-       e ->
-         conn
-         |> put_resp_content_type("application/json")
-         |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
-     end
-   end
-   def set_flavour(conn, _params) do
-     conn
-     |> put_status(400)
-     |> json(%{error: "Unsupported flavour"})
-   end
-   def get_flavour(%{assigns: %{user: user}} = conn, _params) do
-     json(conn, get_user_flavour(user))
-   end
-   defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
-     flavour
-   end
-   defp get_user_flavour(_) do
-     "glitch"
-   end
    def login(%{assigns: %{user: %User{}}} = conn, _params) do
      redirect(conn, to: local_mastodon_root_path(conn))
    end
        |> Enum.map_join(", ", fn {_k, v} -> v end)
  
      conn
-     |> put_status(422)
+     |> put_status(:unprocessable_entity)
      |> json(%{error: error_message})
    end
  
    def errors(conn, {:error, :not_found}) do
+     render_error(conn, :not_found, "Record not found")
+   end
+   def errors(conn, {:error, error_message}) do
      conn
-     |> put_status(404)
-     |> json(%{error: "Record not found"})
+     |> put_status(:bad_request)
+     |> json(%{error: error_message})
    end
  
    def errors(conn, _) do
      conn
-     |> put_status(500)
-     |> json("Something went wrong")
+     |> put_status(:internal_server_error)
+     |> json(dgettext("errors", "Something went wrong"))
    end
  
    def suggestions(%{assigns: %{user: user}} = conn, _) do
      else
        {:error, errors} ->
          conn
-         |> put_status(400)
-         |> json(Jason.encode!(errors))
+         |> put_status(:bad_request)
+         |> json(errors)
      end
    end
  
    def account_register(%{assigns: %{app: _app}} = conn, _params) do
-     conn
-     |> put_status(400)
-     |> json(%{error: "Missing parameters"})
+     render_error(conn, :bad_request, "Missing parameters")
    end
  
    def account_register(conn, _) do
-     conn
-     |> put_status(403)
-     |> json(%{error: "Invalid credentials"})
+     render_error(conn, :forbidden, "Invalid credentials")
    end
  
    def conversations(%{assigns: %{user: user}} = conn, params) do
  
    def try_render(conn, target, params)
        when is_binary(target) do
-     res = render(conn, target, params)
-     if res == nil do
-       conn
-       |> put_status(501)
-       |> json(%{error: "Can't display this activity"})
-     else
-       res
+     case render(conn, target, params) do
+       nil -> render_error(conn, :not_implemented, "Can't display this activity")
+       res -> res
      end
    end
  
    def try_render(conn, _, _) do
-     conn
-     |> put_status(501)
-     |> json(%{error: "Can't display this activity"})
+     render_error(conn, :not_implemented, "Can't display this activity")
    end
  
    defp present?(nil), do: false