Merge branch 'develop' into 'remove-avatar-header'
authorSachin Joshi <satchin.joshi@gmail.com>
Sun, 23 Jun 2019 03:25:50 +0000 (03:25 +0000)
committerSachin Joshi <satchin.joshi@gmail.com>
Sun, 23 Jun 2019 03:25:50 +0000 (03:25 +0000)
# Conflicts:
#   CHANGELOG.md

1  2 
CHANGELOG.md
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
test/web/mastodon_api/mastodon_api_controller_test.exs
test/web/twitter_api/twitter_api_controller_test.exs

diff --combined CHANGELOG.md
index feacf2c5e570cf276dad58b0fbbbc905f66a4f41,0dc8b547d755bbefe05498c6e5028b01aafecd48..846d0102cfeb870b63529937300c54782b848c11
@@@ -4,11 -4,16 +4,16 @@@ All notable changes to this project wil
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
  
  ## [unreleased]
+ ### Security
+ - Mastodon API: Fix display names not being sanitized
  ### Added
+ - Add a generic settings store for frontends / clients to use.
+ - Explicit addressing option for posting.
  - Optional SSH access mode. (Needs `erlang-ssh` package on some distributions).
  - [MongooseIM](https://github.com/esl/MongooseIM) http authentication support.
  - LDAP authentication
  - External OAuth provider authentication
+ - Support for building a release using [`mix release`](https://hexdocs.pm/mix/master/Mix.Tasks.Release.html)
  - A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
  - [Prometheus](https://prometheus.io/) metrics
  - Support for Mastodon's remote interaction
  - Mix Tasks: `mix pleroma.database remove_embedded_objects`
  - Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
  - Mix Tasks: `mix pleroma.user toggle_confirmed`
+ - Mix Tasks: `mix pleroma.config migrate_to_db`
+ - Mix Tasks: `mix pleroma.config migrate_from_db`
+ - Federation: Support for `Question` and `Answer` objects
  - Federation: Support for reports
+ - Configuration: `poll_limits` option
  - Configuration: `safe_dm_mentions` option
  - Configuration: `link_name` option
  - Configuration: `fetch_initial_posts` option
  - Configuration: `notify_email` option
  - Configuration: Media proxy `whitelist` option
  - Configuration: `report_uri` option
+ - Configuration: `limit_to_local_content` option
  - Pleroma API: User subscriptions
  - Pleroma API: Healthcheck endpoint
  - Pleroma API: `/api/v1/pleroma/mascot` per-user frontend mascot configuration endpoints
  - Admin API: added filters (role, tags, email, name) for users endpoint
  - Admin API: Endpoints for managing reports
  - Admin API: Endpoints for deleting and changing the scope of individual reported statuses
+ - Admin API: Endpoints to view and change config settings.
  - AdminFE: initial release with basic user management accessible at /pleroma/admin/
+ - Mastodon API: Add chat token to `verify_credentials` response
+ - Mastodon API: Add background image setting to `update_credentials`
  - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
  - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
  - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
  - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
  - Mastodon API: `POST /api/v1/accounts` (account creation API)
+ - Mastodon API: [Polls](https://docs.joinmastodon.org/api/rest/polls/)
  - ActivityPub C2S: OAuth endpoints
  - Metadata: RelMe provider
  - OAuth: added support for refresh tokens
  - OAuth: added job to clean expired access tokens
  - MRF: Support for rejecting reports from specific instances (`mrf_simple`)
  - MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`)
 +- Ability to reset avatar, profile banner and backgroud
+ - MRF: Support for running subchains.
+ - Configuration: `skip_thread_containment` option
+ - Configuration: `rate_limit` option. See `Pleroma.Plugs.RateLimiter` documentation for details.
+ - MRF: Support for filtering out likely spam messages by rejecting posts from new users that contain links.
  
  ### Changed
  - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
+ - Thread containment / test for complete visibility will be skipped by default.
  - Enforcement of OAuth scopes
  - Add multiple use/time expiring invite token
  - Restyled OAuth pages to fit with Pleroma's default theme
@@@ -57,6 -75,7 +76,7 @@@
  - Federation: Expand the audience of delete activities to all recipients of the deleted object
  - Federation: Removed `inReplyToStatusId` from objects
  - Configuration: Dedupe enabled by default
+ - Configuration: Default log level in `prod` environment is now set to `warn`
  - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
  - Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
  - Admin API: Move the user related API to `api/pleroma/admin/users`
  - Respond with a 404 Not implemented JSON error message when requested API is not implemented
  
  ### Fixed
+ - Follow requests don't get 'stuck' anymore.
  - Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended.
  - Followers counter not being updated when a follower is blocked
  - Deactivated users being able to request an access token
  - Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
  - Mastodon API: Exposing default scope of the user to anyone
  - Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`]
+ - Mastodon API: Replace missing non-nullable Card attributes with empty strings
  - User-Agent is now sent correctly for all HTTP requests.
+ - MRF: Simple policy now properly delists imported or relayed statuses
  
  ## Removed
  - Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations`
  
+ ## [0.9.99999] - 2019-05-31
+ ### Security
+ - Mastodon API: Fix lists leaking private posts
  ## [0.9.9999] - 2019-04-05
  ### Security
  - Mastodon API: Fix content warnings skipping HTML sanitization
index 1ff839e9e26a02bcb01db7a49e4b277bfed8d136,0c22790f27d739567f86d71e85a02fea7318ba03..d6aacd288d8914e37a86d6b8aa156077ba18c0f9
@@@ -11,9 -11,9 +11,9 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
    alias Pleroma.Conversation.Participation
    alias Pleroma.Filter
    alias Pleroma.Formatter
+   alias Pleroma.HTTP
    alias Pleroma.Notification
    alias Pleroma.Object
-   alias Pleroma.Object.Fetcher
    alias Pleroma.Pagination
    alias Pleroma.Repo
    alias Pleroma.ScheduledActivity
  
    require Logger
  
-   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]
-   )
+   plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
+   plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
  
-   @httpoison Application.get_env(:pleroma, :httpoison)
    @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
      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
  
        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)
      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 ->
+         conn
+         |> put_status(404)
+         |> json(%{error: "Record not found"})
+       false ->
+         conn
+         |> put_status(404)
+         |> json(%{error: "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 ->
+         conn
+         |> put_status(404)
+         |> json(%{error: "Record not found"})
+       false ->
+         conn
+         |> put_status(404)
+         |> json(%{error: "Record not found"})
+       {:error, message} ->
+         conn
+         |> put_status(422)
+         |> 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)
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+       case get_cached_status_or_post(conn, params) do
+         {:ignore, message} ->
+           conn
+           |> put_status(422)
+           |> json(%{error: message})
+         {:error, message} ->
+           conn
+           |> put_status(422)
+           |> json(%{error: message})
+         {_, activity} ->
+           conn
+           |> put_view(StatusView)
+           |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+       end
      end
    end
  
+   defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
+     idempotency_key =
+       case get_req_header(conn, "idempotency-key") do
+         [key] -> key
+         _ -> Ecto.UUID.generate()
+       end
+     Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
+       case CommonAPI.post(user, params) do
+         {:ok, activity} -> activity
+         {:error, message} -> {:ignore, message}
+       end
+     end)
+   end
    def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
      with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
        json(conn, %{})
      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: 20
-       )
-     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)
-   end
    def favourites(%{assigns: %{user: user}} = conn, params) do
      params =
        params
        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)
      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
          |> String.replace("{{user}}", user)
  
        with {:ok, %{status: 200, body: body}} <-
-              @httpoison.get(
+              HTTP.get(
                 url,
                 [],
                 adapter: [
index 42ef64c4f2f6051640cba81c9bcc63c133ca721e,837153ed452547690dfccdf6ba04fa6ec7bb12e3..36458b2f491601e8be4c48acf292a551023498a5
@@@ -202,6 -202,9 +202,9 @@@ defmodule Pleroma.Web.Router d
  
      put("/statuses/:id", AdminAPIController, :status_update)
      delete("/statuses/:id", AdminAPIController, :status_delete)
+     get("/config", AdminAPIController, :config_show)
+     post("/config", AdminAPIController, :config_update)
    end
  
    scope "/", Pleroma.Web.TwitterAPI do
        post("/conversations/:id/read", MastodonAPIController, :conversation_read)
  
        get("/endorsements", MastodonAPIController, :empty_array)
-       get("/pleroma/flavour", MastodonAPIController, :get_flavour)
      end
  
      scope [] do
  
        patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
  
 +      patch("/accounts/update_avatar", MastodonAPIController, :update_avatar)
 +      patch("/accounts/update_banner", MastodonAPIController, :update_banner)
 +      patch("/accounts/update_background", MastodonAPIController, :update_background)
 +
        post("/statuses", MastodonAPIController, :post_status)
        delete("/statuses/:id", MastodonAPIController, :delete_status)
  
        put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
        delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
  
+       post("/polls/:id/votes", MastodonAPIController, :poll_vote)
        post("/media", MastodonAPIController, :upload)
        put("/media/:id", MastodonAPIController, :update_media)
  
        put("/filters/:id", MastodonAPIController, :update_filter)
        delete("/filters/:id", MastodonAPIController, :delete_filter)
  
-       post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour)
        get("/pleroma/mascot", MastodonAPIController, :get_mascot)
        put("/pleroma/mascot", MastodonAPIController, :set_mascot)
  
  
      get("/trends", MastodonAPIController, :empty_array)
  
-     get("/accounts/search", MastodonAPIController, :account_search)
+     get("/accounts/search", SearchController, :account_search)
  
      scope [] do
        pipe_through(:oauth_read_or_public)
        get("/statuses/:id", MastodonAPIController, :get_status)
        get("/statuses/:id/context", MastodonAPIController, :get_context)
  
+       get("/polls/:id", MastodonAPIController, :get_poll)
        get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
        get("/accounts/:id/followers", MastodonAPIController, :followers)
        get("/accounts/:id/following", MastodonAPIController, :following)
        get("/accounts/:id", MastodonAPIController, :user)
  
-       get("/search", MastodonAPIController, :search)
+       get("/search", SearchController, :search)
  
        get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
      end
  
    scope "/api/v2", Pleroma.Web.MastodonAPI do
      pipe_through([:api, :oauth_read_or_public])
-     get("/search", MastodonAPIController, :search2)
+     get("/search", SearchController, :search2)
    end
  
    scope "/api", Pleroma.Web do
      post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
    end
  
-   scope "/", Pleroma.Web do
-     pipe_through(:oembed)
-     get("/oembed", OEmbed.OEmbedController, :url)
-   end
    pipeline :activitypub do
      plug(:accepts, ["activity+json", "json"])
      plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
      get("/:sig/:url/:filename", MediaProxyController, :remote)
    end
  
-   if Mix.env() == :dev do
+   if Pleroma.Config.get(:env) == :dev do
      scope "/dev" do
        pipe_through([:mailbox_preview])
  
index a796e38fdbd33acfc23d54512de4a0dfc63c4cb7,6cf107d172c48164f14600d9df0f5ed14bf64a84..45ef7be3d277bdc14f7468012ad3ad5f28794e92
@@@ -456,16 -456,6 +456,16 @@@ defmodule Pleroma.Web.TwitterAPI.Contro
      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)
 +
 +    conn
 +    |> put_view(UserView)
 +    |> render("show.json", %{user: user, for: user})
 +  end
 +
    def update_avatar(%{assigns: %{user: user}} = conn, params) do
      {:ok, object} = ActivityPub.upload(params, type: :avatar)
      change = Changeset.change(user, %{avatar: object.data})
      |> render("show.json", %{user: user, for: user})
    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)
 +      response = %{url: nil} |> Jason.encode!()
 +
 +      conn
 +      |> json_reply(200, response)
 +    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},
      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
 +      response = %{url: nil} |> Jason.encode!()
 +
 +      conn
 +      |> json_reply(200, response)
 +    end
 +  end
 +
    def update_background(%{assigns: %{user: user}} = conn, params) do
      with {:ok, object} <- ActivityPub.upload(params, type: :background),
           new_info <- %{"background" => object.data},
  
    defp build_info_cng(user, params) do
      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, res ->
          if value = params[key] do
            Map.put(res, key, value == "true")
    def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
  
    def only_if_public_instance(conn, _) do
-     if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
+     if Pleroma.Config.get([:instance, :public]) do
        conn
      else
        conn
index 90ca26441235e675a9c4b65b0d87f43c288f30f2,707723421fab1e06d46ceddd55118727f4f2c1a8..de157d5297878ef53b6e9404e3e43e88a59b9d29
@@@ -24,8 -24,6 +24,8 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
    import ExUnit.CaptureLog
    import Tesla.Mock
  
 +  @image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
 +
    setup do
      mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
      :ok
      refute id == third_id
    end
  
+   describe "posting polls" do
+     test "posting a poll", %{conn: conn} do
+       user = insert(:user)
+       time = NaiveDateTime.utc_now()
+       conn =
+         conn
+         |> assign(:user, user)
+         |> post("/api/v1/statuses", %{
+           "status" => "Who is the #bestgrill?",
+           "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
+         })
+       response = json_response(conn, 200)
+       assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
+                title in ["Rei", "Asuka", "Misato"]
+              end)
+       assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
+       refute response["poll"]["expred"]
+     end
+     test "option limit is enforced", %{conn: conn} do
+       user = insert(:user)
+       limit = Pleroma.Config.get([:instance, :poll_limits, :max_options])
+       conn =
+         conn
+         |> assign(:user, user)
+         |> post("/api/v1/statuses", %{
+           "status" => "desu~",
+           "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
+         })
+       %{"error" => error} = json_response(conn, 422)
+       assert error == "Poll can't contain more than #{limit} options"
+     end
+     test "option character limit is enforced", %{conn: conn} do
+       user = insert(:user)
+       limit = Pleroma.Config.get([:instance, :poll_limits, :max_option_chars])
+       conn =
+         conn
+         |> assign(:user, user)
+         |> post("/api/v1/statuses", %{
+           "status" => "...",
+           "poll" => %{
+             "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
+             "expires_in" => 1
+           }
+         })
+       %{"error" => error} = json_response(conn, 422)
+       assert error == "Poll options cannot be longer than #{limit} characters each"
+     end
+     test "minimal date limit is enforced", %{conn: conn} do
+       user = insert(:user)
+       limit = Pleroma.Config.get([:instance, :poll_limits, :min_expiration])
+       conn =
+         conn
+         |> assign(:user, user)
+         |> post("/api/v1/statuses", %{
+           "status" => "imagine arbitrary limits",
+           "poll" => %{
+             "options" => ["this post was made by pleroma gang"],
+             "expires_in" => limit - 1
+           }
+         })
+       %{"error" => error} = json_response(conn, 422)
+       assert error == "Expiration date is too soon"
+     end
+     test "maximum date limit is enforced", %{conn: conn} do
+       user = insert(:user)
+       limit = Pleroma.Config.get([:instance, :poll_limits, :max_expiration])
+       conn =
+         conn
+         |> assign(:user, user)
+         |> post("/api/v1/statuses", %{
+           "status" => "imagine arbitrary limits",
+           "poll" => %{
+             "options" => ["this post was made by pleroma gang"],
+             "expires_in" => limit + 1
+           }
+         })
+       %{"error" => error} = json_response(conn, 422)
+       assert error == "Expiration date is too far in the future"
+     end
+   end
    test "posting a sensitive status", %{conn: conn} do
      user = insert(:user)
  
    test "Conversations", %{conn: conn} do
      user_one = insert(:user)
      user_two = insert(:user)
+     user_three = insert(:user)
  
      {:ok, user_two} = User.follow(user_two, user_one)
  
      {:ok, direct} =
        CommonAPI.post(user_one, %{
-         "status" => "Hi @#{user_two.nickname}!",
+         "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
          "visibility" => "direct"
        })
  
               }
             ] = response
  
+     account_ids = Enum.map(res_accounts, & &1["id"])
      assert length(res_accounts) == 2
+     assert user_two.id in account_ids
+     assert user_three.id in account_ids
      assert is_binary(res_id)
      assert unread == true
      assert res_last_status["id"] == direct.id
        |> assign(:user, user)
        |> get("/api/v1/accounts/verify_credentials")
  
-     assert %{"id" => id, "source" => %{"privacy" => "public"}} = json_response(conn, 200)
+     response = json_response(conn, 200)
+     assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
+     assert response["pleroma"]["chat_token"]
      assert id == to_string(user.id)
    end
  
      assert expected == json_response(conn, 200)
    end
  
 +  test "user avatar can be set", %{conn: conn} do
 +    user = insert(:user)
 +    avatar_image = File.read!("test/fixtures/avatar_data_uri")
 +
 +    conn =
 +      conn
 +      |> assign(:user, user)
 +      |> patch("/api/v1/accounts/update_avatar", %{img: avatar_image})
 +
 +    user = refresh_record(user)
 +
 +    assert %{
 +             "name" => _,
 +             "type" => _,
 +             "url" => [
 +               %{
 +                 "href" => _,
 +                 "mediaType" => _,
 +                 "type" => _
 +               }
 +             ]
 +           } = user.avatar
 +
 +    assert %{"url" => _} = json_response(conn, 200)
 +  end
 +
 +  test "user avatar can be reset", %{conn: conn} do
 +    user = insert(:user)
 +
 +    conn =
 +      conn
 +      |> assign(:user, user)
 +      |> patch("/api/v1/accounts/update_avatar", %{img: ""})
 +
 +    user = User.get_cached_by_id(user.id)
 +
 +    assert user.avatar == nil
 +
 +    assert %{"url" => nil} = json_response(conn, 200)
 +  end
 +
 +  test "can set profile banner", %{conn: conn} do
 +    user = insert(:user)
 +
 +    conn =
 +      conn
 +      |> assign(:user, user)
 +      |> patch("/api/v1/accounts/update_banner", %{"banner" => @image})
 +
 +    user = refresh_record(user)
 +    assert user.info.banner["type"] == "Image"
 +
 +    assert %{"url" => _} = json_response(conn, 200)
 +  end
 +
 +  test "can reset profile banner", %{conn: conn} do
 +    user = insert(:user)
 +
 +    conn =
 +      conn
 +      |> assign(:user, user)
 +      |> patch("/api/v1/accounts/update_banner", %{"banner" => ""})
 +
 +    user = refresh_record(user)
 +    assert user.info.banner == %{}
 +
 +    assert %{"url" => nil} = json_response(conn, 200)
 +  end
 +
 +  test "background image can be set", %{conn: conn} do
 +    user = insert(:user)
 +
 +    conn =
 +      conn
 +      |> assign(:user, user)
 +      |> patch("/api/v1/accounts/update_background", %{"img" => @image})
 +
 +    user = refresh_record(user)
 +    assert user.info.background["type"] == "Image"
 +    assert %{"url" => _} = json_response(conn, 200)
 +  end
 +
 +  test "background image can be reset", %{conn: conn} do
 +    user = insert(:user)
 +
 +    conn =
 +      conn
 +      |> assign(:user, user)
 +      |> patch("/api/v1/accounts/update_background", %{"img" => ""})
 +
 +    user = refresh_record(user)
 +    assert user.info.background == %{}
 +    assert %{"url" => nil} = json_response(conn, 200)
 +  end
 +
    test "creates an oauth app", %{conn: conn} do
      user = insert(:user)
      app_attrs = build(:oauth_app)
      end
    end
  
+   describe "media upload" do
+     setup do
+       upload_config = Pleroma.Config.get([Pleroma.Upload])
+       proxy_config = Pleroma.Config.get([:media_proxy])
+       on_exit(fn ->
+         Pleroma.Config.put([Pleroma.Upload], upload_config)
+         Pleroma.Config.put([:media_proxy], proxy_config)
+       end)
+       user = insert(:user)
+       conn =
+         build_conn()
+         |> assign(:user, user)
+       image = %Plug.Upload{
+         content_type: "image/jpg",
+         path: Path.absname("test/fixtures/image.jpg"),
+         filename: "an_image.jpg"
+       }
+       [conn: conn, image: image]
+     end
+     test "returns uploaded image", %{conn: conn, image: image} do
+       desc = "Description of the image"
+       media =
+         conn
+         |> post("/api/v1/media", %{"file" => image, "description" => desc})
+         |> json_response(:ok)
+       assert media["type"] == "image"
+       assert media["description"] == desc
+       assert media["id"]
+       object = Repo.get(Object, media["id"])
+       assert object.data["actor"] == User.ap_id(conn.assigns[:user])
+     end
+     test "returns proxied url when media proxy is enabled", %{conn: conn, image: image} do
+       Pleroma.Config.put([Pleroma.Upload, :base_url], "https://media.pleroma.social")
+       proxy_url = "https://cache.pleroma.social"
+       Pleroma.Config.put([:media_proxy, :enabled], true)
+       Pleroma.Config.put([:media_proxy, :base_url], proxy_url)
+       media =
+         conn
+         |> post("/api/v1/media", %{"file" => image})
+         |> json_response(:ok)
+       assert String.starts_with?(media["url"], proxy_url)
+     end
+     test "returns media url when proxy is enabled but media url is whitelisted", %{
+       conn: conn,
+       image: image
+     } do
+       media_url = "https://media.pleroma.social"
+       Pleroma.Config.put([Pleroma.Upload, :base_url], media_url)
+       Pleroma.Config.put([:media_proxy, :enabled], true)
+       Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
+       Pleroma.Config.put([:media_proxy, :whitelist], ["media.pleroma.social"])
+       media =
+         conn
+         |> post("/api/v1/media", %{"file" => image})
+         |> json_response(:ok)
+       assert String.starts_with?(media["url"], media_url)
+     end
+   end
    describe "locked accounts" do
      test "/api/v1/follow_requests works" do
        user = insert(:user, %{info: %User.Info{locked: true}})
      assert id == user.id
    end
  
-   test "media upload", %{conn: conn} do
-     file = %Plug.Upload{
-       content_type: "image/jpg",
-       path: Path.absname("test/fixtures/image.jpg"),
-       filename: "an_image.jpg"
-     }
-     desc = "Description of the image"
-     user = insert(:user)
-     conn =
-       conn
-       |> assign(:user, user)
-       |> post("/api/v1/media", %{"file" => file, "description" => desc})
-     assert media = json_response(conn, 200)
-     assert media["type"] == "image"
-     assert media["description"] == desc
-     assert media["id"]
-     object = Repo.get(Object, media["id"])
-     assert object.data["actor"] == User.ap_id(user)
-   end
    test "mascot upload", %{conn: conn} do
      user = insert(:user)
  
      end)
    end
  
-   test "account search", %{conn: conn} do
-     user = insert(:user)
-     user_two = insert(:user, %{nickname: "shp@shitposter.club"})
-     user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
-     results =
-       conn
-       |> assign(:user, user)
-       |> get("/api/v1/accounts/search", %{"q" => "shp"})
-       |> json_response(200)
-     result_ids = for result <- results, do: result["acct"]
-     assert user_two.nickname in result_ids
-     assert user_three.nickname in result_ids
-     results =
-       conn
-       |> assign(:user, user)
-       |> get("/api/v1/accounts/search", %{"q" => "2hu"})
-       |> json_response(200)
-     result_ids = for result <- results, do: result["acct"]
-     assert user_three.nickname in result_ids
-   end
-   test "search", %{conn: conn} do
-     user = insert(:user)
-     user_two = insert(:user, %{nickname: "shp@shitposter.club"})
-     user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
-     {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
-     {:ok, _activity} =
-       CommonAPI.post(user, %{
-         "status" => "This is about 2hu, but private",
-         "visibility" => "private"
-       })
-     {:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
-     conn =
-       conn
-       |> get("/api/v1/search", %{"q" => "2hu"})
-     assert results = json_response(conn, 200)
-     [account | _] = results["accounts"]
-     assert account["id"] == to_string(user_three.id)
-     assert results["hashtags"] == []
-     [status] = results["statuses"]
-     assert status["id"] == to_string(activity.id)
-   end
-   test "search fetches remote statuses", %{conn: conn} do
-     capture_log(fn ->
-       conn =
-         conn
-         |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
-       assert results = json_response(conn, 200)
-       [status] = results["statuses"]
-       assert status["uri"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
-     end)
-   end
-   test "search doesn't show statuses that it shouldn't", %{conn: conn} do
-     {:ok, activity} =
-       CommonAPI.post(insert(:user), %{
-         "status" => "This is about 2hu, but private",
-         "visibility" => "private"
-       })
-     capture_log(fn ->
-       conn =
-         conn
-         |> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]})
-       assert results = json_response(conn, 200)
-       [] = results["statuses"]
-     end)
-   end
-   test "search fetches remote accounts", %{conn: conn} do
-     conn =
-       conn
-       |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"})
-     assert results = json_response(conn, 200)
-     [account] = results["accounts"]
-     assert account["acct"] == "shp@social.heldscal.la"
-   end
    test "returns the favorites of a user", %{conn: conn} do
      user = insert(:user)
      other_user = insert(:user)
      end
    end
  
-   describe "updating credentials" do
-     test "updates the user's bio", %{conn: conn} do
-       user = insert(:user)
-       user2 = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{
-           "note" => "I drink #cofe with @#{user2.nickname}"
-         })
-       assert user = json_response(conn, 200)
-       assert user["note"] ==
-                ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag">#cofe</a> with <span class="h-card"><a data-user=") <>
-                  user2.id <>
-                  ~s(" class="u-url mention" href=") <>
-                  user2.ap_id <> ~s(">@<span>) <> user2.nickname <> ~s(</span></a></span>)
-     end
-     test "updates the user's locking status", %{conn: conn} do
-       user = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{locked: "true"})
-       assert user = json_response(conn, 200)
-       assert user["locked"] == true
-     end
-     test "updates the user's default scope", %{conn: conn} do
-       user = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
-       assert user = json_response(conn, 200)
-       assert user["source"]["privacy"] == "cofe"
-     end
-     test "updates the user's hide_followers status", %{conn: conn} do
-       user = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"})
-       assert user = json_response(conn, 200)
-       assert user["pleroma"]["hide_followers"] == true
-     end
-     test "updates the user's hide_follows status", %{conn: conn} do
-       user = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"})
-       assert user = json_response(conn, 200)
-       assert user["pleroma"]["hide_follows"] == true
-     end
-     test "updates the user's hide_favorites status", %{conn: conn} do
-       user = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
-       assert user = json_response(conn, 200)
-       assert user["pleroma"]["hide_favorites"] == true
-     end
-     test "updates the user's show_role status", %{conn: conn} do
-       user = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{show_role: "false"})
-       assert user = json_response(conn, 200)
-       assert user["source"]["pleroma"]["show_role"] == false
-     end
-     test "updates the user's no_rich_text status", %{conn: conn} do
-       user = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
-       assert user = json_response(conn, 200)
-       assert user["source"]["pleroma"]["no_rich_text"] == true
-     end
-     test "updates the user's name", %{conn: conn} do
-       user = insert(:user)
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
-       assert user = json_response(conn, 200)
-       assert user["display_name"] == "markorepairs"
-     end
-     test "updates the user's avatar", %{conn: conn} do
-       user = insert(:user)
-       new_avatar = %Plug.Upload{
-         content_type: "image/jpg",
-         path: Path.absname("test/fixtures/image.jpg"),
-         filename: "an_image.jpg"
-       }
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
-       assert user_response = json_response(conn, 200)
-       assert user_response["avatar"] != User.avatar_url(user)
-     end
-     test "updates the user's banner", %{conn: conn} do
-       user = insert(:user)
-       new_header = %Plug.Upload{
-         content_type: "image/jpg",
-         path: Path.absname("test/fixtures/image.jpg"),
-         filename: "an_image.jpg"
-       }
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
-       assert user_response = json_response(conn, 200)
-       assert user_response["header"] != User.banner_url(user)
-     end
-     test "requires 'write' permission", %{conn: conn} do
-       token1 = insert(:oauth_token, scopes: ["read"])
-       token2 = insert(:oauth_token, scopes: ["write", "follow"])
-       for token <- [token1, token2] do
-         conn =
-           conn
-           |> put_req_header("authorization", "Bearer #{token.token}")
-           |> patch("/api/v1/accounts/update_credentials", %{})
-         if token == token1 do
-           assert %{"error" => "Insufficient permissions: write."} == json_response(conn, 403)
-         else
-           assert json_response(conn, 200)
-         end
-       end
-     end
-     test "updates profile emojos", %{conn: conn} do
-       user = insert(:user)
-       note = "*sips :blank:*"
-       name = "I am :firefox:"
-       conn =
-         conn
-         |> assign(:user, user)
-         |> patch("/api/v1/accounts/update_credentials", %{
-           "note" => note,
-           "display_name" => name
-         })
-       assert json_response(conn, 200)
-       conn =
-         conn
-         |> get("/api/v1/accounts/#{user.id}")
-       assert user = json_response(conn, 200)
-       assert user["note"] == note
-       assert user["display_name"] == name
-       assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"]
-     end
-   end
    test "get instance information", %{conn: conn} do
      conn = get(conn, "/api/v1/instance")
      assert result = json_response(conn, 200)
               "stats" => _,
               "thumbnail" => _,
               "languages" => _,
-              "registrations" => _
+              "registrations" => _,
+              "poll_limits" => _
             } = result
  
      assert email == from_config_email
                 |> post("/api/v1/statuses/#{activity_two.id}/pin")
                 |> json_response(400)
      end
+   end
  
-     test "Status rich-media Card", %{conn: conn, user: user} do
+   describe "cards" do
+     setup do
        Pleroma.Config.put([:rich_media, :enabled], true)
+       on_exit(fn ->
+         Pleroma.Config.put([:rich_media, :enabled], false)
+       end)
+       user = insert(:user)
+       %{user: user}
+     end
+     test "returns rich-media card", %{conn: conn, user: user} do
        {:ok, activity} = CommonAPI.post(user, %{"status" => "http://example.com/ogp"})
  
+       card_data = %{
+         "image" => "http://ia.media-imdb.com/images/rock.jpg",
+         "provider_name" => "www.imdb.com",
+         "provider_url" => "http://www.imdb.com",
+         "title" => "The Rock",
+         "type" => "link",
+         "url" => "http://www.imdb.com/title/tt0117500/",
+         "description" =>
+           "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
+         "pleroma" => %{
+           "opengraph" => %{
+             "image" => "http://ia.media-imdb.com/images/rock.jpg",
+             "title" => "The Rock",
+             "type" => "video.movie",
+             "url" => "http://www.imdb.com/title/tt0117500/",
+             "description" =>
+               "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
+           }
+         }
+       }
        response =
          conn
          |> get("/api/v1/statuses/#{activity.id}/card")
          |> json_response(200)
  
-       assert response == %{
-                "image" => "http://ia.media-imdb.com/images/rock.jpg",
-                "provider_name" => "www.imdb.com",
-                "provider_url" => "http://www.imdb.com",
-                "title" => "The Rock",
-                "type" => "link",
-                "url" => "http://www.imdb.com/title/tt0117500/",
-                "description" => nil,
-                "pleroma" => %{
-                  "opengraph" => %{
-                    "image" => "http://ia.media-imdb.com/images/rock.jpg",
-                    "title" => "The Rock",
-                    "type" => "video.movie",
-                    "url" => "http://www.imdb.com/title/tt0117500/"
-                  }
-                }
-              }
+       assert response == card_data
  
        # works with private posts
        {:ok, activity} =
          |> get("/api/v1/statuses/#{activity.id}/card")
          |> json_response(200)
  
-       assert response_two == response
+       assert response_two == card_data
+     end
+     test "replaces missing description with an empty string", %{conn: conn, user: user} do
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "http://example.com/ogp-missing-data"})
  
-       Pleroma.Config.put([:rich_media, :enabled], false)
+       response =
+         conn
+         |> get("/api/v1/statuses/#{activity.id}/card")
+         |> json_response(:ok)
+       assert response == %{
+                "type" => "link",
+                "title" => "Pleroma",
+                "description" => "",
+                "image" => nil,
+                "provider_name" => "pleroma.social",
+                "provider_url" => "https://pleroma.social",
+                "url" => "https://pleroma.social/",
+                "pleroma" => %{
+                  "opengraph" => %{
+                    "title" => "Pleroma",
+                    "type" => "website",
+                    "url" => "https://pleroma.social/"
+                  }
+                }
+              }
      end
    end
  
      end
    end
  
-   test "flavours switching (Pleroma Extension)", %{conn: conn} do
-     user = insert(:user)
-     get_old_flavour =
-       conn
-       |> assign(:user, user)
-       |> get("/api/v1/pleroma/flavour")
-     assert "glitch" == json_response(get_old_flavour, 200)
-     set_flavour =
-       conn
-       |> assign(:user, user)
-       |> post("/api/v1/pleroma/flavour/vanilla")
-     assert "vanilla" == json_response(set_flavour, 200)
-     get_new_flavour =
-       conn
-       |> assign(:user, user)
-       |> post("/api/v1/pleroma/flavour/vanilla")
-     assert json_response(set_flavour, 200) == json_response(get_new_flavour, 200)
-   end
    describe "reports" do
      setup do
        reporter = insert(:user)
    end
  
    describe "create account by app" do
-     setup do
-       enabled = Pleroma.Config.get([:app_account_creation, :enabled])
-       max_requests = Pleroma.Config.get([:app_account_creation, :max_requests])
-       interval = Pleroma.Config.get([:app_account_creation, :interval])
-       Pleroma.Config.put([:app_account_creation, :enabled], true)
-       Pleroma.Config.put([:app_account_creation, :max_requests], 5)
-       Pleroma.Config.put([:app_account_creation, :interval], 1)
-       on_exit(fn ->
-         Pleroma.Config.put([:app_account_creation, :enabled], enabled)
-         Pleroma.Config.put([:app_account_creation, :max_requests], max_requests)
-         Pleroma.Config.put([:app_account_creation, :interval], interval)
-       end)
-       :ok
-     end
      test "Account registration via Application", %{conn: conn} do
        conn =
          conn
            agreement: true
          })
  
-       assert json_response(conn, 403) == %{"error" => "Rate limit exceeded."}
+       assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
+     end
+   end
+   describe "GET /api/v1/polls/:id" do
+     test "returns poll entity for object id", %{conn: conn} do
+       user = insert(:user)
+       {:ok, activity} =
+         CommonAPI.post(user, %{
+           "status" => "Pleroma does",
+           "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
+         })
+       object = Object.normalize(activity)
+       conn =
+         conn
+         |> assign(:user, user)
+         |> get("/api/v1/polls/#{object.id}")
+       response = json_response(conn, 200)
+       id = object.id
+       assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
+     end
+     test "does not expose polls for private statuses", %{conn: conn} do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} =
+         CommonAPI.post(user, %{
+           "status" => "Pleroma does",
+           "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
+           "visibility" => "private"
+         })
+       object = Object.normalize(activity)
+       conn =
+         conn
+         |> assign(:user, other_user)
+         |> get("/api/v1/polls/#{object.id}")
+       assert json_response(conn, 404)
+     end
+   end
+   describe "POST /api/v1/polls/:id/votes" do
+     test "votes are added to the poll", %{conn: conn} do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} =
+         CommonAPI.post(user, %{
+           "status" => "A very delicious sandwich",
+           "poll" => %{
+             "options" => ["Lettuce", "Grilled Bacon", "Tomato"],
+             "expires_in" => 20,
+             "multiple" => true
+           }
+         })
+       object = Object.normalize(activity)
+       conn =
+         conn
+         |> assign(:user, other_user)
+         |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
+       assert json_response(conn, 200)
+       object = Object.get_by_id(object.id)
+       assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
+                total_items == 1
+              end)
+     end
+     test "author can't vote", %{conn: conn} do
+       user = insert(:user)
+       {:ok, activity} =
+         CommonAPI.post(user, %{
+           "status" => "Am I cute?",
+           "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
+         })
+       object = Object.normalize(activity)
+       assert conn
+              |> assign(:user, user)
+              |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
+              |> json_response(422) == %{"error" => "Poll's author can't vote"}
+       object = Object.get_by_id(object.id)
+       refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
+     end
+     test "does not allow multiple choices on a single-choice question", %{conn: conn} do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} =
+         CommonAPI.post(user, %{
+           "status" => "The glass is",
+           "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
+         })
+       object = Object.normalize(activity)
+       assert conn
+              |> assign(:user, other_user)
+              |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
+              |> json_response(422) == %{"error" => "Too many choices"}
+       object = Object.get_by_id(object.id)
+       refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
+                total_items == 1
+              end)
      end
    end
  end
index 373efa639b90f4cef1ce7975b476e88883fe83b9,8187ffd0ee1dadde12f542aeeafdee4e50bddf02..3760b44b6c2688c67236a3220e0469cd591c43be
@@@ -40,18 -40,6 +40,18 @@@ defmodule Pleroma.Web.TwitterAPI.Contro
        user = refresh_record(user)
        assert user.info.banner["type"] == "Image"
      end
 +
 +    test "profile banner can be reset", %{conn: conn} do
 +      user = insert(:user)
 +
 +      conn
 +      |> assign(:user, user)
 +      |> post(authenticated_twitter_api__path(conn, :update_banner), %{"banner" => ""})
 +      |> json_response(200)
 +
 +      user = refresh_record(user)
 +      assert user.info.banner == %{}
 +    end
    end
  
    describe "POST /api/qvitter/update_background_image" do
        user = refresh_record(user)
        assert user.info.background["type"] == "Image"
      end
 +
 +    test "background can be reset", %{conn: conn} do
 +      user = insert(:user)
 +
 +      conn
 +      |> assign(:user, user)
 +      |> post(authenticated_twitter_api__path(conn, :update_background), %{"img" => ""})
 +      |> json_response(200)
 +
 +      user = refresh_record(user)
 +      assert user.info.background == %{}
 +    end
    end
  
    describe "POST /api/account/verify_credentials" do
      end
  
      test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do
-       instance =
-         Application.get_env(:pleroma, :instance)
-         |> Keyword.put(:public, false)
-       Application.put_env(:pleroma, :instance, instance)
+       Pleroma.Config.put([:instance, :public], false)
  
        conn
        |> get("/api/statuses/public_timeline.json")
        |> json_response(403)
  
-       instance =
-         Application.get_env(:pleroma, :instance)
-         |> Keyword.put(:public, true)
-       Application.put_env(:pleroma, :instance, instance)
+       Pleroma.Config.put([:instance, :public], true)
      end
  
      test "returns 200 to authenticated request when the instance is not public",
           %{conn: conn, user: user} do
-       instance =
-         Application.get_env(:pleroma, :instance)
-         |> Keyword.put(:public, false)
-       Application.put_env(:pleroma, :instance, instance)
+       Pleroma.Config.put([:instance, :public], false)
  
        conn
        |> with_credentials(user.nickname, "test")
        |> get("/api/statuses/public_timeline.json")
        |> json_response(200)
  
-       instance =
-         Application.get_env(:pleroma, :instance)
-         |> Keyword.put(:public, true)
-       Application.put_env(:pleroma, :instance, instance)
+       Pleroma.Config.put([:instance, :public], true)
      end
  
      test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do
      setup [:valid_user]
  
      test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do
-       instance =
-         Application.get_env(:pleroma, :instance)
-         |> Keyword.put(:public, false)
-       Application.put_env(:pleroma, :instance, instance)
+       Pleroma.Config.put([:instance, :public], false)
  
        conn
        |> get("/api/statuses/public_and_external_timeline.json")
        |> json_response(403)
  
-       instance =
-         Application.get_env(:pleroma, :instance)
-         |> Keyword.put(:public, true)
-       Application.put_env(:pleroma, :instance, instance)
+       Pleroma.Config.put([:instance, :public], true)
      end
  
      test "returns 200 to authenticated request when the instance is not public",
           %{conn: conn, user: user} do
-       instance =
-         Application.get_env(:pleroma, :instance)
-         |> Keyword.put(:public, false)
-       Application.put_env(:pleroma, :instance, instance)
+       Pleroma.Config.put([:instance, :public], false)
  
        conn
        |> with_credentials(user.nickname, "test")
        |> get("/api/statuses/public_and_external_timeline.json")
        |> json_response(200)
  
-       instance =
-         Application.get_env(:pleroma, :instance)
-         |> Keyword.put(:public, true)
-       Application.put_env(:pleroma, :instance, instance)
+       Pleroma.Config.put([:instance, :public], true)
      end
  
      test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do
        current_user = User.get_cached_by_id(current_user.id)
        assert is_map(current_user.avatar)
  
 +      assert json_response(conn, 200) ==
 +               UserView.render("show.json", %{user: current_user, for: current_user})
 +    end
 +
 +    test "user avatar can be reset", %{conn: conn, user: current_user} do
 +      conn =
 +        conn
 +        |> with_credentials(current_user.nickname, "test")
 +        |> post("/api/qvitter/update_avatar.json", %{img: ""})
 +
 +      current_user = User.get_cached_by_id(current_user.id)
 +      assert current_user.avatar == nil
 +
        assert json_response(conn, 200) ==
                 UserView.render("show.json", %{user: current_user, for: current_user})
      end
            "hide_follows" => "false"
          })
  
-       user = Repo.get!(User, user.id)
+       user = refresh_record(user)
        assert user.info.hide_follows == false
        assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
      end
        assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
      end
  
+     test "it sets and un-sets skip_thread_containment", %{conn: conn} do
+       user = insert(:user)
+       response =
+         conn
+         |> assign(:user, user)
+         |> post("/api/account/update_profile.json", %{"skip_thread_containment" => "true"})
+         |> json_response(200)
+       assert response["pleroma"]["skip_thread_containment"] == true
+       user = refresh_record(user)
+       assert user.info.skip_thread_containment
+       response =
+         conn
+         |> assign(:user, user)
+         |> post("/api/account/update_profile.json", %{"skip_thread_containment" => "false"})
+         |> json_response(200)
+       assert response["pleroma"]["skip_thread_containment"] == false
+       refute refresh_record(user).info.skip_thread_containment
+     end
      test "it locks an account", %{conn: conn} do
        user = insert(:user)