diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
new file mode 100644
index 000000000..750f65386
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
@@ -0,0 +1,24 @@
+<%= if get_flash(@conn, :info) do %>
+
<%= get_flash(@conn, :info) %>
+<% end %>
+<%= if get_flash(@conn, :error) do %>
+
<%= get_flash(@conn, :error) %>
+<% end %>
+
+
Two-factor recovery
+
+<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
+
+ <%= label f, :code, "Recovery code" %>
+ <%= text_input f, :code %>
+ <%= hidden_input f, :mfa_token, value: @mfa_token %>
+ <%= hidden_input f, :state, value: @state %>
+ <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
+ <%= hidden_input f, :challenge_type, value: "recovery" %>
+
+
+<%= submit "Verify" %>
+<% end %>
+
">
+ Enter a two-factor code
+
diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
new file mode 100644
index 000000000..af6e546b0
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
@@ -0,0 +1,24 @@
+<%= if get_flash(@conn, :info) do %>
+
<%= get_flash(@conn, :info) %>
+<% end %>
+<%= if get_flash(@conn, :error) do %>
+
<%= get_flash(@conn, :error) %>
+<% end %>
+
+
Two-factor authentication
+
+<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
+
+ <%= label f, :code, "Authentication code" %>
+ <%= text_input f, :code %>
+ <%= hidden_input f, :mfa_token, value: @mfa_token %>
+ <%= hidden_input f, :state, value: @state %>
+ <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
+ <%= hidden_input f, :challenge_type, value: "totp" %>
+
+
+<%= submit "Verify" %>
+<% end %>
+
">
+ Enter a two-factor recovery code
+
diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
new file mode 100644
index 000000000..adc3a3e3d
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
@@ -0,0 +1,13 @@
+<%= if @error do %>
+
<%= @error %>
+<% end %>
+
Two-factor authentication
+
<%= @followee.nickname %>
+
+<%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
+<%= text_input f, :code, placeholder: "Authentication code", required: true %>
+
+<%= hidden_input f, :id, value: @followee.id %>
+<%= hidden_input f, :token, value: @mfa_token %>
+<%= submit "Authorize" %>
+<% end %>
diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
index 89da760da..521dc9322 100644
--- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
@@ -8,10 +8,12 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
require Logger
alias Pleroma.Activity
+ alias Pleroma.MFA
alias Pleroma.Object.Fetcher
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.Auth.Authenticator
+ alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.CommonAPI
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
@@ -68,6 +70,8 @@ defp is_status?(acct) do
# POST /ostatus_subscribe
#
+ # adds a remote account in followers if user already is signed in.
+ #
def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
@@ -78,9 +82,33 @@ def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" =>
end
end
+ # POST /ostatus_subscribe
+ #
+ # step 1.
+ # checks login\password and displays step 2 form of MFA if need.
+ #
def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do
- with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+ with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{_, {:ok, user}, _} <- {:auth, Authenticator.get_user(conn), followee},
+ {_, _, _, false} <- {:mfa_required, followee, user, MFA.require?(user)},
+ {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
+ redirect(conn, to: "/users/#{followee.id}")
+ else
+ error ->
+ handle_follow_error(conn, error)
+ end
+ end
+
+ # POST /ostatus_subscribe
+ #
+ # step 2
+ # checks TOTP code. otherwise displays form with errors
+ #
+ def do_follow(conn, %{"mfa" => %{"code" => code, "token" => token, "id" => id}}) do
+ with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+ {_, _, {:ok, %{user: user}}} <- {:mfa_token, followee, MFA.Token.validate(token)},
+ {_, _, _, {:ok, _}} <-
+ {:verify_mfa_code, followee, token, TOTPAuthenticator.verify(code, user)},
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
redirect(conn, to: "/users/#{followee.id}")
else
@@ -94,6 +122,23 @@ def do_follow(%{assigns: %{user: nil}} = conn, _) do
render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
end
+ defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do
+ render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
+ end
+
+ defp handle_follow_error(conn, {:verify_mfa_code, followee, token, _} = _) do
+ render(conn, "follow_mfa.html", %{
+ error: "Wrong authentication code",
+ followee: followee,
+ mfa_token: token
+ })
+ end
+
+ defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
+ {:ok, %{token: token}} = MFA.Token.create_token(user)
+ render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
+ end
+
defp handle_follow_error(conn, {:auth, _, followee} = _) do
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index cf1d9c74c..5cfb385ac 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -3,54 +3,27 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
+ import Pleroma.Web.Gettext
+
alias Pleroma.Emails.Mailer
alias Pleroma.Emails.UserEmail
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserInviteToken
- require Pleroma.Constants
-
def register_user(params, opts \\ []) do
params =
params
- |> Map.take([
- :nickname,
- :password,
- :captcha_solution,
- :captcha_token,
- :captcha_answer_data,
- :token,
- :email,
- :trusted_app
- ])
- |> Map.put(:bio, User.parse_bio(params[:bio] || ""))
- |> Map.put(:name, params.fullname)
- |> Map.put(:password_confirmation, params[:confirm])
+ |> Map.take([:email, :token, :password])
+ |> Map.put(:bio, params |> Map.get(:bio, "") |> User.parse_bio())
+ |> Map.put(:nickname, params[:username])
+ |> Map.put(:name, Map.get(params, :fullname, params[:username]))
+ |> Map.put(:password_confirmation, params[:password])
- case validate_captcha(params) do
- :ok ->
- if Pleroma.Config.get([:instance, :registrations_open]) do
- create_user(params, opts)
- else
- create_user_with_invite(params, opts)
- end
-
- {:error, error} ->
- # I have no idea how this error handling works
- {:error, %{error: Jason.encode!(%{captcha: [error]})}}
- end
- end
-
- defp validate_captcha(params) do
- if params[:trusted_app] || not Pleroma.Config.get([Pleroma.Captcha, :enabled]) do
- :ok
+ if Pleroma.Config.get([:instance, :registrations_open]) do
+ create_user(params, opts)
else
- Pleroma.Captcha.validate(
- params.captcha_token,
- params.captcha_solution,
- params.captcha_answer_data
- )
+ create_user_with_invite(params, opts)
end
end
@@ -75,16 +48,17 @@ defp create_user(params, opts) do
{:error, changeset} ->
errors =
- Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
+ changeset
+ |> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|> Jason.encode!()
- {:error, %{error: errors}}
+ {:error, errors}
end
end
def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email),
- %User{local: true, email: email} = user when not is_nil(email) <-
+ %User{local: true, email: email} = user when is_binary(email) <-
User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
@@ -106,4 +80,58 @@ def password_reset(nickname_or_email) do
{:error, "unknown user"}
end
end
+
+ def validate_captcha(app, params) do
+ if app.trusted || not Pleroma.Captcha.enabled?() do
+ :ok
+ else
+ do_validate_captcha(params)
+ end
+ end
+
+ defp do_validate_captcha(params) do
+ with :ok <- validate_captcha_presence(params),
+ :ok <-
+ Pleroma.Captcha.validate(
+ params[:captcha_token],
+ params[:captcha_solution],
+ params[:captcha_answer_data]
+ ) do
+ :ok
+ else
+ {:error, :captcha_error} ->
+ captcha_error(dgettext("errors", "CAPTCHA Error"))
+
+ {:error, :invalid} ->
+ captcha_error(dgettext("errors", "Invalid CAPTCHA"))
+
+ {:error, :kocaptcha_service_unavailable} ->
+ captcha_error(dgettext("errors", "Kocaptcha service unavailable"))
+
+ {:error, :expired} ->
+ captcha_error(dgettext("errors", "CAPTCHA expired"))
+
+ {:error, :already_used} ->
+ captcha_error(dgettext("errors", "CAPTCHA already used"))
+
+ {:error, :invalid_answer_data} ->
+ captcha_error(dgettext("errors", "Invalid answer data"))
+
+ {:error, error} ->
+ captcha_error(error)
+ end
+ end
+
+ defp validate_captcha_presence(params) do
+ [:captcha_solution, :captcha_token, :captcha_answer_data]
+ |> Enum.find_value(:ok, fn key ->
+ unless is_binary(params[key]) do
+ error = dgettext("errors", "Invalid CAPTCHA (Missing parameter: %{name})", name: key)
+ {:error, error}
+ end
+ end)
+ end
+
+ # For some reason FE expects error message to be a serialized JSON
+ defp captcha_error(error), do: {:error, Jason.encode!(%{captcha: [error]})}
end
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 443868878..237b29ded 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -25,7 +25,7 @@ def render("update.json", %Activity{} = activity, %User{} = user) do
|> Jason.encode!()
end
- def render("notification.json", %User{} = user, %Notification{} = notify) do
+ def render("notification.json", %Notification{} = notify, %User{} = user) do
%{
event: "notification",
payload:
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index 08e42a7e5..4f9281851 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -200,11 +200,17 @@ def skip_plug(conn) do
@impl Plug
@doc """
- If marked as skipped, returns `conn`, otherwise calls `perform/2`.
+ Before-plug hook that
+ * ensures the plug is not skipped
+ * processes `:if_func` / `:unless_func` functional pre-run conditions
+ * adds plug to the list of called plugs and calls `perform/2` if checks are passed
+
Note: multiple invocations of the same plug (with different or same options) are allowed.
"""
def call(%Plug.Conn{} = conn, options) do
- if PlugHelper.plug_skipped?(conn, __MODULE__) do
+ if PlugHelper.plug_skipped?(conn, __MODULE__) ||
+ (options[:if_func] && !options[:if_func].(conn)) ||
+ (options[:unless_func] && options[:unless_func].(conn)) do
conn
else
conn =
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 7ffd0e51b..71ccf251a 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -86,54 +86,24 @@ def represent_user(user, "XML") do
|> XmlBuilder.to_doc()
end
- defp get_magic_key("data:application/magic-public-key," <> magic_key) do
- {:ok, magic_key}
- end
-
- defp get_magic_key(nil) do
- Logger.debug("Undefined magic key.")
- {:ok, nil}
- end
-
- defp get_magic_key(_) do
- {:error, "Missing magic key data."}
- end
-
defp webfinger_from_xml(doc) do
- with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
- {:ok, magic_key} <- get_magic_key(magic_key),
- topic <-
- XML.string_from_xpath(
- ~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
- doc
- ),
- subject <- XML.string_from_xpath("//Subject", doc),
- subscribe_address <-
- XML.string_from_xpath(
- ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
- doc
- ),
- ap_id <-
- XML.string_from_xpath(
- ~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
- doc
- ) do
- data = %{
- "magic_key" => magic_key,
- "topic" => topic,
- "subject" => subject,
- "subscribe_address" => subscribe_address,
- "ap_id" => ap_id
- }
+ subject = XML.string_from_xpath("//Subject", doc)
- {:ok, data}
- else
- {:error, e} ->
- {:error, e}
+ subscribe_address =
+ ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
+ |> XML.string_from_xpath(doc)
- e ->
- {:error, e}
- end
+ ap_id =
+ ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
+ |> XML.string_from_xpath(doc)
+
+ data = %{
+ "subject" => subject,
+ "subscribe_address" => subscribe_address,
+ "ap_id" => ap_id
+ }
+
+ {:ok, data}
end
defp webfinger_from_json(doc) do
@@ -146,9 +116,6 @@ defp webfinger_from_json(doc) do
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"])
- {_, "http://ostatus.org/schema/1.0/subscribe"} ->
- Map.put(data, "subscribe_address", link["template"])
-
_ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}")
data
@@ -194,13 +161,15 @@ def finger(account) do
URI.parse(account).host
end
+ encoded_account = URI.encode("acct:#{account}")
+
address =
case find_lrdd_template(domain) do
{:ok, template} ->
- String.replace(template, "{uri}", URI.encode(account))
+ String.replace(template, "{uri}", encoded_account)
_ ->
- "https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
+ "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
end
with response <-
diff --git a/mix.exs b/mix.exs
index beb05aab9..6d65e18d4 100644
--- a/mix.exs
+++ b/mix.exs
@@ -176,6 +176,7 @@ defp deps do
{:quack, "~> 0.1.1"},
{:joken, "~> 2.0"},
{:benchee, "~> 1.0"},
+ {:pot, "~> 0.10.2"},
{:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
{:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"},
diff --git a/mix.lock b/mix.lock
index ee9d93bfb..c400202b7 100644
--- a/mix.lock
+++ b/mix.lock
@@ -37,7 +37,7 @@
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"},
"ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"},
"ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"},
- "ex_syslogger": {:hex, :ex_syslogger, "1.5.0", "bc936ee3fd13d9e592cb4c3a1e8a55fccd33b05e3aa7b185f211f3ed263ff8f0", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.0.5", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "f3b4b184dcdd5f356b7c26c6cd72ab0918ba9dfb4061ccfaf519e562942af87b"},
+ "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
"excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "151c476331d49b45601ffc45f43cb3a8beb396b02a34e3777fea0ad34ae57d89"},
"fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"},
"fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"},
@@ -89,6 +89,7 @@
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4737ce62a31747b4c63c12b20c62307e51bb4fcd730ca0c32c280991e0606c90"},
+ "pot": {:hex, :pot, "0.10.2", "9895c83bcff8cd22d9f5bc79dfc88a188176b261b618ad70d93faf5c5ca36e67", [:rebar3], [], "hexpm", "ac589a8e296b7802681e93cd0a436faec117ea63e9916709c628df31e17e91e2"},
"prometheus": {:hex, :prometheus, "4.5.0", "8f4a2246fe0beb50af0f77c5e0a5bb78fe575c34a9655d7f8bc743aad1c6bf76", [:mix, :rebar3], [], "hexpm", "679b5215480fff612b8351f45c839d995a07ce403e42ff02f1c6b20960d41a4e"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"},
"prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"},
@@ -102,7 +103,7 @@
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"},
- "syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm", "769ddfabd0d2a16f3f9c17eb7509951e0ca4f68363fb26f2ee51a8ec4a49881a"},
+ "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "61b7503cef33f00834f78ddfafe0d5d9dec2270b", [ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b"]},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
diff --git a/priv/repo/migrations/20190506054542_add_multi_factor_authentication_settings_to_user.exs b/priv/repo/migrations/20190506054542_add_multi_factor_authentication_settings_to_user.exs
new file mode 100644
index 000000000..8b653c61f
--- /dev/null
+++ b/priv/repo/migrations/20190506054542_add_multi_factor_authentication_settings_to_user.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.AddMultiFactorAuthenticationSettingsToUser do
+ use Ecto.Migration
+
+ def change do
+ alter table(:users) do
+ add(:multi_factor_authentication_settings, :map, default: %{})
+ end
+ end
+end
diff --git a/priv/repo/migrations/20190508193213_create_mfa_tokens.exs b/priv/repo/migrations/20190508193213_create_mfa_tokens.exs
new file mode 100644
index 000000000..da9f8fabe
--- /dev/null
+++ b/priv/repo/migrations/20190508193213_create_mfa_tokens.exs
@@ -0,0 +1,16 @@
+defmodule Pleroma.Repo.Migrations.CreateMfaTokens do
+ use Ecto.Migration
+
+ def change do
+ create table(:mfa_tokens) do
+ add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+ add(:authorization_id, references(:oauth_authorizations, on_delete: :delete_all))
+ add(:token, :string)
+ add(:valid_until, :naive_datetime_usec)
+
+ timestamps()
+ end
+
+ create(unique_index(:mfa_tokens, :token))
+ end
+end
diff --git a/priv/repo/migrations/20200415181818_update_markers.exs b/priv/repo/migrations/20200415181818_update_markers.exs
new file mode 100644
index 000000000..976363565
--- /dev/null
+++ b/priv/repo/migrations/20200415181818_update_markers.exs
@@ -0,0 +1,40 @@
+defmodule Pleroma.Repo.Migrations.UpdateMarkers do
+ use Ecto.Migration
+ import Ecto.Query
+ alias Pleroma.Repo
+
+ def up do
+ update_markers()
+ end
+
+ def down do
+ :ok
+ end
+
+ defp update_markers do
+ now = NaiveDateTime.utc_now()
+
+ markers_attrs =
+ from(q in "notifications",
+ select: %{
+ timeline: "notifications",
+ user_id: q.user_id,
+ last_read_id:
+ type(fragment("MAX( CASE WHEN seen = true THEN id ELSE null END )"), :string)
+ },
+ group_by: [q.user_id]
+ )
+ |> Repo.all()
+ |> Enum.map(fn %{last_read_id: last_read_id} = attrs ->
+ attrs
+ |> Map.put(:last_read_id, last_read_id || "")
+ |> Map.put_new(:inserted_at, now)
+ |> Map.put_new(:updated_at, now)
+ end)
+
+ Repo.insert_all("markers", markers_attrs,
+ on_conflict: {:replace, [:last_read_id]},
+ conflict_target: [:user_id, :timeline]
+ )
+ end
+end
diff --git a/priv/repo/migrations/20200428221338_insert_skeletons_for_deleted_users.exs b/priv/repo/migrations/20200428221338_insert_skeletons_for_deleted_users.exs
new file mode 100644
index 000000000..11d9a70ba
--- /dev/null
+++ b/priv/repo/migrations/20200428221338_insert_skeletons_for_deleted_users.exs
@@ -0,0 +1,45 @@
+defmodule Pleroma.Repo.Migrations.InsertSkeletonsForDeletedUsers do
+ use Ecto.Migration
+
+ alias Pleroma.User
+ alias Pleroma.Repo
+
+ import Ecto.Query
+
+ def change do
+ Application.ensure_all_started(:flake_id)
+
+ local_ap_id =
+ User.Query.build(%{local: true})
+ |> select([u], u.ap_id)
+ |> limit(1)
+ |> Repo.one()
+
+ unless local_ap_id == nil do
+ # Hack to get instance base url because getting it from Phoenix
+ # would require starting the whole application
+ instance_uri =
+ local_ap_id
+ |> URI.parse()
+ |> Map.put(:query, nil)
+ |> Map.put(:path, nil)
+ |> URI.to_string()
+
+ {:ok, %{rows: ap_ids}} =
+ Ecto.Adapters.SQL.query(
+ Repo,
+ "select distinct unnest(nonexistent_locals.recipients) from activities, lateral (select array_agg(recipient) as recipients from unnest(activities.recipients) as recipient where recipient similar to '#{
+ instance_uri
+ }/users/[A-Za-z0-9]*' and not(recipient in (select ap_id from users where local = true))) nonexistent_locals;",
+ [],
+ timeout: :infinity
+ )
+
+ ap_ids
+ |> Enum.each(fn [ap_id] ->
+ Ecto.Changeset.change(%User{}, deactivated: true, ap_id: ap_id)
+ |> Repo.insert()
+ end)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20200505072231_remove_magic_key_field.exs b/priv/repo/migrations/20200505072231_remove_magic_key_field.exs
new file mode 100644
index 000000000..2635e671b
--- /dev/null
+++ b/priv/repo/migrations/20200505072231_remove_magic_key_field.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.RemoveMagicKeyField do
+ use Ecto.Migration
+
+ def change do
+ alter table(:users) do
+ remove(:magic_key, :string)
+ end
+ end
+end
diff --git a/priv/static/adminfe/app.85534e14.css b/priv/static/adminfe/app.796ca6d4.css
similarity index 68%
rename from priv/static/adminfe/app.85534e14.css
rename to priv/static/adminfe/app.796ca6d4.css
index 473ec1b86..1b83a8a39 100644
--- a/priv/static/adminfe/app.85534e14.css
+++ b/priv/static/adminfe/app.796ca6d4.css
@@ -1 +1 @@
-.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .28s;transition:opacity .28s}.fade-enter,.fade-leave-active{opacity:0}.fade-transform-enter-active,.fade-transform-leave-active{-webkit-transition:all .5s;transition:all .5s}.fade-transform-enter{opacity:0;-webkit-transform:translateX(-30px);transform:translateX(-30px)}.fade-transform-leave-to{opacity:0;-webkit-transform:translateX(30px);transform:translateX(30px)}.breadcrumb-enter-active,.breadcrumb-leave-active{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-enter,.breadcrumb-leave-active{opacity:0;-webkit-transform:translateX(20px);transform:translateX(20px)}.breadcrumb-move{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-leave-active{position:absolute}.el-breadcrumb__inner,.el-breadcrumb__inner a{font-weight:400!important}.el-upload input[type=file]{display:none!important}.el-upload__input{display:none}.cell .el-tag{margin-right:0}.small-padding .cell{padding-left:5px;padding-right:5px}.fixed-width .el-button--mini{padding:7px 10px;width:60px}.status-col .cell{padding:0 10px;text-align:center}.status-col .cell .el-tag{margin-right:0}.el-dialog{-webkit-transform:none;transform:none;left:0;position:relative;margin:0 auto}.article-textarea textarea{padding-right:40px;resize:none;border-radius:0;border:none;border-bottom:1px solid #bfcbd9}.upload-container .el-upload{width:100%}.upload-container .el-upload .el-upload-dragger{width:100%;height:200px}.el-dropdown-menu a{display:block}#app .main-container{min-height:100%;-webkit-transition:margin-left .28s;transition:margin-left .28s;margin-left:180px;position:relative}#app .sidebar-container{-webkit-transition:width .28s;transition:width .28s;width:180px!important;height:100%;position:fixed;font-size:0;top:0;bottom:0;left:0;z-index:1001;overflow:hidden}#app .sidebar-container .horizontal-collapse-transition{-webkit-transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out;transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out}#app .sidebar-container .scrollbar-wrapper{overflow-x:hidden!important}#app .sidebar-container .scrollbar-wrapper .el-scrollbar__view{height:100%}#app .sidebar-container .el-scrollbar__bar.is-vertical{right:0}#app .sidebar-container .is-horizontal{display:none}#app .sidebar-container a{display:inline-block;width:100%;overflow:hidden}#app .sidebar-container .svg-icon{margin-right:16px}#app .sidebar-container .el-menu{border:none;height:100%;width:100%!important}#app .sidebar-container .el-submenu__title:hover,#app .sidebar-container .submenu-title-noDropdown:hover{background-color:#263445!important}#app .sidebar-container .is-active>.el-submenu__title{color:#f4f4f5!important}#app .sidebar-container .el-submenu .el-menu-item,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title{min-width:180px!important;background-color:#1f2d3d!important}#app .sidebar-container .el-submenu .el-menu-item:hover,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#001528!important}#app .hideSidebar .sidebar-container{width:36px!important}#app .hideSidebar .main-container{margin-left:36px}#app .hideSidebar .submenu-title-noDropdown{padding-left:10px!important;position:relative}#app .hideSidebar .submenu-title-noDropdown .el-tooltip{padding:0 10px!important}#app .hideSidebar .el-submenu{overflow:hidden}#app .hideSidebar .el-submenu>.el-submenu__title{padding-left:10px!important}#app .hideSidebar .el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}#app .hideSidebar .el-menu--collapse .el-submenu>.el-submenu__title>span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}#app .el-menu--collapse .el-menu .el-submenu{min-width:180px!important}#app .mobile .main-container{margin-left:0}#app .mobile .sidebar-container{-webkit-transition:-webkit-transform .28s;transition:-webkit-transform .28s;transition:transform .28s;transition:transform .28s,-webkit-transform .28s;width:180px!important}#app .mobile.hideSidebar .sidebar-container{pointer-events:none;-webkit-transition-duration:.3s;transition-duration:.3s;-webkit-transform:translate3d(-180px,0,0);transform:translate3d(-180px,0,0)}#app .withoutAnimation .main-container,#app .withoutAnimation .sidebar-container{-webkit-transition:none;transition:none}.el-menu--vertical>.el-menu .svg-icon{margin-right:16px}.el-menu--vertical .el-menu-item:hover,.el-menu--vertical .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#263445!important}.blue-btn{background:#324157}.blue-btn:hover{color:#324157}.blue-btn:hover:after,.blue-btn:hover:before{background:#324157}.light-blue-btn{background:#3a71a8}.light-blue-btn:hover{color:#3a71a8}.light-blue-btn:hover:after,.light-blue-btn:hover:before{background:#3a71a8}.red-btn{background:#c03639}.red-btn:hover{color:#c03639}.red-btn:hover:after,.red-btn:hover:before{background:#c03639}.pink-btn{background:#e65d6e}.pink-btn:hover{color:#e65d6e}.pink-btn:hover:after,.pink-btn:hover:before{background:#e65d6e}.green-btn{background:#30b08f}.green-btn:hover{color:#30b08f}.green-btn:hover:after,.green-btn:hover:before{background:#30b08f}.tiffany-btn{background:#4ab7bd}.tiffany-btn:hover{color:#4ab7bd}.tiffany-btn:hover:after,.tiffany-btn:hover:before{background:#4ab7bd}.yellow-btn{background:#fec171}.yellow-btn:hover{color:#fec171}.yellow-btn:hover:after,.yellow-btn:hover:before{background:#fec171}.pan-btn{font-size:14px;color:#fff;padding:14px 36px;border-radius:8px;border:none;outline:none;-webkit-transition:all .6s ease;transition:all .6s ease;position:relative;display:inline-block}.pan-btn:hover{background:#fff}.pan-btn:hover:after,.pan-btn:hover:before{width:100%;-webkit-transition:all .6s ease;transition:all .6s ease}.pan-btn:after,.pan-btn:before{content:"";position:absolute;top:0;right:0;height:2px;width:0;-webkit-transition:all .4s ease;transition:all .4s ease}.pan-btn:after{right:inherit;top:inherit;left:0;bottom:0}.custom-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;color:#fff;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;padding:10px 15px;font-size:14px;border-radius:4px}body{height:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif;background:#fff;color:#000}label{font-weight:700}html{-webkit-box-sizing:border-box;box-sizing:border-box}#app,html{height:100%}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}.no-padding{padding:0!important}.padding-content{padding:4px 0}a:active,a:focus{outline:none}a,a:focus,a:hover{cursor:pointer;color:inherit;text-decoration:none}div:focus{outline:none}.fr{float:right}.fl{float:left}.pr-5{padding-right:5px}.pl-5{padding-left:5px}.block{display:block}.pointer{cursor:pointer}.inlineBlock{display:block}.clearfix:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}code{background:#eef1f6;padding:15px 16px;margin-bottom:20px;display:block;line-height:36px;font-size:15px;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif}code a{color:#337ab7;cursor:pointer}code a:hover{color:#20a0ff}.warn-content{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem;line-height:1.6rem;word-spacing:.05rem}.warn-content a{color:#42b983;font-weight:600}.app-container{padding:20px}.components-container{margin:30px 50px;position:relative}.pagination-container{margin-top:30px}.text-center{text-align:center}.sub-navbar{height:50px;line-height:50px;position:relative;width:100%;text-align:right;padding-right:20px;-webkit-transition:position .6s ease;transition:position .6s ease;background:-webkit-gradient(linear,left top,right top,from(#20b6f9),color-stop(0,#20b6f9),color-stop(100%,#2178f1),to(#2178f1));background:linear-gradient(90deg,#20b6f9,#20b6f9 0,#2178f1 100%,#2178f1 0)}.sub-navbar .subtitle{font-size:20px;color:#fff}.sub-navbar.deleted,.sub-navbar.draft{background:#d0d0d0}.link-type,.link-type:focus{color:#337ab7;cursor:pointer}.link-type:focus:hover,.link-type:hover{color:#20a0ff}.filter-container{padding-bottom:10px}.filter-container .filter-item{display:inline-block;vertical-align:middle;margin-bottom:10px}.multiselect{line-height:16px}.multiselect--active{z-index:1000!important}.hamburger[data-v-69c6c5c4]{display:inline-block;vertical-align:middle;width:20px;height:20px}.hamburger.is-active[data-v-69c6c5c4]{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.navbar[data-v-19937682]{height:50px;overflow:hidden}.navbar .hamburger-container[data-v-19937682]{line-height:46px;height:100%;float:left;cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .hamburger-container[data-v-19937682]:hover{background:rgba(0,0,0,.025)}.navbar .breadcrumb-container[data-v-19937682]{float:left}.navbar .errLog-container[data-v-19937682]{display:inline-block;vertical-align:top}.navbar .right-menu[data-v-19937682]{float:right;height:100%;line-height:50px}.navbar .right-menu[data-v-19937682]:focus{outline:none}.navbar .right-menu .right-menu-item[data-v-19937682]{display:inline-block;padding:0 8px;height:100%;font-size:18px;color:#5a5e66;vertical-align:text-bottom}.navbar .right-menu .right-menu-item.hover-effect[data-v-19937682]{cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .right-menu .right-menu-item.hover-effect[data-v-19937682]:hover{background:rgba(0,0,0,.025)}.navbar .right-menu .avatar-container .avatar-wrapper[data-v-19937682]{margin-top:5px;position:relative}.navbar .right-menu .avatar-container .avatar-wrapper .user-avatar[data-v-19937682]{cursor:pointer;width:40px;height:40px;border-radius:10px}.navbar .right-menu .avatar-container .avatar-wrapper .el-icon-caret-bottom[data-v-19937682]{cursor:pointer;position:absolute;right:-20px;top:25px;font-size:12px}.scroll-container[data-v-591d6778]{white-space:nowrap;position:relative;overflow:hidden;width:100%}.scroll-container[data-v-591d6778] .el-scrollbar__bar{bottom:0}.scroll-container[data-v-591d6778] .el-scrollbar__wrap{height:49px}.tags-view-container[data-v-e1cdb714]{height:34px;width:100%;background:#fff;border-bottom:1px solid #d8dce5;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04);box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04)}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-e1cdb714]{display:inline-block;position:relative;cursor:pointer;height:26px;line-height:26px;border:1px solid #d8dce5;color:#495060;background:#fff;padding:0 8px;font-size:12px;margin-left:5px;margin-top:4px}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-e1cdb714]:first-of-type{margin-left:15px}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-e1cdb714]:last-of-type{margin-right:15px}.tags-view-container .tags-view-wrapper .tags-view-item.active[data-v-e1cdb714]{background-color:#42b983;color:#fff;border-color:#42b983}.tags-view-container .tags-view-wrapper .tags-view-item.active[data-v-e1cdb714]:before{content:"";background:#fff;display:inline-block;width:8px;height:8px;border-radius:50%;position:relative;margin-right:2px}.tags-view-container .contextmenu[data-v-e1cdb714]{margin:0;background:#fff;z-index:100;position:absolute;list-style-type:none;padding:5px 0;border-radius:4px;font-size:12px;font-weight:400;color:#333;-webkit-box-shadow:2px 2px 3px 0 rgba(0,0,0,.3);box-shadow:2px 2px 3px 0 rgba(0,0,0,.3)}.tags-view-container .contextmenu li[data-v-e1cdb714]{margin:0;padding:7px 16px;cursor:pointer}.tags-view-container .contextmenu li[data-v-e1cdb714]:hover{background:#eee}.tags-view-wrapper .tags-view-item .el-icon-close{width:16px;height:16px;vertical-align:2px;border-radius:50%;text-align:center;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.tags-view-wrapper .tags-view-item .el-icon-close:before{-webkit-transform:scale(.6);transform:scale(.6);display:inline-block;vertical-align:-3px}.tags-view-wrapper .tags-view-item .el-icon-close:hover{background-color:#b4bccc;color:#fff}.app-main[data-v-f852c4f2]{min-height:calc(100vh - 84px);width:100%;position:relative;overflow:hidden}.app-wrapper[data-v-767d264f]{position:relative;height:100%;width:100%}.app-wrapper[data-v-767d264f]:after{content:"";display:table;clear:both}.app-wrapper.mobile.openSidebar[data-v-767d264f]{position:fixed;top:0}.drawer-bg[data-v-767d264f]{background:#000;opacity:.3;width:100%;top:0;height:100%;position:absolute;z-index:999}.svg-icon[data-v-17178ffc]{width:1em;height:1em;vertical-align:-.15em;fill:currentColor;overflow:hidden}
\ No newline at end of file
+.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .28s;transition:opacity .28s}.fade-enter,.fade-leave-active{opacity:0}.fade-transform-enter-active,.fade-transform-leave-active{-webkit-transition:all .5s;transition:all .5s}.fade-transform-enter{opacity:0;-webkit-transform:translateX(-30px);transform:translateX(-30px)}.fade-transform-leave-to{opacity:0;-webkit-transform:translateX(30px);transform:translateX(30px)}.breadcrumb-enter-active,.breadcrumb-leave-active{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-enter,.breadcrumb-leave-active{opacity:0;-webkit-transform:translateX(20px);transform:translateX(20px)}.breadcrumb-move{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-leave-active{position:absolute}.el-breadcrumb__inner,.el-breadcrumb__inner a{font-weight:400!important}.el-upload input[type=file]{display:none!important}.el-upload__input{display:none}.cell .el-tag{margin-right:0}.small-padding .cell{padding-left:5px;padding-right:5px}.fixed-width .el-button--mini{padding:7px 10px;width:60px}.status-col .cell{padding:0 10px;text-align:center}.status-col .cell .el-tag{margin-right:0}.el-dialog{-webkit-transform:none;transform:none;left:0;position:relative;margin:0 auto}.article-textarea textarea{padding-right:40px;resize:none;border-radius:0;border:none;border-bottom:1px solid #bfcbd9}.upload-container .el-upload{width:100%}.upload-container .el-upload .el-upload-dragger{width:100%;height:200px}.el-dropdown-menu a{display:block}#app .main-container{min-height:100%;-webkit-transition:margin-left .28s;transition:margin-left .28s;margin-left:180px;position:relative}#app .sidebar-container{-webkit-transition:width .28s;transition:width .28s;width:180px!important;height:100%;position:fixed;font-size:0;top:0;bottom:0;left:0;z-index:5000;overflow:hidden}#app .sidebar-container .horizontal-collapse-transition{-webkit-transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out;transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out}#app .sidebar-container .scrollbar-wrapper{overflow-x:hidden!important}#app .sidebar-container .scrollbar-wrapper .el-scrollbar__view{height:100%}#app .sidebar-container .el-scrollbar__bar.is-vertical{right:0}#app .sidebar-container .is-horizontal{display:none}#app .sidebar-container a{display:inline-block;width:100%;overflow:hidden}#app .sidebar-container .svg-icon{margin-right:16px}#app .sidebar-container .el-menu{border:none;height:100%;width:100%!important}#app .sidebar-container .el-submenu__title:hover,#app .sidebar-container .submenu-title-noDropdown:hover{background-color:#263445!important}#app .sidebar-container .is-active>.el-submenu__title{color:#f4f4f5!important}#app .sidebar-container .el-submenu .el-menu-item,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title{min-width:180px!important;background-color:#1f2d3d!important}#app .sidebar-container .el-submenu .el-menu-item:hover,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#001528!important}#app .hideSidebar .sidebar-container{width:36px!important}#app .hideSidebar .main-container{margin-left:36px}#app .hideSidebar .submenu-title-noDropdown{padding-left:10px!important;position:relative}#app .hideSidebar .submenu-title-noDropdown .el-tooltip{padding:0 10px!important}#app .hideSidebar .el-submenu{overflow:hidden}#app .hideSidebar .el-submenu>.el-submenu__title{padding-left:10px!important}#app .hideSidebar .el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}#app .hideSidebar .el-menu--collapse .el-submenu>.el-submenu__title>span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}#app .el-menu--collapse .el-menu .el-submenu{min-width:180px!important}#app .mobile .main-container{margin-left:0}#app .mobile .sidebar-container{-webkit-transition:-webkit-transform .28s;transition:-webkit-transform .28s;transition:transform .28s;transition:transform .28s,-webkit-transform .28s;width:180px!important}#app .mobile.hideSidebar .sidebar-container{pointer-events:none;-webkit-transition-duration:.3s;transition-duration:.3s;-webkit-transform:translate3d(-180px,0,0);transform:translate3d(-180px,0,0)}#app .withoutAnimation .main-container,#app .withoutAnimation .sidebar-container{-webkit-transition:none;transition:none}.el-menu--vertical>.el-menu .svg-icon{margin-right:16px}.el-menu--vertical .el-menu-item:hover,.el-menu--vertical .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#263445!important}.blue-btn{background:#324157}.blue-btn:hover{color:#324157}.blue-btn:hover:after,.blue-btn:hover:before{background:#324157}.light-blue-btn{background:#3a71a8}.light-blue-btn:hover{color:#3a71a8}.light-blue-btn:hover:after,.light-blue-btn:hover:before{background:#3a71a8}.red-btn{background:#c03639}.red-btn:hover{color:#c03639}.red-btn:hover:after,.red-btn:hover:before{background:#c03639}.pink-btn{background:#e65d6e}.pink-btn:hover{color:#e65d6e}.pink-btn:hover:after,.pink-btn:hover:before{background:#e65d6e}.green-btn{background:#30b08f}.green-btn:hover{color:#30b08f}.green-btn:hover:after,.green-btn:hover:before{background:#30b08f}.tiffany-btn{background:#4ab7bd}.tiffany-btn:hover{color:#4ab7bd}.tiffany-btn:hover:after,.tiffany-btn:hover:before{background:#4ab7bd}.yellow-btn{background:#fec171}.yellow-btn:hover{color:#fec171}.yellow-btn:hover:after,.yellow-btn:hover:before{background:#fec171}.pan-btn{font-size:14px;color:#fff;padding:14px 36px;border-radius:8px;border:none;outline:none;-webkit-transition:all .6s ease;transition:all .6s ease;position:relative;display:inline-block}.pan-btn:hover{background:#fff}.pan-btn:hover:after,.pan-btn:hover:before{width:100%;-webkit-transition:all .6s ease;transition:all .6s ease}.pan-btn:after,.pan-btn:before{content:"";position:absolute;top:0;right:0;height:2px;width:0;-webkit-transition:all .4s ease;transition:all .4s ease}.pan-btn:after{right:inherit;top:inherit;left:0;bottom:0}.custom-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;color:#fff;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;padding:10px 15px;font-size:14px;border-radius:4px}body{height:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif;background:#fff;color:#000}label{font-weight:700}html{-webkit-box-sizing:border-box;box-sizing:border-box}#app,html{height:100%}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}.no-padding{padding:0!important}.padding-content{padding:4px 0}a:active,a:focus{outline:none}a,a:focus,a:hover{cursor:pointer;color:inherit;text-decoration:none}div:focus{outline:none}.fr{float:right}.fl{float:left}.pr-5{padding-right:5px}.pl-5{padding-left:5px}.block{display:block}.pointer{cursor:pointer}.inlineBlock{display:block}.clearfix:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}code{background:#eef1f6;padding:15px 16px;margin-bottom:20px;display:block;line-height:36px;font-size:15px;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif}code a{color:#337ab7;cursor:pointer}code a:hover{color:#20a0ff}.warn-content{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem;line-height:1.6rem;word-spacing:.05rem}.warn-content a{color:#42b983;font-weight:600}.app-container{padding:20px}.components-container{margin:30px 50px;position:relative}.pagination-container{margin-top:30px}.text-center{text-align:center}.sub-navbar{height:50px;line-height:50px;position:relative;width:100%;text-align:right;padding-right:20px;-webkit-transition:position .6s ease;transition:position .6s ease;background:-webkit-gradient(linear,left top,right top,from(#20b6f9),color-stop(0,#20b6f9),color-stop(100%,#2178f1),to(#2178f1));background:linear-gradient(90deg,#20b6f9,#20b6f9 0,#2178f1 100%,#2178f1 0)}.sub-navbar .subtitle{font-size:20px;color:#fff}.sub-navbar.deleted,.sub-navbar.draft{background:#d0d0d0}.link-type,.link-type:focus{color:#337ab7;cursor:pointer}.link-type:focus:hover,.link-type:hover{color:#20a0ff}.filter-container{padding-bottom:10px}.filter-container .filter-item{display:inline-block;vertical-align:middle;margin-bottom:10px}.multiselect{line-height:16px}.multiselect--active{z-index:1000!important}.hamburger[data-v-69c6c5c4]{display:inline-block;vertical-align:middle;width:20px;height:20px}.hamburger.is-active[data-v-69c6c5c4]{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.navbar[data-v-28de7ff2]{height:50px;overflow:hidden}.navbar .hamburger-container[data-v-28de7ff2]{line-height:46px;height:100%;float:left;cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .hamburger-container[data-v-28de7ff2]:hover{background:rgba(0,0,0,.025)}.navbar .breadcrumb-container[data-v-28de7ff2]{float:left}.navbar .errLog-container[data-v-28de7ff2]{display:inline-block;vertical-align:top}.navbar .right-menu[data-v-28de7ff2]{float:right;height:100%;line-height:50px}.navbar .right-menu[data-v-28de7ff2]:focus{outline:none}.navbar .right-menu .right-menu-item[data-v-28de7ff2]{display:inline-block;padding:0 15px;height:100%;font-size:18px;color:#5a5e66;vertical-align:text-bottom}.navbar .right-menu .right-menu-item.hover-effect[data-v-28de7ff2]{cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .right-menu .right-menu-item.hover-effect[data-v-28de7ff2]:hover{background:rgba(0,0,0,.025)}.navbar .right-menu .avatar-container .avatar-wrapper[data-v-28de7ff2]{margin-top:5px;position:relative}.navbar .right-menu .avatar-container .avatar-wrapper .user-avatar[data-v-28de7ff2]{cursor:pointer;width:40px;height:40px;border-radius:10px}.navbar .right-menu .avatar-container .avatar-wrapper .el-icon-caret-bottom[data-v-28de7ff2]{cursor:pointer;position:absolute;right:-20px;top:25px;font-size:12px}.scroll-container[data-v-591d6778]{white-space:nowrap;position:relative;overflow:hidden;width:100%}.scroll-container[data-v-591d6778] .el-scrollbar__bar{bottom:0}.scroll-container[data-v-591d6778] .el-scrollbar__wrap{height:49px}.tags-view-container[data-v-e1cdb714]{height:34px;width:100%;background:#fff;border-bottom:1px solid #d8dce5;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04);box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04)}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-e1cdb714]{display:inline-block;position:relative;cursor:pointer;height:26px;line-height:26px;border:1px solid #d8dce5;color:#495060;background:#fff;padding:0 8px;font-size:12px;margin-left:5px;margin-top:4px}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-e1cdb714]:first-of-type{margin-left:15px}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-e1cdb714]:last-of-type{margin-right:15px}.tags-view-container .tags-view-wrapper .tags-view-item.active[data-v-e1cdb714]{background-color:#42b983;color:#fff;border-color:#42b983}.tags-view-container .tags-view-wrapper .tags-view-item.active[data-v-e1cdb714]:before{content:"";background:#fff;display:inline-block;width:8px;height:8px;border-radius:50%;position:relative;margin-right:2px}.tags-view-container .contextmenu[data-v-e1cdb714]{margin:0;background:#fff;z-index:100;position:absolute;list-style-type:none;padding:5px 0;border-radius:4px;font-size:12px;font-weight:400;color:#333;-webkit-box-shadow:2px 2px 3px 0 rgba(0,0,0,.3);box-shadow:2px 2px 3px 0 rgba(0,0,0,.3)}.tags-view-container .contextmenu li[data-v-e1cdb714]{margin:0;padding:7px 16px;cursor:pointer}.tags-view-container .contextmenu li[data-v-e1cdb714]:hover{background:#eee}.tags-view-wrapper .tags-view-item .el-icon-close{width:16px;height:16px;vertical-align:2px;border-radius:50%;text-align:center;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.tags-view-wrapper .tags-view-item .el-icon-close:before{-webkit-transform:scale(.6);transform:scale(.6);display:inline-block;vertical-align:-3px}.tags-view-wrapper .tags-view-item .el-icon-close:hover{background-color:#b4bccc;color:#fff}.app-main[data-v-f852c4f2]{min-height:calc(100vh - 84px);width:100%;position:relative;overflow:hidden}.app-wrapper[data-v-767d264f]{position:relative;height:100%;width:100%}.app-wrapper[data-v-767d264f]:after{content:"";display:table;clear:both}.app-wrapper.mobile.openSidebar[data-v-767d264f]{position:fixed;top:0}.drawer-bg[data-v-767d264f]{background:#000;opacity:.3;width:100%;top:0;height:100%;position:absolute;z-index:999}.svg-icon[data-v-17178ffc]{width:1em;height:1em;vertical-align:-.15em;fill:currentColor;overflow:hidden}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-15fa.5a5f973d.css b/priv/static/adminfe/chunk-0558.af0d89cd.css
similarity index 100%
rename from priv/static/adminfe/chunk-15fa.5a5f973d.css
rename to priv/static/adminfe/chunk-0558.af0d89cd.css
diff --git a/priv/static/adminfe/chunk-0778.d9e7180a.css b/priv/static/adminfe/chunk-0778.d9e7180a.css
new file mode 100644
index 000000000..9d730019a
--- /dev/null
+++ b/priv/static/adminfe/chunk-0778.d9e7180a.css
@@ -0,0 +1 @@
+.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px}.invites-container .create-invite-token{text-align:left;width:350px;padding:10px}.invites-container .create-new-token-dialog{width:50%}.invites-container .create-new-token-dialog a{margin-bottom:3px}.invites-container .create-new-token-dialog .el-card__body{padding:10px 20px}.invites-container .el-dialog__body{padding:5px 20px 0}.invites-container h1{margin:0}.invites-container .icon{margin-right:5px}.invites-container .invite-token-table{width:100%;margin:0 15px}.invites-container .invite-via-email{text-align:left;width:350px;padding:10px}.invites-container .invite-via-email-dialog{width:50%}.invites-container .invites-header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin:10px 15px}.invites-container .info{color:#666;font-size:13px;line-height:22px;margin:0 0 10px}.invites-container .new-token-card .el-form-item{margin:0}.invites-container .reboot-button{padding:10px;margin:0;width:145px}@media only screen and (max-width:480px){.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 10px 7px}.invites-container .cell{padding:0}.invites-container .create-invite-token{width:100%}.invites-container .create-new-token-dialog{width:85%}.invites-container .el-date-editor{width:150px}.invites-container .el-dialog__body{padding:5px 15px 0}.invites-container h1{margin:0}.invites-container .invite-token-table{width:100%;margin:0 5px;font-size:12px;font-weight:500}.invites-container .invite-via-email{width:100%;margin:10px 0 0}.invites-container .invite-via-email-dialog{width:85%}.invites-container .invites-header-container{margin:0 10px}.invites-container .info{margin:0 0 10px 5px}.invites-container th .cell{padding:0}.create-invite-token,.invite-via-email{width:100%}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-876c.90dffac4.css b/priv/static/adminfe/chunk-0961.d3692214.css
similarity index 100%
rename from priv/static/adminfe/chunk-876c.90dffac4.css
rename to priv/static/adminfe/chunk-0961.d3692214.css
diff --git a/priv/static/adminfe/chunk-0d8f.d85f5a29.css b/priv/static/adminfe/chunk-0d8f.d85f5a29.css
deleted file mode 100644
index 931620872..000000000
--- a/priv/static/adminfe/chunk-0d8f.d85f5a29.css
+++ /dev/null
@@ -1 +0,0 @@
-.select-field[data-v-29abde8c]{width:350px}@media only screen and (max-width:480px){.select-field[data-v-29abde8c]{width:100%;margin-bottom:5px}}@media only screen and (max-width:801px) and (min-width:481px){.select-field[data-v-29abde8c]{width:50%}}.actions-button[data-v-3850612b]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-3850612b]{float:right}.el-icon-edit[data-v-3850612b]{margin-right:5px}.tag-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-3850612b]{padding-right:20px}.no-hover[data-v-3850612b]:hover{color:#606266;background-color:#fff;cursor:auto}.el-dialog__body{padding:20px}.create-account-form-item{margin-bottom:20px}.create-account-form-item-without-margin{margin-bottom:0}@media only screen and (max-width:480px){.create-user-dialog{width:85%}.create-account-form-item{margin-bottom:20px}.el-dialog__body{padding:20px}}.moderate-user-button{text-align:left;width:200px;padding:10px}.moderate-user-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.actions-button{text-align:left;width:350px;padding:10px}.actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0 15px 10px}.active-tag{color:#409eff;font-weight:700}.active-tag .el-icon-check{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link:hover{cursor:pointer;color:#409eff}.create-account>.el-icon-plus{margin-right:5px}.password-reset-token{margin:0 0 14px}.password-reset-token-dialog{width:50%}.reset-password-link{text-decoration:underline}.users-container h1{margin:10px 0 0 15px}.users-container .pagination{margin:25px 0;text-align:center}.users-container .search{width:350px;float:right}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 15px 15px}.users-container .user-count{color:grey;font-size:28px}@media only screen and (max-width:480px){.password-reset-token-dialog{width:85%}.users-container h1{margin:7px 10px 15px}.users-container .actions-button{width:100%}.users-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .el-icon-arrow-down{font-size:12px}.users-container .search{width:100%}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px}.users-container .el-tag{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger,.users-container .el-tag.el-tag--success{padding-left:8px}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-13e9.98eaadba.css b/priv/static/adminfe/chunk-13e9.98eaadba.css
deleted file mode 100644
index 9f377eee2..000000000
--- a/priv/static/adminfe/chunk-13e9.98eaadba.css
+++ /dev/null
@@ -1 +0,0 @@
-.moderation-log-container[data-v-5d520014]{margin:0 15px}h1[data-v-5d520014]{margin:10px 0 20px}.el-timeline[data-v-5d520014]{margin:25px 45px 0 0;padding:0}.moderation-log-date-panel[data-v-5d520014]{width:350px}.moderation-log-nav-container[data-v-5d520014]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.moderation-log-search[data-v-5d520014]{width:350px}.moderation-log-user-select[data-v-5d520014]{margin:0 0 20px;width:350px}.search-container[data-v-5d520014]{text-align:right}.pagination[data-v-5d520014]{text-align:center}@media only screen and (max-width:480px){.moderation-log-date-panel[data-v-5d520014]{width:100%}.moderation-log-user-select[data-v-5d520014]{margin:0 0 10px;width:55%}.moderation-log-search[data-v-5d520014]{width:40%}}@media only screen and (max-width:801px) and (min-width:481px){.moderation-log-date-panel[data-v-5d520014]{width:55%}.moderation-log-user-select[data-v-5d520014]{margin:0 0 10px;width:55%}.moderation-log-search[data-v-5d520014]{width:40%}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-22d2.813009b9.css b/priv/static/adminfe/chunk-22d2.813009b9.css
new file mode 100644
index 000000000..f0a98583e
--- /dev/null
+++ b/priv/static/adminfe/chunk-22d2.813009b9.css
@@ -0,0 +1 @@
+.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.moderate-user-button{text-align:left;width:350px;padding:10px}.moderate-user-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.moderate-user-button{width:100%}}.security-settings-container{display:-webkit-box;display:-ms-flexbox;display:flex}.security-settings-container label{width:15%;height:36px}.security-settings-modal .el-dialog__body{padding-top:10px}.security-settings-modal .el-form-item,.security-settings-modal .password-alert{margin-bottom:15px}.security-settings-modal .password-input{margin-bottom:0}.security-settings-submit-button{float:right}@media (max-width:800px){.security-settings-modal .el-dialog{width:90%}}.security-settings-modal .el-alert .el-alert__description{word-break:break-word;font-size:1em}.security-settings-modal .form-text{display:block;margin-top:.25rem;color:#909399}header[data-v-d4bc7d00]{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;margin:22px 0;padding-left:15px}header h1[data-v-d4bc7d00]{margin:0 0 0 10px}table[data-v-d4bc7d00]{margin:10px 0 0 15px}table .name-col[data-v-d4bc7d00]{width:150px}.avatar-name-container[data-v-d4bc7d00]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-table--border[data-v-d4bc7d00]:after,.el-table--group[data-v-d4bc7d00]:after,.el-table[data-v-d4bc7d00]:before{background-color:transparent}.image[data-v-d4bc7d00]{width:20%}.image img[data-v-d4bc7d00]{width:100%}.left-header-container[data-v-d4bc7d00]{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.no-statuses[data-v-d4bc7d00]{margin-left:28px;color:#606266}.poll ul[data-v-d4bc7d00]{list-style-type:none;padding:0;width:30%}.reboot-button[data-v-d4bc7d00]{padding:10px;margin-left:10px}.recent-statuses-container[data-v-d4bc7d00]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:67%}.recent-statuses-header[data-v-d4bc7d00]{margin-top:10px}.security-setting-button[data-v-d4bc7d00]{margin-top:20px;width:100%}.statuses[data-v-d4bc7d00]{padding:0 20px 0 0}.show-private[data-v-d4bc7d00]{width:200px;text-align:left;line-height:67px;margin-right:20px}.show-private-statuses[data-v-d4bc7d00]{margin-left:28px;margin-bottom:20px}.recent-statuses[data-v-d4bc7d00]{margin-left:28px}.user-page-header[data-v-d4bc7d00]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:0 15px 0 20px;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.user-page-header h1[data-v-d4bc7d00]{display:inline}.user-profile-card[data-v-d4bc7d00]{margin:0 20px;width:30%;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content}.user-profile-container[data-v-d4bc7d00]{display:-webkit-box;display:-ms-flexbox;display:flex}.user-profile-table[data-v-d4bc7d00]{margin:0}.user-profile-tag[data-v-d4bc7d00]{margin:0 4px 4px 0}@media only screen and (max-width:480px){.avatar-name-container[data-v-d4bc7d00]{margin-bottom:10px}.recent-statuses[data-v-d4bc7d00]{margin:20px 10px 15px}.recent-statuses-container[data-v-d4bc7d00]{width:100%;margin:0 10px}.show-private-statuses[data-v-d4bc7d00]{margin:0 10px 20px}.user-page-header[data-v-d4bc7d00]{padding:0;margin:7px 15px 15px 10px}.user-page-header-container .el-dropdown[data-v-d4bc7d00]{width:95%;margin:0 15px 15px 10px}.user-profile-card[data-v-d4bc7d00]{margin:0 10px;width:95%}.user-profile-card td[data-v-d4bc7d00]{width:80px}.user-profile-container[data-v-d4bc7d00]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}@media only screen and (max-width:801px) and (min-width:481px){.recent-statuses[data-v-d4bc7d00]{margin:20px 10px 15px 0}.recent-statuses-container[data-v-d4bc7d00]{width:97%;margin:0 20px}.show-private-statuses[data-v-d4bc7d00]{margin:0 10px 20px 0}.user-page-header[data-v-d4bc7d00]{padding:0;margin:7px 15px 20px 20px}.user-profile-card[data-v-d4bc7d00]{margin:0 20px;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.user-profile-container[data-v-d4bc7d00]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-2b9c.feb61a2b.css b/priv/static/adminfe/chunk-2b9c.feb61a2b.css
deleted file mode 100644
index f54eca1f5..000000000
--- a/priv/static/adminfe/chunk-2b9c.feb61a2b.css
+++ /dev/null
@@ -1 +0,0 @@
-.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.moderate-user-button{text-align:left;width:200px;padding:10px}.moderate-user-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media (max-width:800px){.security-settings-modal .el-dialog{width:90%}}.security-settings-modal .el-alert .el-alert__description{word-break:break-word;font-size:1em}.security-settings-modal .form-text{display:block;margin-top:.25rem;color:#909399}.avatar-name-container[data-v-77412d30],header[data-v-77412d30]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}header[data-v-77412d30]{margin:22px 0;padding-left:15px}header h1[data-v-77412d30]{margin:0 0 0 10px}table[data-v-77412d30]{margin:10px 0 0 15px}table .name-col[data-v-77412d30]{width:150px}.el-table--border[data-v-77412d30]:after,.el-table--group[data-v-77412d30]:after,.el-table[data-v-77412d30]:before{background-color:transparent}.poll ul[data-v-77412d30]{list-style-type:none;padding:0;width:30%}.image[data-v-77412d30]{width:20%}.image img[data-v-77412d30]{width:100%}.no-statuses[data-v-77412d30]{margin-left:28px;color:#606266}.recent-statuses-container[data-v-77412d30]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:67%}.recent-statuses-header[data-v-77412d30]{margin-top:10px}.statuses[data-v-77412d30]{padding:0 20px 0 0}.show-private[data-v-77412d30]{width:200px;text-align:left;line-height:67px;margin-right:20px}.show-private-statuses[data-v-77412d30]{margin-left:28px;margin-bottom:20px}.recent-statuses[data-v-77412d30]{margin-left:28px}.user-page-header[data-v-77412d30]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:0 20px}.user-page-header h1[data-v-77412d30]{display:inline}.user-profile-card[data-v-77412d30]{margin:0 20px;width:30%;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content}.user-profile-container[data-v-77412d30]{display:-webkit-box;display:-ms-flexbox;display:flex}.user-profile-table[data-v-77412d30]{margin:0}.user-profile-tag[data-v-77412d30]{margin:0 4px 4px 0}@media only screen and (max-width:480px){.avatar-name-container[data-v-77412d30]{margin-bottom:10px}.recent-statuses[data-v-77412d30]{margin:20px 10px 15px}.recent-statuses-container[data-v-77412d30]{width:100%;margin:0 10px}.show-private-statuses[data-v-77412d30]{margin:0 10px 20px}.user-page-header[data-v-77412d30]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding:0;margin:7px 0 15px 10px}.user-profile-card[data-v-77412d30]{margin:0 10px;width:95%}.user-profile-card td[data-v-77412d30]{width:80px}.user-profile-container[data-v-77412d30]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}@media only screen and (max-width:801px) and (min-width:481px){.avatar-name-container[data-v-77412d30]{margin-bottom:20px}.recent-statuses[data-v-77412d30]{margin:20px 10px 15px 0}.recent-statuses-container[data-v-77412d30]{width:97%;margin:0 20px}.show-private-statuses[data-v-77412d30]{margin:0 10px 20px 0}.user-page-header[data-v-77412d30]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding:0;margin:7px 0 20px 20px}.user-profile-card[data-v-77412d30]{margin:0 20px;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.user-profile-container[data-v-77412d30]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-136a.f1130f8e.css b/priv/static/adminfe/chunk-3384.2278f87c.css
similarity index 64%
rename from priv/static/adminfe/chunk-136a.f1130f8e.css
rename to priv/static/adminfe/chunk-3384.2278f87c.css
index f492b37d0..96e3273eb 100644
--- a/priv/static/adminfe/chunk-136a.f1130f8e.css
+++ b/priv/static/adminfe/chunk-3384.2278f87c.css
@@ -1 +1 @@
-.copy-popover{width:330px}.emoji-buttons{place-self:center;min-width:200px}.emoji-container-grid{display:grid;grid-template-columns:75px auto auto 200px;grid-column-gap:15px;margin-bottom:10px}.emoji-preview-img{max-width:100%;place-self:center}.emoji-info{place-self:center}.copy-pack-container{place-self:center stretch}.copy-pack-select{width:100%}.remote-emoji-container-grid{display:grid;grid-template-columns:75px auto auto 160px;grid-column-gap:15px;margin-bottom:10px}@media only screen and (max-width:480px){.emoji-container-flex{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;border:1px solid #dcdfe6;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;padding:15px;margin:0 15px 15px 0}.emoji-info,.emoji-preview-img{margin-bottom:10px}.emoji-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;width:100%}.emoji-buttons button{padding:10px 5px;width:47%}}@media only screen and (max-width:801px) and (min-width:481px){.emoji-container-grid{grid-column-gap:10px}.emoji-buttons .el-button+.el-button{margin-left:5px}.remote-emoji-container-grid{grid-column-gap:10px}}.add-new-emoji{height:36px;font-size:14px;font-weight:700;color:#606266}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.upload-button{margin-left:10px}.upload-file-url{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.new-emoji-uploader-form label.el-form-item__label{padding:0}}.download-archive{width:250px}.download-pack-button-container{width:265px}.download-pack-button-container .el-link,.download-pack-button-container .el-link span,.download-pack-button-container .el-link span .download-archive{width:inherit}.download-shared-pack{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.download-shared-pack-button{margin-left:10px}.el-collapse-item__content{padding-bottom:0}.el-collapse-item__header{height:36px;font-size:14px;font-weight:700;color:#606266}.emoji-pack-card{margin-top:5px}.emoji-pack-metadata .el-form-item{margin-bottom:10px}.has-background .el-collapse-item__header{background:#f6f6f6}.no-background .el-collapse-item__header{background:#fff}.pack-button-container{margin:0 0 18px 120px}.save-pack-button-container{margin-bottom:8px;width:265px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.delete-pack-button{width:45%}.download-pack-button-container{width:100%}.download-shared-pack{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.download-shared-pack-button{margin-left:0;margin-top:10px;padding:10px}.pack-button-container{width:100%;margin:0 0 22px}.remote-pack-metadata .el-form-item__content{line-height:24px;margin-top:4px}.save-pack-button{width:54%}.save-pack-button-container{margin-bottom:8px;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.save-pack-button-container button{padding:10px 5px}.save-pack-button-container .el-button+.el-button{margin-left:3px}}.emoji-packs-header-button-container{margin:0 0 22px 15px}.create-pack,.emoji-packs-header-button-container{display:-webkit-box;display:-ms-flexbox;display:flex}.create-pack{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.create-pack-button{margin-left:10px}.emoji-packs-form{margin:0 30px}.emoji-packs-header{margin:10px 0 20px 15px}.import-pack-button{margin-left:10px}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:22px}@media only screen and (min-width:1824px){.emoji-packs{max-width:1824px;margin:auto}}@media only screen and (max-width:480px){.create-pack{height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.create-pack-button{margin-left:0}.divider{margin:15px 0}.el-message{min-width:80%}.el-message-box{width:80%}.emoji-packs-form{margin:0 7px}.emoji-packs-form label{padding-right:8px}.emoji-packs-form .el-form-item{margin-bottom:15px}.emoji-packs-header{margin:15px}.emoji-packs-header-button-container{height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.emoji-packs-header-button-container .el-button+.el-button{margin:7px 0 0}.emoji-packs-header-button-container .el-button+.el-button,.reload-emoji-button{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}}
\ No newline at end of file
+.copy-popover{width:330px}.emoji-buttons{place-self:center;min-width:200px}.emoji-container-grid{display:grid;grid-template-columns:75px auto auto 200px;grid-column-gap:15px;margin-bottom:10px}.emoji-preview-img{max-width:100%;place-self:center}.emoji-info{place-self:center}.copy-pack-container{place-self:center stretch}.copy-pack-select{width:100%}.remote-emoji-container-grid{display:grid;grid-template-columns:75px auto auto 160px;grid-column-gap:15px;margin-bottom:10px}@media only screen and (max-width:480px){.emoji-container-flex{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;border:1px solid #dcdfe6;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;padding:15px;margin:0 15px 15px 0}.emoji-info,.emoji-preview-img{margin-bottom:10px}.emoji-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;width:100%}.emoji-buttons button{padding:10px 5px;width:47%}}@media only screen and (max-width:801px) and (min-width:481px){.emoji-container-grid{grid-column-gap:10px}.emoji-buttons .el-button+.el-button{margin-left:5px}.remote-emoji-container-grid{grid-column-gap:10px}}.add-new-emoji{height:36px;font-size:14px;font-weight:700;color:#606266}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.upload-button{margin-left:10px}.upload-file-url{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.new-emoji-uploader-form label.el-form-item__label{padding:0}}.download-archive{width:250px}.download-pack-button-container{width:265px}.download-pack-button-container .el-link,.download-pack-button-container .el-link span,.download-pack-button-container .el-link span .download-archive{width:inherit}.download-shared-pack{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.download-shared-pack-button{margin-left:10px}.el-collapse-item__content{padding-bottom:0}.el-collapse-item__header{height:36px;font-size:14px;font-weight:700;color:#606266}.emoji-pack-card{margin-top:5px}.emoji-pack-metadata .el-form-item{margin-bottom:10px}.has-background .el-collapse-item__header{background:#f6f6f6}.no-background .el-collapse-item__header{background:#fff}.pack-button-container{margin:0 0 18px 120px}.save-pack-button-container{margin-bottom:8px;width:265px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.delete-pack-button{width:45%}.download-pack-button-container{width:100%}.download-shared-pack{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.download-shared-pack-button{margin-left:0;margin-top:10px;padding:10px}.pack-button-container{width:100%;margin:0 0 22px}.remote-pack-metadata .el-form-item__content{line-height:24px;margin-top:4px}.save-pack-button{width:54%}.save-pack-button-container{margin-bottom:8px;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.save-pack-button-container button{padding:10px 5px}.save-pack-button-container .el-button+.el-button{margin-left:3px}}.emoji-header-container{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin:0 15px 22px}.create-pack,.emoji-header-container,.emoji-packs-header-button-container{display:-webkit-box;display:-ms-flexbox;display:flex}.create-pack{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.create-pack-button{margin-left:10px}.emoji-packs-form{margin:0 30px}.emoji-packs-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin:10px 15px 15px}.import-pack-button{margin-left:10px}h1{margin:0}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:22px}.reboot-button{padding:10px;margin:0;width:145px}@media only screen and (min-width:1824px){.emoji-packs{max-width:1824px;margin:auto}}@media only screen and (max-width:480px){.create-pack{height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.create-pack-button{margin-left:0}.divider{margin:15px 0}.el-message{min-width:80%}.el-message-box{width:80%}.emoji-header-container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.emoji-packs-form{margin:0 7px}.emoji-packs-form label{padding-right:8px}.emoji-packs-form .el-form-item{margin-bottom:15px}.emoji-packs-header{margin:15px}.emoji-packs-header-button-container{height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.emoji-packs-header-button-container .el-button+.el-button{margin:7px 0 0}.emoji-packs-header-button-container .el-button+.el-button,.reload-emoji-button{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-4011.c4799067.css b/priv/static/adminfe/chunk-4011.c4799067.css
new file mode 100644
index 000000000..1fb099c0c
--- /dev/null
+++ b/priv/static/adminfe/chunk-4011.c4799067.css
@@ -0,0 +1 @@
+a{text-decoration:underline}.center-label label{text-align:center}.center-label label span{float:left}.code{background-color:rgba(173,190,214,.48);border-radius:3px;font-family:monospace;padding:0 3px}.delete-setting-button{margin-left:5px}.description>p{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;line-height:20px;margin:0 0 14px}.description>p code{display:inline;padding:2px 3px;font-size:14px}.description-container{overflow-wrap:break-word;margin-bottom:0}.divider{margin:0 0 18px}.divider.thick-line{height:2px}.docs-search-container{float:right;margin-right:30px}.editable-keyword-container{width:100%}.el-form-item .rate-limit{margin-right:0}.el-input-group__prepend{padding-left:10px;padding-right:10px}.el-tabs__header{z-index:3000}.esshd-list{margin:0}.expl,.expl>p{color:#666;font-size:13px;line-height:22px;margin:5px 0 0;overflow-wrap:break-word;overflow:hidden;text-overflow:ellipsis}.expl>p code,.expl code{display:inline;line-height:22px;font-size:13px;padding:2px 3px}.follow-relay{width:350px;margin-right:7px}.form-container{margin-bottom:80px}.grouped-settings-header{margin:0 0 14px}.highlight{background-color:#e6e6e6}.icons-button-container{width:100%;margin-bottom:10px}.icons-button-desc{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;margin-left:5px}.icon-container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:95%}.icon-values-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0 10px 10px 0}.icon-key-input{width:30%;margin-right:8px}.icon-minus-button{width:36px;height:36px}.icon-value-input{width:70%;margin-left:8px}.icons-container,.input-container{display:-webkit-box;display:-ms-flexbox;display:flex}.input-container{-webkit-box-align:start;-ms-flex-align:start;align-items:start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.input-container .el-form-item{margin-right:30px;width:100%}.input-container .el-select,.keyword-container{width:100%}label{overflow:hidden;text-overflow:ellipsis}.label-font{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700}.limit-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.limit-expl{margin-left:10px}.limit-input{width:47%;margin:0 0 5px 1%}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:18px}.mascot{margin-bottom:15px}.mascot-container{width:100%}.mascot-input{margin-bottom:7px}.mascot-name-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:7px}.mascot-name-input{margin-right:10px}.multiple-select-container{width:100%}.name-input{width:30%;margin-right:8px}.pattern-input{width:20%;margin-right:8px}.proxy-url-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:10px;width:100%}.proxy-url-host-input{width:35%;margin-right:8px}.proxy-url-value-input{width:35%;margin-left:8px;margin-right:10px}.prune-options{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.prune-options .el-radio{margin-top:11px}.rate-limit .el-form-item__content{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex}.rate-limit-container,.rate-limit-content{width:100%}.rate-limit-label{float:right}.rate-limit-label-container{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;height:36px;width:100%;margin-right:10px}.reboot-button{width:145px;text-align:left;padding:10px;float:right;margin:0 30px 0 0}.reboot-button-container{width:100%;position:fixed;top:60px;right:0;z-index:2000}.relays-container{margin:0 15px}.replacement-input{width:80%;margin-left:8px;margin-right:10px}.scale-input{width:47%;margin:0 1% 5px 0}.setting-input{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.settings-container{max-width:1824px;margin:auto}.settings-container .el-tabs{margin-top:20px}.settings-delete-button{margin-left:5px}.settings-docs-button{width:163px;text-align:left;padding:10px}.settings-header{margin:10px 15px 15px}.header-sidebar-opened{max-width:1585px}.header-sidebar-closed{max-width:1728px}.settings-header-container{height:87px}.settings-search-input{width:350px;margin-left:5px}.single-input{margin-right:10px}.socks5-checkbox{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;margin-left:10px}.socks5-checkbox-container{width:40%;height:36px;margin-right:5px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ssl-tls-opts{margin:36px 0 0}.submit-button{float:right;margin:0 30px 22px 0}.submit-button-container{width:100%;position:fixed;bottom:0;right:0;z-index:2000}.switch-input{height:36px}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.value-input{width:70%;margin-left:8px;margin-right:10px}@media only screen and (min-width:1824px){.header-sidebar-closed{max-width:1772px}.header-sidebar-opened{max-width:1630px}.reboot-button-container{width:100%;max-width:inherit;margin-left:auto;margin-right:auto;right:auto}.reboot-sidebar-opened{max-width:1630px}.reboot-sidebar-closed{max-width:1772px}.sidebar-closed{max-width:1586px}.sidebar-opened{max-width:1442px}.submit-button-container{width:100%;max-width:inherit;margin-left:auto;margin-right:auto;right:auto}}@media only screen and (max-width:480px){.crontab,.crontab label{width:100%}.delete-setting-button{margin:4px 0 0 5px;height:28px}.delete-setting-button-container{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p{line-height:18px;margin:0 5px 7px 15px}.description>p code{display:inline;line-height:18px;padding:2px 3px;font-size:14px}.divider{margin:0 0 10px}.divider .thick-line{height:2px}.follow-relay{width:70%;margin-right:5px}.follow-relay input{width:100%}.follow-relay-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}h1{font-size:24px}.input{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container{width:100%}.input-container .el-form-item:first-child{margin:0;padding:0 15px 10px}.input-container .el-form-item.crontab-container:first-child{margin:0;padding:0}.input-container .el-form-item:first-child .mascot-form-item,.input-container .el-form-item:first-child .rate-limit{padding:0}.input-container .settings-delete-button{margin-top:4px;float:right}.input-row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.label-with-margin{margin-left:15px}.limit-input{width:45%}.nav-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px}.proxy-url-input{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:0}.proxy-url-host-input{width:100%;margin-bottom:5px}.proxy-url-value-input{width:100%;margin-left:0}.prune-options{height:80px}.prune-options,.rate-limit .el-form-item__content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.rate-limit-label{float:left}.reboot-button{margin:0 15px 0 0}.reboot-button-container{top:57px}.scale-input{width:45%}.setting-label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.settings-header{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;display:inline-block;margin:10px 15px 15px}.docs-search-container{float:right}.settings-search-input{width:100%;margin-left:0}.settings-search-input-container{margin:0 15px 15px}.settings-menu{width:163px;margin-right:5px}.socks5-checkbox-container{width:100%}.submit-button{margin:0 15px 22px 0}.el-input__inner{padding:0 5px}.el-form-item__label:not(.no-top-margin){padding-bottom:5px;line-height:22px;margin-top:7px;width:100%}.el-form-item__label:not(.no-top-margin) span{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.el-message{min-width:80%}.el-select__tags{overflow:hidden}.expl,.expl>p{line-height:16px}.icon-key-input{width:40%;margin-right:4px}.icon-minus-button{width:28px;height:28px;margin-top:4px}.icon-values-container{margin:0 7px 7px 0}.icon-value-input{width:60%;margin-left:4px}.icons-button-container{line-height:24px}.line{margin-bottom:10px}.mascot-form-item .el-form-item__label:not(.no-top-margin){margin:0;padding:0}.mascot-container{margin-bottom:5px}.name-input{width:40%;margin-right:5px}p.expl{line-height:20px}.pattern-input{width:40%;margin-right:4px}.relays-container{margin:0 10px}.replacement-input{width:60%;margin-left:4px;margin-right:5px}.settings-header-container{height:45px}.value-input{width:60%;margin-left:5px;margin-right:8px}}@media only screen and (max-width:818px) and (min-width:481px){.delete-setting-button{margin:4px 0 0 10px;height:28px}.delete-setting-button-container{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p{line-height:18px;margin:0 15px 10px 0}.icon-minus-button{width:28px;height:28px;margin-top:4px}.input{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container .el-form-item__label span{margin-left:10px}.input-row,.nav-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.nav-container{height:36px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 30px 15px 15px}.rate-limit-label-container{width:250px}.settings-delete-button{float:right}.settings-header-container{height:36px}.settings-search-input{width:250px;margin:0 0 15px 15px}}a[data-v-667428a6]{text-decoration:underline}.center-label label[data-v-667428a6]{text-align:center}.center-label label span[data-v-667428a6]{float:left}.code[data-v-667428a6]{background-color:rgba(173,190,214,.48);border-radius:3px;font-family:monospace;padding:0 3px}.delete-setting-button[data-v-667428a6]{margin-left:5px}.description>p[data-v-667428a6]{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;line-height:20px;margin:0 0 14px}.description>p code[data-v-667428a6]{display:inline;padding:2px 3px;font-size:14px}.description-container[data-v-667428a6]{overflow-wrap:break-word;margin-bottom:0}.divider[data-v-667428a6]{margin:0 0 18px}.divider.thick-line[data-v-667428a6]{height:2px}.docs-search-container[data-v-667428a6]{float:right;margin-right:30px}.editable-keyword-container[data-v-667428a6]{width:100%}.el-form-item .rate-limit[data-v-667428a6]{margin-right:0}.el-input-group__prepend[data-v-667428a6]{padding-left:10px;padding-right:10px}.el-tabs__header[data-v-667428a6]{z-index:3000}.esshd-list[data-v-667428a6]{margin:0}.expl>p[data-v-667428a6],.expl[data-v-667428a6]{color:#666;font-size:13px;line-height:22px;margin:5px 0 0;overflow-wrap:break-word;overflow:hidden;text-overflow:ellipsis}.expl>p code[data-v-667428a6],.expl code[data-v-667428a6]{display:inline;line-height:22px;font-size:13px;padding:2px 3px}.follow-relay[data-v-667428a6]{width:350px;margin-right:7px}.form-container[data-v-667428a6]{margin-bottom:80px}.grouped-settings-header[data-v-667428a6]{margin:0 0 14px}.highlight[data-v-667428a6]{background-color:#e6e6e6}.icons-button-container[data-v-667428a6]{width:100%;margin-bottom:10px}.icons-button-desc[data-v-667428a6]{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;margin-left:5px}.icon-container[data-v-667428a6]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:95%}.icon-values-container[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0 10px 10px 0}.icon-key-input[data-v-667428a6]{width:30%;margin-right:8px}.icon-minus-button[data-v-667428a6]{width:36px;height:36px}.icon-value-input[data-v-667428a6]{width:70%;margin-left:8px}.icons-container[data-v-667428a6],.input-container[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex}.input-container[data-v-667428a6]{-webkit-box-align:start;-ms-flex-align:start;align-items:start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.input-container .el-form-item[data-v-667428a6]{margin-right:30px;width:100%}.input-container .el-select[data-v-667428a6],.keyword-container[data-v-667428a6]{width:100%}label[data-v-667428a6]{overflow:hidden;text-overflow:ellipsis}.label-font[data-v-667428a6]{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700}.limit-button-container[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.limit-expl[data-v-667428a6]{margin-left:10px}.limit-input[data-v-667428a6]{width:47%;margin:0 0 5px 1%}.line[data-v-667428a6]{width:100%;height:0;border:1px solid #eee;margin-bottom:18px}.mascot[data-v-667428a6]{margin-bottom:15px}.mascot-container[data-v-667428a6]{width:100%}.mascot-input[data-v-667428a6]{margin-bottom:7px}.mascot-name-container[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:7px}.mascot-name-input[data-v-667428a6]{margin-right:10px}.multiple-select-container[data-v-667428a6]{width:100%}.name-input[data-v-667428a6]{width:30%;margin-right:8px}.pattern-input[data-v-667428a6]{width:20%;margin-right:8px}.proxy-url-input[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:10px;width:100%}.proxy-url-host-input[data-v-667428a6]{width:35%;margin-right:8px}.proxy-url-value-input[data-v-667428a6]{width:35%;margin-left:8px;margin-right:10px}.prune-options[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.prune-options .el-radio[data-v-667428a6]{margin-top:11px}.rate-limit .el-form-item__content[data-v-667428a6]{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex}.rate-limit-container[data-v-667428a6],.rate-limit-content[data-v-667428a6]{width:100%}.rate-limit-label[data-v-667428a6]{float:right}.rate-limit-label-container[data-v-667428a6]{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;height:36px;width:100%;margin-right:10px}.reboot-button[data-v-667428a6]{width:145px;text-align:left;padding:10px;float:right;margin:0 30px 0 0}.reboot-button-container[data-v-667428a6]{width:100%;position:fixed;top:60px;right:0;z-index:2000}.relays-container[data-v-667428a6]{margin:0 15px}.replacement-input[data-v-667428a6]{width:80%;margin-left:8px;margin-right:10px}.scale-input[data-v-667428a6]{width:47%;margin:0 1% 5px 0}.setting-input[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.settings-container[data-v-667428a6]{max-width:1824px;margin:auto}.settings-container .el-tabs[data-v-667428a6]{margin-top:20px}.settings-delete-button[data-v-667428a6]{margin-left:5px}.settings-docs-button[data-v-667428a6]{width:163px;text-align:left;padding:10px}.settings-header[data-v-667428a6]{margin:10px 15px 15px}.header-sidebar-opened[data-v-667428a6]{max-width:1585px}.header-sidebar-closed[data-v-667428a6]{max-width:1728px}.settings-header-container[data-v-667428a6]{height:87px}.settings-search-input[data-v-667428a6]{width:350px;margin-left:5px}.single-input[data-v-667428a6]{margin-right:10px}.socks5-checkbox[data-v-667428a6]{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;margin-left:10px}.socks5-checkbox-container[data-v-667428a6]{width:40%;height:36px;margin-right:5px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ssl-tls-opts[data-v-667428a6]{margin:36px 0 0}.submit-button[data-v-667428a6]{float:right;margin:0 30px 22px 0}.submit-button-container[data-v-667428a6]{width:100%;position:fixed;bottom:0;right:0;z-index:2000}.switch-input[data-v-667428a6]{height:36px}.text[data-v-667428a6]{line-height:20px;margin-right:15px}.upload-container[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.value-input[data-v-667428a6]{width:70%;margin-left:8px;margin-right:10px}@media only screen and (min-width:1824px){.header-sidebar-closed[data-v-667428a6]{max-width:1772px}.header-sidebar-opened[data-v-667428a6]{max-width:1630px}.reboot-button-container[data-v-667428a6]{width:100%;max-width:inherit;margin-left:auto;margin-right:auto;right:auto}.reboot-sidebar-opened[data-v-667428a6]{max-width:1630px}.reboot-sidebar-closed[data-v-667428a6]{max-width:1772px}.sidebar-closed[data-v-667428a6]{max-width:1586px}.sidebar-opened[data-v-667428a6]{max-width:1442px}.submit-button-container[data-v-667428a6]{width:100%;max-width:inherit;margin-left:auto;margin-right:auto;right:auto}}@media only screen and (max-width:480px){.crontab[data-v-667428a6],.crontab label[data-v-667428a6]{width:100%}.delete-setting-button[data-v-667428a6]{margin:4px 0 0 5px;height:28px}.delete-setting-button-container[data-v-667428a6]{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p[data-v-667428a6]{line-height:18px;margin:0 5px 7px 15px}.description>p code[data-v-667428a6]{display:inline;line-height:18px;padding:2px 3px;font-size:14px}.divider[data-v-667428a6]{margin:0 0 10px}.divider .thick-line[data-v-667428a6]{height:2px}.follow-relay[data-v-667428a6]{width:70%;margin-right:5px}.follow-relay input[data-v-667428a6]{width:100%}.follow-relay-container[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}h1[data-v-667428a6]{font-size:24px}.input[data-v-667428a6]{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container[data-v-667428a6]{width:100%}.input-container .el-form-item[data-v-667428a6]:first-child{margin:0;padding:0 15px 10px}.input-container .el-form-item.crontab-container[data-v-667428a6]:first-child{margin:0;padding:0}.input-container .el-form-item:first-child .mascot-form-item[data-v-667428a6],.input-container .el-form-item:first-child .rate-limit[data-v-667428a6]{padding:0}.input-container .settings-delete-button[data-v-667428a6]{margin-top:4px;float:right}.input-row[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.label-with-margin[data-v-667428a6]{margin-left:15px}.limit-input[data-v-667428a6]{width:45%}.nav-container[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px}.proxy-url-input[data-v-667428a6]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:0}.proxy-url-host-input[data-v-667428a6]{width:100%;margin-bottom:5px}.proxy-url-value-input[data-v-667428a6]{width:100%;margin-left:0}.prune-options[data-v-667428a6]{height:80px}.prune-options[data-v-667428a6],.rate-limit .el-form-item__content[data-v-667428a6]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.rate-limit-label[data-v-667428a6]{float:left}.reboot-button[data-v-667428a6]{margin:0 15px 0 0}.reboot-button-container[data-v-667428a6]{top:57px}.scale-input[data-v-667428a6]{width:45%}.setting-label[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.settings-header[data-v-667428a6]{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;display:inline-block;margin:10px 15px 15px}.docs-search-container[data-v-667428a6]{float:right}.settings-search-input[data-v-667428a6]{width:100%;margin-left:0}.settings-search-input-container[data-v-667428a6]{margin:0 15px 15px}.settings-menu[data-v-667428a6]{width:163px;margin-right:5px}.socks5-checkbox-container[data-v-667428a6]{width:100%}.submit-button[data-v-667428a6]{margin:0 15px 22px 0}.el-input__inner[data-v-667428a6]{padding:0 5px}.el-form-item__label[data-v-667428a6]:not(.no-top-margin){padding-bottom:5px;line-height:22px;margin-top:7px;width:100%}.el-form-item__label:not(.no-top-margin) span[data-v-667428a6]{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.el-message[data-v-667428a6]{min-width:80%}.el-select__tags[data-v-667428a6]{overflow:hidden}.expl>p[data-v-667428a6],.expl[data-v-667428a6]{line-height:16px}.icon-key-input[data-v-667428a6]{width:40%;margin-right:4px}.icon-minus-button[data-v-667428a6]{width:28px;height:28px;margin-top:4px}.icon-values-container[data-v-667428a6]{margin:0 7px 7px 0}.icon-value-input[data-v-667428a6]{width:60%;margin-left:4px}.icons-button-container[data-v-667428a6]{line-height:24px}.line[data-v-667428a6]{margin-bottom:10px}.mascot-form-item .el-form-item__label[data-v-667428a6]:not(.no-top-margin){margin:0;padding:0}.mascot-container[data-v-667428a6]{margin-bottom:5px}.name-input[data-v-667428a6]{width:40%;margin-right:5px}p.expl[data-v-667428a6]{line-height:20px}.pattern-input[data-v-667428a6]{width:40%;margin-right:4px}.relays-container[data-v-667428a6]{margin:0 10px}.replacement-input[data-v-667428a6]{width:60%;margin-left:4px;margin-right:5px}.settings-header-container[data-v-667428a6]{height:45px}.value-input[data-v-667428a6]{width:60%;margin-left:5px;margin-right:8px}}@media only screen and (max-width:818px) and (min-width:481px){.delete-setting-button[data-v-667428a6]{margin:4px 0 0 10px;height:28px}.delete-setting-button-container[data-v-667428a6]{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p[data-v-667428a6]{line-height:18px;margin:0 15px 10px 0}.icon-minus-button[data-v-667428a6]{width:28px;height:28px;margin-top:4px}.input[data-v-667428a6]{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container .el-form-item__label span[data-v-667428a6]{margin-left:10px}.input-row[data-v-667428a6],.nav-container[data-v-667428a6]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.nav-container[data-v-667428a6]{height:36px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 30px 15px 15px}.rate-limit-label-container[data-v-667428a6]{width:250px}.settings-delete-button[data-v-667428a6]{float:right}.settings-header-container[data-v-667428a6]{height:36px}.settings-search-input[data-v-667428a6]{width:250px;margin:0 0 15px 15px}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-46ef.145de4f9.css b/priv/static/adminfe/chunk-46ef.145de4f9.css
deleted file mode 100644
index deb5249ac..000000000
--- a/priv/static/adminfe/chunk-46ef.145de4f9.css
+++ /dev/null
@@ -1 +0,0 @@
-.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:20px 15px 15px}.invites-container .create-invite-token{text-align:left;width:350px;padding:10px}.invites-container .create-new-token-dialog{width:40%}.invites-container .el-dialog__body{padding:5px 20px 0}.invites-container h1{margin:10px 0 0 15px}.invites-container .icon{margin-right:5px}.invites-container .invite-token-table{width:100%;margin:0 15px}.invites-container .invite-via-email{text-align:left;width:350px;padding:10px}.invites-container .invite-via-email-dialog{width:50%}.invites-container .info{color:#666;font-size:13px;line-height:22px;margin:0 0 10px}@media only screen and (max-width:480px){.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 10px 7px}.invites-container .cell{padding:0}.invites-container .create-invite-token{width:100%}.invites-container .create-new-token-dialog{width:85%}.invites-container .el-date-editor{width:150px}.invites-container .el-dialog__body{padding:5px 15px 0}.invites-container h1{margin:7px 10px 15px}.invites-container .invite-token-table{width:100%;margin:0 5px;font-size:12px;font-weight:500}.invites-container .invite-via-email{width:100%;margin:10px 0 0}.invites-container .invite-via-email-dialog{width:85%}.invites-container .info{margin:0 0 10px 5px}.invites-container th .cell{padding:0}.create-invite-token,.invite-via-email{width:100%}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-6b68.0cc00484.css b/priv/static/adminfe/chunk-6b68.0cc00484.css
new file mode 100644
index 000000000..7061b3d03
--- /dev/null
+++ b/priv/static/adminfe/chunk-6b68.0cc00484.css
@@ -0,0 +1 @@
+.actions-button[data-v-3850612b]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-3850612b]{float:right}.el-icon-edit[data-v-3850612b]{margin-right:5px}.tag-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-3850612b]{padding-right:20px}.no-hover[data-v-3850612b]:hover{color:#606266;background-color:#fff;cursor:auto}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-4ffb.dd09fe2e.css b/priv/static/adminfe/chunk-6e81.0e80d020.css
similarity index 100%
rename from priv/static/adminfe/chunk-4ffb.dd09fe2e.css
rename to priv/static/adminfe/chunk-6e81.0e80d020.css
diff --git a/priv/static/adminfe/chunk-7637.941c4edb.css b/priv/static/adminfe/chunk-7637.941c4edb.css
new file mode 100644
index 000000000..be1d183a9
--- /dev/null
+++ b/priv/static/adminfe/chunk-7637.941c4edb.css
@@ -0,0 +1 @@
+.moderation-log-container[data-v-103bba83]{margin:0 15px}h1[data-v-103bba83]{margin:0}.el-timeline[data-v-103bba83]{margin:25px 45px 0 0;padding:0}.moderation-log-date-panel[data-v-103bba83]{width:350px}.moderation-log-header-container[data-v-103bba83]{-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:10px 0 15px}.moderation-log-header-container[data-v-103bba83],.moderation-log-nav-container[data-v-103bba83]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.moderation-log-search[data-v-103bba83]{width:350px}.moderation-log-user-select[data-v-103bba83]{margin:0 0 20px;width:350px}.reboot-button[data-v-103bba83]{padding:10px;margin:0;width:145px}.search-container[data-v-103bba83]{text-align:right}.pagination[data-v-103bba83]{text-align:center}@media only screen and (max-width:480px){h1[data-v-103bba83]{font-size:24px}.moderation-log-date-panel[data-v-103bba83]{width:100%}.moderation-log-user-select[data-v-103bba83]{margin:0 0 10px;width:55%}.moderation-log-search[data-v-103bba83]{width:40%}}@media only screen and (max-width:801px) and (min-width:481px){.moderation-log-date-panel[data-v-103bba83]{width:55%}.moderation-log-user-select[data-v-103bba83]{margin:0 0 10px;width:55%}.moderation-log-search[data-v-103bba83]{width:40%}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-87b3.3c6ede9c.css b/priv/static/adminfe/chunk-87b3.3c6ede9c.css
deleted file mode 100644
index f0e6bf4ee..000000000
--- a/priv/static/adminfe/chunk-87b3.3c6ede9c.css
+++ /dev/null
@@ -1 +0,0 @@
-a{text-decoration:underline}.center-label label{text-align:center}.center-label label span{float:left}.code{background-color:rgba(173,190,214,.48);border-radius:3px;font-family:monospace;padding:0 3px}.delete-setting-button{margin-left:5px}.description>p{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;line-height:20px;margin:0 0 14px}.description>p code{display:inline;padding:2px 3px;font-size:14px}.description-container{overflow-wrap:break-word;margin-bottom:0}.divider{margin:0 0 18px}.divider.thick-line{height:2px}.editable-keyword-container{width:100%}.el-form-item .rate-limit{margin-right:0}.el-input-group__prepend{padding-left:10px;padding-right:10px}.esshd-list{margin:0}.expl,.expl>p{color:#666;font-size:13px;line-height:22px;margin:5px 0 0;overflow-wrap:break-word;overflow:hidden;text-overflow:ellipsis}.expl>p code,.expl code{display:inline;line-height:22px;font-size:13px;padding:2px 3px}.follow-relay{width:350px;margin-right:7px}.form-container{margin-bottom:80px}.grouped-settings-header{margin:0 0 14px}.highlight{background-color:#e6e6e6}.icons-button-container{width:100%;margin-bottom:10px}.icons-button-desc{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;margin-left:5px}.icon-container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:95%}.icon-values-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0 10px 10px 0}.icon-key-input{width:30%;margin-right:8px}.icon-minus-button{width:36px;height:36px}.icon-value-input{width:70%;margin-left:8px}.icons-container,.input-container{display:-webkit-box;display:-ms-flexbox;display:flex}.input-container{-webkit-box-align:start;-ms-flex-align:start;align-items:start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.input-container .el-form-item{margin-right:30px;width:100%}.input-container .el-select,.keyword-container{width:100%}label{overflow:hidden;text-overflow:ellipsis}.label-font{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700}.limit-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.limit-expl{margin-left:10px}.limit-input{width:47%;margin:0 0 5px 1%}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:18px}.mascot{margin-bottom:15px}.mascot-container{width:100%}.mascot-input{margin-bottom:7px}.mascot-name-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:7px}.mascot-name-input{margin-right:10px}.multiple-select-container{width:100%}.name-input{width:30%;margin-right:8px}.pattern-input{width:20%;margin-right:8px}.proxy-url-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:10px;width:100%}.proxy-url-host-input{width:35%;margin-right:8px}.proxy-url-value-input{width:35%;margin-left:8px;margin-right:10px}.prune-options{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.prune-options .el-radio{margin-top:11px}.rate-limit .el-form-item__content{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex}.rate-limit-container,.rate-limit-content{width:100%}.rate-limit-label{float:right}.rate-limit-label-container{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;height:36px;width:100%;margin-right:10px}.relays-container{margin:0 15px}.replacement-input{width:80%;margin-left:8px;margin-right:10px}.scale-input{width:47%;margin:0 1% 5px 0}.setting-input{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.settings-container{max-width:1824px;margin:auto}.settings-container .el-tabs{margin-top:20px}.settings-delete-button{margin-left:5px}.settings-docs-button{width:163px;text-align:left;padding:10px}.settings-header{margin:0}.header-sidebar-opened{max-width:1585px}.header-sidebar-closed{max-width:1728px}.settings-header-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 30px 15px 15px}.settings-reboot-button{width:145px;text-align:left;padding:10px;margin-right:5px}.single-input{margin-right:10px}.socks5-checkbox{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;margin-left:10px}.socks5-checkbox-container{width:40%;height:36px;margin-right:5px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ssl-tls-opts{margin:36px 0 0}.submit-button{float:right;margin:0 30px 22px 0}.submit-button-container{width:100%;position:fixed;bottom:0;right:0;z-index:10000}.switch-input{height:36px}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.value-input{width:70%;margin-left:8px;margin-right:10px}@media only screen and (min-width:1824px){.sidebar-closed{max-width:1586px}.sidebar-opened{max-width:1442px}.submit-button-container{width:100%;max-width:inherit;margin-left:auto;margin-right:auto;right:auto}}@media only screen and (max-width:480px){.crontab,.crontab label{width:100%}.delete-setting-button{margin:4px 0 0 5px;height:28px}.delete-setting-button-container{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p{line-height:18px;margin:0 5px 7px 0}.description>p code{display:inline;line-height:18px;padding:2px 3px;font-size:14px}.divider{margin:0 0 10px}.divider .thick-line{height:2px}.follow-relay{width:70%;margin-right:5px}.follow-relay input{width:100%}.follow-relay-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.input{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container{width:100%}.input-container .el-form-item:first-child{margin:0;padding:0 15px 10px 0}.input-container .el-form-item.crontab-container:first-child{margin:0;padding:0}.input-container .el-form-item:first-child .mascot-form-item,.input-container .el-form-item:first-child .rate-limit{padding:0}.input-container .settings-delete-button{margin-top:4px;float:right}.input-row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.limit-input{width:45%}.proxy-url-input{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:0}.proxy-url-host-input{width:100%;margin-bottom:5px}.proxy-url-value-input{width:100%;margin-left:0}.prune-options{height:80px}.prune-options,.rate-limit .el-form-item__content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.rate-limit-label{float:left}.scale-input{width:45%}.setting-label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.settings-header{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;display:inline-block;margin:0}.settings-header-container{margin:10px 15px 15px}.nav-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px}.settings-menu{width:163px}.socks5-checkbox-container{width:100%}.submit-button{margin:0 15px 22px 0}.el-input__inner{padding:0 5px}.el-form-item__label:not(.no-top-margin){padding-left:3px;padding-right:10px;line-height:22px;margin-top:7px}.el-message{min-width:80%}.el-select__tags{overflow:hidden}.expl,.expl>p{line-height:16px}.icon-key-input{width:40%;margin-right:4px}.icon-minus-button{width:28px;height:28px;margin-top:4px}.icon-values-container{margin:0 7px 7px 0}.icon-value-input{width:60%;margin-left:4px}.icons-button-container{line-height:24px}.line{margin-bottom:10px}.mascot-container{margin-bottom:5px}.name-input{width:40%;margin-right:5px}p.expl{line-height:20px}.pattern-input{width:40%;margin-right:4px}.relays-container{margin:0 10px}.replacement-input{width:60%;margin-left:4px;margin-right:5px}.value-input{width:60%;margin-left:5px;margin-right:8px}}@media only screen and (max-width:818px) and (min-width:481px){.delete-setting-button{margin:4px 0 0 10px;height:28px}.delete-setting-button-container{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p{line-height:18px;margin:0 15px 10px 0}.icon-minus-button{width:28px;height:28px;margin-top:4px}.input{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container .el-form-item__label span{margin-left:10px}.input-row,.nav-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.nav-container{height:36px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 30px 15px 15px}.rate-limit-label-container{width:250px}.settings-delete-button{float:right}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-88c9.184084df.css b/priv/static/adminfe/chunk-88c9.184084df.css
deleted file mode 100644
index f3299f33b..000000000
--- a/priv/static/adminfe/chunk-88c9.184084df.css
+++ /dev/null
@@ -1 +0,0 @@
-a{text-decoration:underline}.note-header{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.note-actor{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.note-actor-name{margin:0;height:22px}.note-avatar-img{width:15px;height:15px;margin-right:5px}.note-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.note-card{margin-bottom:15px}.note-content{font-size:15px}.note-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.el-card__header{padding:10px 17px}.note-header{height:80px}.note-actor-container{margin-bottom:5px}.note-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.account{text-decoration:underline}.avatar-img{vertical-align:bottom;width:15px;height:15px;margin-left:5px}.divider{margin:15px 0}.el-card__body{padding:17px}.el-card__header{background-color:#fafafa;padding:10px 20px}.el-collapse{border-bottom:none}.el-collapse-item__header{height:46px;font-size:14px}.el-collapse-item__content{padding-bottom:7px}.el-icon-arrow-right{margin-right:6px}.el-icon-close{padding:10px 5px 10px 10px;cursor:pointer}h4{margin:0;height:17px}.report .header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.id{color:grey;margin-top:6px}.line{width:100%;height:0;border:.5px solid #ebeef5;margin:15px 0}.new-note p{font-size:14px;font-weight:500;height:17px;margin:13px 0 7px}.note{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.1);box-shadow:0 2px 5px 0 rgba(0,0,0,.1);margin-bottom:10px}.no-notes{font-style:italic;color:grey}.report-row-key{font-weight:500;font-size:14px}.report-title{margin:0}.report-note-form{margin:15px 0 0}.report-post-note{margin:5px 0 0;text-align:right}.reports-pagination{margin:25px 0;text-align:center}.reports-timeline{margin:30px 45px 45px 19px;padding:0}.statuses{margin-top:15px}.submit-button{display:block;margin:7px 0 17px auto}.timestamp{margin:0;font-style:italic;color:grey}@media only screen and (max-width:480px){.report .header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;height:auto}.report .id{margin:6px 0 0}.report .report-actions-button,.report .report-tag{margin:3px 0 6px}.report .title-container{margin-bottom:7px}.reports-timeline{margin:20px 10px}.reports-timeline .el-timeline-item__wrapper{padding-left:20px}}.select-field[data-v-ecc36f5a]{width:350px}@media only screen and (max-width:480px){.select-field[data-v-ecc36f5a]{width:100%;margin-bottom:5px}}@media only screen and (max-width:801px) and (min-width:481px){.select-field[data-v-ecc36f5a]{width:50%}}.reports-container .reports-filter-container[data-v-0a3cd0a0]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin:22px 15px;padding-bottom:0}.reports-container h1[data-v-0a3cd0a0]{margin:10px 0 0 15px}.reports-container .no-reports-message[data-v-0a3cd0a0]{color:grey;margin-left:19px}.reports-container .report-count[data-v-0a3cd0a0]{color:grey;font-size:28px}@media only screen and (max-width:480px){.reports-container h1[data-v-0a3cd0a0]{margin:7px 10px 15px}.reports-container .reports-filter-container[data-v-0a3cd0a0]{margin:0 10px}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-970d.f59cca8c.css b/priv/static/adminfe/chunk-970d.f59cca8c.css
new file mode 100644
index 000000000..15511f12f
--- /dev/null
+++ b/priv/static/adminfe/chunk-970d.f59cca8c.css
@@ -0,0 +1 @@
+a{text-decoration:underline}.note-header{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.note-actor{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.note-actor-name{margin:0;height:22px}.note-avatar-img{width:15px;height:15px;margin-right:5px}.note-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.note-card{margin-bottom:15px}.note-content{font-size:15px}.note-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.el-card__header{padding:10px 17px}.note-header{height:80px}.note-actor-container{margin-bottom:5px}.note-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.account{text-decoration:underline}.avatar-img{vertical-align:bottom;width:15px;height:15px;margin-left:5px}.divider{margin:15px 0}.deactivated{color:grey}.el-card__body{padding:17px}.el-card__header{background-color:#fafafa;padding:10px 20px}.el-collapse{border-bottom:none}.el-collapse-item__header{height:46px;font-size:14px}.el-collapse-item__content{padding-bottom:7px}.el-icon-arrow-right{margin-right:6px}.el-icon-close{padding:10px 5px 10px 10px;cursor:pointer}h4{margin:0;height:17px}.report .report-header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.id{color:grey;margin-top:6px}.line{width:100%;height:0;border:.5px solid #ebeef5;margin:15px 0}.new-note p{font-size:14px;font-weight:500;height:17px;margin:13px 0 7px}.note{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.1);box-shadow:0 2px 5px 0 rgba(0,0,0,.1);margin-bottom:10px}.no-notes{font-style:italic;color:grey}.report-row-key{font-weight:500;font-size:14px}.report-title{margin:0}.report-note-form{margin:15px 0 0}.report-post-note{margin:5px 0 0;text-align:right}.reports-pagination{margin:25px 0;text-align:center}.reports-timeline{margin:30px 45px 45px 19px;padding:0}.statuses{margin-top:15px}.submit-button{display:block;margin:7px 0 17px auto}.timestamp{margin:0;font-style:italic;color:grey}@media only screen and (max-width:480px){.report .report-header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;height:auto}.report .id{margin:6px 0 0}.report .report-actions-button,.report .report-tag{margin:3px 0 6px}.report .title-container{margin-bottom:7px}.reports-timeline{margin:20px 10px}.reports-timeline .el-timeline-item__wrapper{padding-left:20px}}.select-field[data-v-ecc36f5a]{width:350px}@media only screen and (max-width:480px){.select-field[data-v-ecc36f5a]{width:100%;margin-bottom:5px}}@media only screen and (max-width:801px) and (min-width:481px){.select-field[data-v-ecc36f5a]{width:50%}}.reports-container .reboot-button[data-v-fa601560]{padding:10px;margin:0;width:145px}.reports-container .reports-filter-container[data-v-fa601560]{margin:15px 45px 22px 15px;padding-bottom:0}.reports-container .reports-filter-container[data-v-fa601560],.reports-container .reports-header-container[data-v-fa601560]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.reports-container .reports-header-container[data-v-fa601560]{margin:10px 15px}.reports-container h1[data-v-fa601560]{margin:0}.reports-container .no-reports-message[data-v-fa601560]{color:grey;margin-left:19px}.reports-container .report-count[data-v-fa601560]{color:grey;font-size:28px}@media only screen and (max-width:480px){.reports-container h1[data-v-fa601560]{margin:7px 10px 15px}.reports-container .reboot-button[data-v-fa601560]{margin:0 0 5px 10px;width:145px}.reports-container .report-count[data-v-fa601560]{font-size:22px}.reports-container .reports-filter-container[data-v-fa601560]{margin:0 10px}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-cf57.26596375.css b/priv/static/adminfe/chunk-cf57.26596375.css
deleted file mode 100644
index 9f72b88c1..000000000
--- a/priv/static/adminfe/chunk-cf57.26596375.css
+++ /dev/null
@@ -1 +0,0 @@
-.actions-button[data-v-3850612b]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-3850612b]{float:right}.el-icon-edit[data-v-3850612b]{margin-right:5px}.tag-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-3850612b]{padding-right:20px}.no-hover[data-v-3850612b]:hover{color:#606266;background-color:#fff;cursor:auto}.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.statuses-container{padding:0 15px}.statuses-container h1{margin:10px 0 15px}.statuses-container .status-container{margin:0 0 10px}.checkbox-container{margin-bottom:15px}.filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 0 15px}.select-instance{width:350px}.statuses-pagination{padding:15px 0;text-align:center}@media only screen and (max-width:480px){.checkbox-container{margin-bottom:10px}.filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:10px 0}.select-field{width:100%;margin-bottom:5px}.select-instance{width:100%}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-d38a.cabdc22e.css b/priv/static/adminfe/chunk-d38a.cabdc22e.css
new file mode 100644
index 000000000..4a2bf472b
--- /dev/null
+++ b/priv/static/adminfe/chunk-d38a.cabdc22e.css
@@ -0,0 +1 @@
+.select-field[data-v-4bc96860]{width:350px}@media only screen and (max-width:480px){.select-field[data-v-4bc96860]{width:100%;margin-bottom:5px}}.el-dialog__body{padding:20px}.create-account-form-item{margin-bottom:20px}.create-account-form-item-without-margin{margin-bottom:0}@media only screen and (max-width:480px){.create-user-dialog{width:85%}.create-account-form-item{margin-bottom:20px}.el-dialog__body{padding:20px}}.moderate-user-button{text-align:left;width:350px;padding:10px}.moderate-user-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.moderate-user-button{width:100%}}.actions-button{text-align:left;width:350px;padding:10px}.actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0 15px 10px}.actions-container .el-dropdown{margin-left:10px}.active-tag{color:#409eff;font-weight:700}.active-tag .el-icon-check{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link:hover{cursor:pointer;color:#409eff}.create-account>.el-icon-plus{margin-right:5px}.users-header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.password-reset-token{margin:0 0 14px}.password-reset-token-dialog{width:50%}.reset-password-link{text-decoration:underline}.users-container h1{margin:10px 0 0 15px;height:40px}.users-container .pagination{margin:25px 0;text-align:center}.users-container .reboot-button{margin:0 15px 0 0;padding:10px;width:145px}.users-container .search{width:350px;float:right;margin-left:10px}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px}.users-container .user-count{color:grey;font-size:28px}@media only screen and (max-width:480px){.password-reset-token-dialog{width:85%}.users-container h1{margin:0}.users-container .actions-button{width:100%}.users-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .el-icon-arrow-down{font-size:12px}.users-container .search{width:100%;margin-left:0}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px}.users-container .el-tag{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger,.users-container .el-tag.el-tag--success{padding-left:8px}.users-container .reboot-button{margin:0}.users-container .users-header-container{margin:7px 10px 12px}.users-container .user-count{color:grey;font-size:22px}}@media only screen and (max-width:801px) and (min-width:481px){.actions-button,.search{width:49%}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-e458.f88bafea.css b/priv/static/adminfe/chunk-e458.f88bafea.css
new file mode 100644
index 000000000..085bdf076
--- /dev/null
+++ b/priv/static/adminfe/chunk-e458.f88bafea.css
@@ -0,0 +1 @@
+.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.statuses-container{padding:0 15px}.statuses-container h1{margin:10px 0 15px}.statuses-container .status-container{margin:0 0 10px}.checkbox-container{margin-bottom:15px}.filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 0 15px}.reboot-button{padding:10px;margin:0;width:145px}.select-instance{width:396px}.statuses-header,.statuses-header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.statuses-pagination{padding:15px 0;text-align:center}@media only screen and (max-width:480px){.checkbox-container{margin-bottom:10px}.filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:10px 0}.select-field{width:100%;margin-bottom:5px}.select-instance{width:100%}.statuses-header-container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.statuses-header-container .el-button{padding:10px 6.5px}.statuses-header-container .reboot-button{margin:10px 0 0}}
\ No newline at end of file
diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html
index 3651c1cf0..a236dd0f7 100644
--- a/priv/static/adminfe/index.html
+++ b/priv/static/adminfe/index.html
@@ -1 +1 @@
-
Admin FE
\ No newline at end of file
+
Admin FE
\ No newline at end of file
diff --git a/priv/static/adminfe/static/fonts/element-icons.535877f.woff b/priv/static/adminfe/static/fonts/element-icons.535877f.woff
deleted file mode 100644
index 02b9a2539..000000000
Binary files a/priv/static/adminfe/static/fonts/element-icons.535877f.woff and /dev/null differ
diff --git a/priv/static/adminfe/static/fonts/element-icons.732389d.ttf b/priv/static/adminfe/static/fonts/element-icons.732389d.ttf
deleted file mode 100644
index 91b74de36..000000000
Binary files a/priv/static/adminfe/static/fonts/element-icons.732389d.ttf and /dev/null differ
diff --git a/priv/static/adminfe/static/js/app.203f69f8.js b/priv/static/adminfe/static/js/app.203f69f8.js
new file mode 100644
index 000000000..d06fdf71d
Binary files /dev/null and b/priv/static/adminfe/static/js/app.203f69f8.js differ
diff --git a/priv/static/adminfe/static/js/app.203f69f8.js.map b/priv/static/adminfe/static/js/app.203f69f8.js.map
new file mode 100644
index 000000000..eb78cd464
Binary files /dev/null and b/priv/static/adminfe/static/js/app.203f69f8.js.map differ
diff --git a/priv/static/adminfe/static/js/app.d898cc2b.js b/priv/static/adminfe/static/js/app.d898cc2b.js
deleted file mode 100644
index 9d60db06b..000000000
Binary files a/priv/static/adminfe/static/js/app.d898cc2b.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/app.d898cc2b.js.map b/priv/static/adminfe/static/js/app.d898cc2b.js.map
deleted file mode 100644
index 1c4ec7590..000000000
Binary files a/priv/static/adminfe/static/js/app.d898cc2b.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-15fa.34070731.js b/priv/static/adminfe/static/js/chunk-0558.75954137.js
similarity index 98%
rename from priv/static/adminfe/static/js/chunk-15fa.34070731.js
rename to priv/static/adminfe/static/js/chunk-0558.75954137.js
index 937908d00..7b29707fa 100644
Binary files a/priv/static/adminfe/static/js/chunk-15fa.34070731.js and b/priv/static/adminfe/static/js/chunk-0558.75954137.js differ
diff --git a/priv/static/adminfe/static/js/chunk-15fa.34070731.js.map b/priv/static/adminfe/static/js/chunk-0558.75954137.js.map
similarity index 99%
rename from priv/static/adminfe/static/js/chunk-15fa.34070731.js.map
rename to priv/static/adminfe/static/js/chunk-0558.75954137.js.map
index d3830be7c..e9e2affb6 100644
Binary files a/priv/static/adminfe/static/js/chunk-15fa.34070731.js.map and b/priv/static/adminfe/static/js/chunk-0558.75954137.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-0778.b17650df.js b/priv/static/adminfe/static/js/chunk-0778.b17650df.js
new file mode 100644
index 000000000..1a174cc1e
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-0778.b17650df.js differ
diff --git a/priv/static/adminfe/static/js/chunk-0778.b17650df.js.map b/priv/static/adminfe/static/js/chunk-0778.b17650df.js.map
new file mode 100644
index 000000000..1f96c3236
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-0778.b17650df.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-876c.e4ceccca.js b/priv/static/adminfe/static/js/chunk-0961.ef33e81b.js
similarity index 97%
rename from priv/static/adminfe/static/js/chunk-876c.e4ceccca.js
rename to priv/static/adminfe/static/js/chunk-0961.ef33e81b.js
index 841ceb9dc..e090bb93c 100644
Binary files a/priv/static/adminfe/static/js/chunk-876c.e4ceccca.js and b/priv/static/adminfe/static/js/chunk-0961.ef33e81b.js differ
diff --git a/priv/static/adminfe/static/js/chunk-876c.e4ceccca.js.map b/priv/static/adminfe/static/js/chunk-0961.ef33e81b.js.map
similarity index 99%
rename from priv/static/adminfe/static/js/chunk-876c.e4ceccca.js.map
rename to priv/static/adminfe/static/js/chunk-0961.ef33e81b.js.map
index 88976a4fe..97c6a4b54 100644
Binary files a/priv/static/adminfe/static/js/chunk-876c.e4ceccca.js.map and b/priv/static/adminfe/static/js/chunk-0961.ef33e81b.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-0d8f.6d50ff86.js b/priv/static/adminfe/static/js/chunk-0d8f.6d50ff86.js
deleted file mode 100644
index 4b0945f57..000000000
Binary files a/priv/static/adminfe/static/js/chunk-0d8f.6d50ff86.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-0d8f.6d50ff86.js.map b/priv/static/adminfe/static/js/chunk-0d8f.6d50ff86.js.map
deleted file mode 100644
index da24cbef5..000000000
Binary files a/priv/static/adminfe/static/js/chunk-0d8f.6d50ff86.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-136a.c4719e3e.js b/priv/static/adminfe/static/js/chunk-136a.c4719e3e.js
deleted file mode 100644
index 0c2f1a52e..000000000
Binary files a/priv/static/adminfe/static/js/chunk-136a.c4719e3e.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-136a.c4719e3e.js.map b/priv/static/adminfe/static/js/chunk-136a.c4719e3e.js.map
deleted file mode 100644
index 4b137fd49..000000000
Binary files a/priv/static/adminfe/static/js/chunk-136a.c4719e3e.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-13e9.79da1569.js b/priv/static/adminfe/static/js/chunk-13e9.79da1569.js
deleted file mode 100644
index b98177b82..000000000
Binary files a/priv/static/adminfe/static/js/chunk-13e9.79da1569.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-13e9.79da1569.js.map b/priv/static/adminfe/static/js/chunk-13e9.79da1569.js.map
deleted file mode 100644
index 118a47034..000000000
Binary files a/priv/static/adminfe/static/js/chunk-13e9.79da1569.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js b/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js
new file mode 100644
index 000000000..903f553b0
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js differ
diff --git a/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js.map b/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js.map
new file mode 100644
index 000000000..68735ed26
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js b/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js
deleted file mode 100644
index f06da0268..000000000
Binary files a/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js.map b/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js.map
deleted file mode 100644
index 1ec750dd1..000000000
Binary files a/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-3384.458ffaf1.js b/priv/static/adminfe/static/js/chunk-3384.458ffaf1.js
new file mode 100644
index 000000000..eb2b55d37
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-3384.458ffaf1.js differ
diff --git a/priv/static/adminfe/static/js/chunk-3384.458ffaf1.js.map b/priv/static/adminfe/static/js/chunk-3384.458ffaf1.js.map
new file mode 100644
index 000000000..0bb577aab
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-3384.458ffaf1.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-4011.67fb1692.js b/priv/static/adminfe/static/js/chunk-4011.67fb1692.js
new file mode 100644
index 000000000..775ed26f1
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-4011.67fb1692.js differ
diff --git a/priv/static/adminfe/static/js/chunk-4011.67fb1692.js.map b/priv/static/adminfe/static/js/chunk-4011.67fb1692.js.map
new file mode 100644
index 000000000..6df398cbc
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-4011.67fb1692.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-46ef.671cac7d.js b/priv/static/adminfe/static/js/chunk-46ef.671cac7d.js
deleted file mode 100644
index 805cdea13..000000000
Binary files a/priv/static/adminfe/static/js/chunk-46ef.671cac7d.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-46ef.671cac7d.js.map b/priv/static/adminfe/static/js/chunk-46ef.671cac7d.js.map
deleted file mode 100644
index f6b420bb2..000000000
Binary files a/priv/static/adminfe/static/js/chunk-46ef.671cac7d.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js b/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js
new file mode 100644
index 000000000..bfdf936f8
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js differ
diff --git a/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js.map b/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js.map
new file mode 100644
index 000000000..d1d728b80
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-4ffb.0e8f3772.js b/priv/static/adminfe/static/js/chunk-6e81.3733ace2.js
similarity index 85%
rename from priv/static/adminfe/static/js/chunk-4ffb.0e8f3772.js
rename to priv/static/adminfe/static/js/chunk-6e81.3733ace2.js
index 5a7aa9f59..c888ce03f 100644
Binary files a/priv/static/adminfe/static/js/chunk-4ffb.0e8f3772.js and b/priv/static/adminfe/static/js/chunk-6e81.3733ace2.js differ
diff --git a/priv/static/adminfe/static/js/chunk-4ffb.0e8f3772.js.map b/priv/static/adminfe/static/js/chunk-6e81.3733ace2.js.map
similarity index 98%
rename from priv/static/adminfe/static/js/chunk-4ffb.0e8f3772.js.map
rename to priv/static/adminfe/static/js/chunk-6e81.3733ace2.js.map
index 7c020768c..63128dd67 100644
Binary files a/priv/static/adminfe/static/js/chunk-4ffb.0e8f3772.js.map and b/priv/static/adminfe/static/js/chunk-6e81.3733ace2.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js b/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js
new file mode 100644
index 000000000..b38644b98
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js differ
diff --git a/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js.map b/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js.map
new file mode 100644
index 000000000..ddd53f1cd
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js b/priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js
deleted file mode 100644
index 3899ff190..000000000
Binary files a/priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js.map b/priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js.map
deleted file mode 100644
index 6c6a85667..000000000
Binary files a/priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-88c9.e3583744.js b/priv/static/adminfe/static/js/chunk-88c9.e3583744.js
deleted file mode 100644
index 0070fc30a..000000000
Binary files a/priv/static/adminfe/static/js/chunk-88c9.e3583744.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-88c9.e3583744.js.map b/priv/static/adminfe/static/js/chunk-88c9.e3583744.js.map
deleted file mode 100644
index 20e503d0c..000000000
Binary files a/priv/static/adminfe/static/js/chunk-88c9.e3583744.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-970d.2457e066.js b/priv/static/adminfe/static/js/chunk-970d.2457e066.js
new file mode 100644
index 000000000..0f99d835e
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-970d.2457e066.js differ
diff --git a/priv/static/adminfe/static/js/chunk-970d.2457e066.js.map b/priv/static/adminfe/static/js/chunk-970d.2457e066.js.map
new file mode 100644
index 000000000..6896407b0
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-970d.2457e066.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js b/priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js
deleted file mode 100644
index 2b4fd918f..000000000
Binary files a/priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js.map b/priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js.map
deleted file mode 100644
index 6457630bd..000000000
Binary files a/priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js.map and /dev/null differ
diff --git a/priv/static/adminfe/static/js/chunk-d38a.a851004a.js b/priv/static/adminfe/static/js/chunk-d38a.a851004a.js
new file mode 100644
index 000000000..c302af310
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-d38a.a851004a.js differ
diff --git a/priv/static/adminfe/static/js/chunk-d38a.a851004a.js.map b/priv/static/adminfe/static/js/chunk-d38a.a851004a.js.map
new file mode 100644
index 000000000..6779f6dc1
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-d38a.a851004a.js.map differ
diff --git a/priv/static/adminfe/static/js/chunk-e458.4e5aad44.js b/priv/static/adminfe/static/js/chunk-e458.4e5aad44.js
new file mode 100644
index 000000000..a02c83110
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-e458.4e5aad44.js differ
diff --git a/priv/static/adminfe/static/js/chunk-e458.4e5aad44.js.map b/priv/static/adminfe/static/js/chunk-e458.4e5aad44.js.map
new file mode 100644
index 000000000..e623af23d
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-e458.4e5aad44.js.map differ
diff --git a/priv/static/adminfe/static/js/runtime.1b4f6ce0.js b/priv/static/adminfe/static/js/runtime.1b4f6ce0.js
new file mode 100644
index 000000000..6558531ba
Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.1b4f6ce0.js differ
diff --git a/priv/static/adminfe/static/js/runtime.1b4f6ce0.js.map b/priv/static/adminfe/static/js/runtime.1b4f6ce0.js.map
new file mode 100644
index 000000000..9295ac636
Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.1b4f6ce0.js.map differ
diff --git a/priv/static/adminfe/static/js/runtime.cb26bbd1.js b/priv/static/adminfe/static/js/runtime.cb26bbd1.js
deleted file mode 100644
index 7180cc6e3..000000000
Binary files a/priv/static/adminfe/static/js/runtime.cb26bbd1.js and /dev/null differ
diff --git a/priv/static/adminfe/static/js/runtime.cb26bbd1.js.map b/priv/static/adminfe/static/js/runtime.cb26bbd1.js.map
deleted file mode 100644
index 631198682..000000000
Binary files a/priv/static/adminfe/static/js/runtime.cb26bbd1.js.map and /dev/null differ
diff --git a/priv/static/index.html b/priv/static/index.html
index a6cf28409..6e3e9905d 100644
--- a/priv/static/index.html
+++ b/priv/static/index.html
@@ -1 +1,5 @@
-
Pleroma
\ No newline at end of file
+<<<<<<< HEAD
+
Pleroma
+=======
+
Pleroma
+>>>>>>> fbcc53760e6fcd393513c05a5bd7a4a6a6f3b731
diff --git a/priv/static/static-fe/static-fe.css b/priv/static/static-fe/static-fe.css
new file mode 100644
index 000000000..db61ff266
--- /dev/null
+++ b/priv/static/static-fe/static-fe.css
@@ -0,0 +1,183 @@
+body {
+ background-color: #282c37;
+ font-family: sans-serif;
+ color: white;
+}
+
+main {
+ margin: 50px auto;
+ max-width: 960px;
+ padding: 40px;
+ background-color: #313543;
+ border-radius: 4px;
+}
+
+header {
+ margin: 50px auto;
+ max-width: 960px;
+ padding: 40px;
+ background-color: #313543;
+ border-radius: 4px;
+}
+
+.activity {
+ border-radius: 4px;
+ padding: 1em;
+ padding-bottom: 2em;
+ margin-bottom: 1em;
+}
+
+.avatar {
+ cursor: pointer;
+}
+
+.avatar img {
+ float: left;
+ border-radius: 4px;
+ margin-right: 4px;
+}
+
+.activity-content img, video, audio {
+ padding: 1em;
+ max-width: 800px;
+ max-height: 800px;
+}
+
+#selected {
+ background-color: #1b2735;
+}
+
+.counts dt, .counts dd {
+ float: left;
+ margin-left: 1em;
+}
+
+a {
+ color: white;
+}
+
+.h-card {
+ min-height: 48px;
+ margin-bottom: 8px;
+}
+
+header a, .h-card a {
+ text-decoration: none;
+}
+
+header a:hover, .h-card a:hover {
+ text-decoration: underline;
+}
+
+.display-name {
+ padding-top: 4px;
+ display: block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ color: white;
+}
+
+/* keep emoji from being hilariously huge */
+.display-name img {
+ max-height: 1em;
+}
+
+.display-name .nickname {
+ padding-top: 4px;
+ display: block;
+}
+
+.nickname:hover {
+ text-decoration: none;
+}
+
+.pull-right {
+ float: right;
+}
+
+.collapse {
+ margin: 0;
+ width: auto;
+}
+
+h1 {
+ margin: 0;
+}
+
+h2 {
+ color: #9baec8;
+ font-weight: normal;
+ font-size: 20px;
+ margin-bottom: 40px;
+}
+
+form {
+ width: 100%;
+}
+
+input {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 10px;
+ margin-top: 20px;
+ background-color: rgba(0,0,0,.1);
+ color: white;
+ border: 0;
+ border-bottom: 2px solid #9baec8;
+ font-size: 14px;
+}
+
+input:focus {
+ border-bottom: 2px solid #4b8ed8;
+}
+
+input[type="checkbox"] {
+ width: auto;
+}
+
+button {
+ box-sizing: border-box;
+ width: 100%;
+ color: white;
+ background-color: #419bdd;
+ border-radius: 4px;
+ border: none;
+ padding: 10px;
+ margin-top: 30px;
+ text-transform: uppercase;
+ font-weight: 500;
+ font-size: 16px;
+}
+
+.alert-danger {
+ box-sizing: border-box;
+ width: 100%;
+ color: #D8000C;
+ background-color: #FFD2D2;
+ border-radius: 4px;
+ border: none;
+ padding: 10px;
+ margin-top: 20px;
+ font-weight: 500;
+ font-size: 16px;
+}
+
+.alert-info {
+ box-sizing: border-box;
+ width: 100%;
+ color: #00529B;
+ background-color: #BDE5F8;
+ border-radius: 4px;
+ border: none;
+ padding: 10px;
+ margin-top: 20px;
+ font-weight: 500;
+ font-size: 16px;
+}
+
+img.emoji {
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ vertical-align: middle;
+}
diff --git a/priv/static/static/css/app.1055039ce3f2fe4dd110.css b/priv/static/static/css/app.1055039ce3f2fe4dd110.css
deleted file mode 100644
index 1867ca81a..000000000
--- a/priv/static/static/css/app.1055039ce3f2fe4dd110.css
+++ /dev/null
@@ -1,108 +0,0 @@
-.with-load-more-footer {
- padding: 10px;
- text-align: center;
- border-top: 1px solid;
- border-top-color: #222;
- border-top-color: var(--border, #222);
-}
-.with-load-more-footer .error {
- font-size: 14px;
-}
-.tab-switcher {
- display: -ms-flexbox;
- display: flex;
- -ms-flex-direction: column;
- flex-direction: column;
-}
-.tab-switcher .contents {
- -ms-flex: 1 0 auto;
- flex: 1 0 auto;
- min-height: 0px;
-}
-.tab-switcher .contents .hidden {
- display: none;
-}
-.tab-switcher .contents.scrollable-tabs {
- -ms-flex-preferred-size: 0;
- flex-basis: 0;
- overflow-y: auto;
-}
-.tab-switcher .tabs {
- display: -ms-flexbox;
- display: flex;
- position: relative;
- width: 100%;
- overflow-y: hidden;
- overflow-x: auto;
- padding-top: 5px;
- box-sizing: border-box;
-}
-.tab-switcher .tabs::after, .tab-switcher .tabs::before {
- display: block;
- content: "";
- -ms-flex: 1 1 auto;
- flex: 1 1 auto;
- border-bottom: 1px solid;
- border-bottom-color: #222;
- border-bottom-color: var(--border, #222);
-}
-.tab-switcher .tabs .tab-wrapper {
- height: 28px;
- position: relative;
- display: -ms-flexbox;
- display: flex;
- -ms-flex: 0 0 auto;
- flex: 0 0 auto;
-}
-.tab-switcher .tabs .tab-wrapper .tab {
- width: 100%;
- min-width: 1px;
- position: relative;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- padding: 6px 1em;
- padding-bottom: 99px;
- margin-bottom: -93px;
- white-space: nowrap;
- color: #b9b9ba;
- color: var(--tabText, #b9b9ba);
- background-color: #182230;
- background-color: var(--tab, #182230);
-}
-.tab-switcher .tabs .tab-wrapper .tab:not(.active) {
- z-index: 4;
-}
-.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {
- z-index: 6;
-}
-.tab-switcher .tabs .tab-wrapper .tab.active {
- background: transparent;
- z-index: 5;
- color: #b9b9ba;
- color: var(--tabActiveText, #b9b9ba);
-}
-.tab-switcher .tabs .tab-wrapper .tab img {
- max-height: 26px;
- vertical-align: top;
- margin-top: -5px;
-}
-.tab-switcher .tabs .tab-wrapper:not(.active)::after {
- content: "";
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 7;
- border-bottom: 1px solid;
- border-bottom-color: #222;
- border-bottom-color: var(--border, #222);
-}
-.with-subscription-loading {
- padding: 10px;
- text-align: center;
-}
-.with-subscription-loading .error {
- font-size: 14px;
-}
-
-/*# sourceMappingURL=app.1055039ce3f2fe4dd110.css.map*/
\ No newline at end of file
diff --git a/priv/static/static/css/app.1055039ce3f2fe4dd110.css.map b/priv/static/static/css/app.1055039ce3f2fe4dd110.css.map
deleted file mode 100644
index 861ee8313..000000000
--- a/priv/static/static/css/app.1055039ce3f2fe4dd110.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACxFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.1055039ce3f2fe4dd110.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/app.613cef07981cd95ccceb.css b/priv/static/static/css/app.613cef07981cd95ccceb.css
new file mode 100644
index 000000000..c1d5f8188
--- /dev/null
+++ b/priv/static/static/css/app.613cef07981cd95ccceb.css
@@ -0,0 +1,5 @@
+.with-load-more-footer{padding:10px;text-align:center;border-top:1px solid;border-top-color:#222;border-top-color:var(--border, #222)}.with-load-more-footer .error{font-size:14px}
+.tab-switcher{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.tab-switcher .contents{-ms-flex:1 0 auto;flex:1 0 auto;min-height:0px}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents.scrollable-tabs{-ms-flex-preferred-size:0;flex-basis:0;overflow-y:auto}.tab-switcher .tabs{display:-ms-flexbox;display:flex;position:relative;width:100%;overflow-y:hidden;overflow-x:auto;padding-top:5px;box-sizing:border-box}.tab-switcher .tabs::after,.tab-switcher .tabs::before{display:block;content:"";-ms-flex:1 1 auto;flex:1 1 auto;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}.tab-switcher .tabs .tab-wrapper{height:28px;position:relative;display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto}.tab-switcher .tabs .tab-wrapper .tab{width:100%;min-width:1px;position:relative;border-bottom-left-radius:0;border-bottom-right-radius:0;padding:6px 1em;padding-bottom:99px;margin-bottom:-93px;white-space:nowrap;color:#b9b9ba;color:var(--tabText, #b9b9ba);background-color:#182230;background-color:var(--tab, #182230)}.tab-switcher .tabs .tab-wrapper .tab:not(.active){z-index:4}.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover{z-index:6}.tab-switcher .tabs .tab-wrapper .tab.active{background:transparent;z-index:5;color:#b9b9ba;color:var(--tabActiveText, #b9b9ba)}.tab-switcher .tabs .tab-wrapper .tab img{max-height:26px;vertical-align:top;margin-top:-5px}.tab-switcher .tabs .tab-wrapper:not(.active)::after{content:"";position:absolute;left:0;right:0;bottom:0;z-index:7;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}
+.with-subscription-loading{padding:10px;text-align:center}.with-subscription-loading .error{font-size:14px}
+
+/*# sourceMappingURL=app.613cef07981cd95ccceb.css.map*/
\ No newline at end of file
diff --git a/priv/static/static/css/app.613cef07981cd95ccceb.css.map b/priv/static/static/css/app.613cef07981cd95ccceb.css.map
new file mode 100644
index 000000000..556e0bb0b
--- /dev/null
+++ b/priv/static/static/css/app.613cef07981cd95ccceb.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA,uBAAuB,aAAa,kBAAkB,qBAAqB,sBAAsB,qCAAqC,8BAA8B,e;ACApK,cAAc,oBAAoB,aAAa,0BAA0B,sBAAsB,wBAAwB,kBAAkB,cAAc,eAAe,gCAAgC,aAAa,wCAAwC,0BAA0B,aAAa,gBAAgB,oBAAoB,oBAAoB,aAAa,kBAAkB,WAAW,kBAAkB,gBAAgB,gBAAgB,sBAAsB,uDAAuD,cAAc,WAAW,kBAAkB,cAAc,wBAAwB,yBAAyB,wCAAwC,iCAAiC,YAAY,kBAAkB,oBAAoB,aAAa,kBAAkB,cAAc,sCAAsC,WAAW,cAAc,kBAAkB,4BAA4B,6BAA6B,gBAAgB,oBAAoB,oBAAoB,mBAAmB,cAAc,8BAA8B,yBAAyB,qCAAqC,mDAAmD,UAAU,yDAAyD,UAAU,6CAA6C,uBAAuB,UAAU,cAAc,oCAAoC,0CAA0C,gBAAgB,mBAAmB,gBAAgB,qDAAqD,WAAW,kBAAkB,OAAO,QAAQ,SAAS,UAAU,wBAAwB,yBAAyB,wC;ACAtlD,2BAA2B,aAAa,kBAAkB,kCAAkC,e","file":"static/css/app.613cef07981cd95ccceb.css","sourcesContent":[".with-load-more-footer{padding:10px;text-align:center;border-top:1px solid;border-top-color:#222;border-top-color:var(--border, #222)}.with-load-more-footer .error{font-size:14px}",".tab-switcher{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.tab-switcher .contents{-ms-flex:1 0 auto;flex:1 0 auto;min-height:0px}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents.scrollable-tabs{-ms-flex-preferred-size:0;flex-basis:0;overflow-y:auto}.tab-switcher .tabs{display:-ms-flexbox;display:flex;position:relative;width:100%;overflow-y:hidden;overflow-x:auto;padding-top:5px;box-sizing:border-box}.tab-switcher .tabs::after,.tab-switcher .tabs::before{display:block;content:\"\";-ms-flex:1 1 auto;flex:1 1 auto;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}.tab-switcher .tabs .tab-wrapper{height:28px;position:relative;display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto}.tab-switcher .tabs .tab-wrapper .tab{width:100%;min-width:1px;position:relative;border-bottom-left-radius:0;border-bottom-right-radius:0;padding:6px 1em;padding-bottom:99px;margin-bottom:-93px;white-space:nowrap;color:#b9b9ba;color:var(--tabText, #b9b9ba);background-color:#182230;background-color:var(--tab, #182230)}.tab-switcher .tabs .tab-wrapper .tab:not(.active){z-index:4}.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover{z-index:6}.tab-switcher .tabs .tab-wrapper .tab.active{background:transparent;z-index:5;color:#b9b9ba;color:var(--tabActiveText, #b9b9ba)}.tab-switcher .tabs .tab-wrapper .tab img{max-height:26px;vertical-align:top;margin-top:-5px}.tab-switcher .tabs .tab-wrapper:not(.active)::after{content:\"\";position:absolute;left:0;right:0;bottom:0;z-index:7;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}",".with-subscription-loading{padding:10px;text-align:center}.with-subscription-loading .error{font-size:14px}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/vendors~app.b2603a50868c68a1c192.css b/priv/static/static/css/vendors~app.18fea621d430000acc27.css
similarity index 92%
rename from priv/static/static/css/vendors~app.b2603a50868c68a1c192.css
rename to priv/static/static/css/vendors~app.18fea621d430000acc27.css
index a2e625f5e..ef783cbb3 100644
--- a/priv/static/static/css/vendors~app.b2603a50868c68a1c192.css
+++ b/priv/static/static/css/vendors~app.18fea621d430000acc27.css
@@ -1,11 +1,11 @@
/*!
- * Cropper.js v1.4.3
+ * Cropper.js v1.5.6
* https://fengyuanchen.github.io/cropperjs
*
* Copyright 2015-present Chen Fengyuan
* Released under the MIT license
*
- * Date: 2018-10-24T13:07:11.429Z
+ * Date: 2019-10-04T04:33:44.164Z
*/
.cropper-container {
@@ -16,7 +16,6 @@ .cropper-container {
-ms-touch-action: none;
touch-action: none;
-webkit-user-select: none;
- -moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@@ -56,14 +55,14 @@ .cropper-drag-box {
.cropper-modal {
background-color: #000;
- opacity: .5;
+ opacity: 0.5;
}
.cropper-view-box {
display: block;
height: 100%;
- outline-color: rgba(51, 153, 255, 0.75);
outline: 1px solid #39f;
+ outline-color: rgba(51, 153, 255, 0.75);
overflow: hidden;
width: 100%;
}
@@ -71,7 +70,7 @@ .cropper-view-box {
.cropper-dashed {
border: 0 dashed #eee;
display: block;
- opacity: .5;
+ opacity: 0.5;
position: absolute;
}
@@ -97,28 +96,28 @@ .cropper-center {
display: block;
height: 0;
left: 50%;
- opacity: .75;
+ opacity: 0.75;
position: absolute;
top: 50%;
width: 0;
}
-.cropper-center:before,
-.cropper-center:after {
+.cropper-center::before,
+.cropper-center::after {
background-color: #eee;
content: ' ';
display: block;
position: absolute;
}
-.cropper-center:before {
+.cropper-center::before {
height: 1px;
left: -3px;
top: 0;
width: 7px;
}
-.cropper-center:after {
+.cropper-center::after {
height: 7px;
left: 0;
top: -3px;
@@ -130,7 +129,7 @@ .cropper-line,
.cropper-point {
display: block;
height: 100%;
- opacity: .1;
+ opacity: 0.1;
position: absolute;
width: 100%;
}
@@ -176,7 +175,7 @@ .cropper-line.line-s {
.cropper-point {
background-color: #39f;
height: 5px;
- opacity: .75;
+ opacity: 0.75;
width: 5px;
}
@@ -252,12 +251,12 @@ @media (min-width: 992px) {
@media (min-width: 1200px) {
.cropper-point.point-se {
height: 5px;
- opacity: .75;
+ opacity: 0.75;
width: 5px;
}
}
-.cropper-point.point-se:before {
+.cropper-point.point-se::before {
background-color: #39f;
bottom: -50%;
content: ' ';
@@ -304,4 +303,4 @@ .cropper-disabled .cropper-point {
}
-/*# sourceMappingURL=vendors~app.b2603a50868c68a1c192.css.map*/
\ No newline at end of file
+/*# sourceMappingURL=vendors~app.18fea621d430000acc27.css.map*/
\ No newline at end of file
diff --git a/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map b/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map
new file mode 100644
index 000000000..057d67d6a
--- /dev/null
+++ b/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///./node_modules/cropperjs/dist/cropper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,wCAAwC;AACxC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"static/css/vendors~app.18fea621d430000acc27.css","sourcesContent":["/*!\n * Cropper.js v1.5.6\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2019-10-04T04:33:44.164Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 0.75);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n.cropper-center::after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/vendors~app.b2603a50868c68a1c192.css.map b/priv/static/static/css/vendors~app.b2603a50868c68a1c192.css.map
deleted file mode 100644
index e7013b291..000000000
--- a/priv/static/static/css/vendors~app.b2603a50868c68a1c192.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["webpack:///./node_modules/cropperjs/dist/cropper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,wCAAwC;AACxC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"static/css/vendors~app.b2603a50868c68a1c192.css","sourcesContent":["/*!\n * Cropper.js v1.4.3\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2018-10-24T13:07:11.429Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: .5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline-color: rgba(51, 153, 255, 0.75);\n outline: 1px solid #39f;\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: .5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: .75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center:before,\n.cropper-center:after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center:before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center:after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: .1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: .75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: .75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se:before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/font/fontello.1588419330867.eot b/priv/static/static/font/fontello.1588419330867.eot
new file mode 100644
index 000000000..7f8c61e38
Binary files /dev/null and b/priv/static/static/font/fontello.1588419330867.eot differ
diff --git a/priv/static/static/font/fontello.1588419330867.svg b/priv/static/static/font/fontello.1588419330867.svg
new file mode 100644
index 000000000..71f81f435
--- /dev/null
+++ b/priv/static/static/font/fontello.1588419330867.svg
@@ -0,0 +1,122 @@
+
+
+
\ No newline at end of file
diff --git a/priv/static/static/font/fontello.1588419330867.ttf b/priv/static/static/font/fontello.1588419330867.ttf
new file mode 100644
index 000000000..7dc4f108b
Binary files /dev/null and b/priv/static/static/font/fontello.1588419330867.ttf differ
diff --git a/priv/static/static/font/fontello.1588419330867.woff b/priv/static/static/font/fontello.1588419330867.woff
new file mode 100644
index 000000000..2bf4cbc16
Binary files /dev/null and b/priv/static/static/font/fontello.1588419330867.woff differ
diff --git a/priv/static/static/font/fontello.1588419330867.woff2 b/priv/static/static/font/fontello.1588419330867.woff2
new file mode 100644
index 000000000..a31bf3f29
Binary files /dev/null and b/priv/static/static/font/fontello.1588419330867.woff2 differ
diff --git a/priv/static/static/fontello.1588419330867.css b/priv/static/static/fontello.1588419330867.css
new file mode 100644
index 000000000..198eff184
--- /dev/null
+++ b/priv/static/static/fontello.1588419330867.css
@@ -0,0 +1,142 @@
+@font-face {
+ font-family: "Icons";
+ src: url("./font/fontello.1588419330867.eot");
+ src: url("./font/fontello.1588419330867.eot") format("embedded-opentype"),
+ url("./font/fontello.1588419330867.woff2") format("woff2"),
+ url("./font/fontello.1588419330867.woff") format("woff"),
+ url("./font/fontello.1588419330867.ttf") format("truetype"),
+ url("./font/fontello.1588419330867.svg") format("svg");
+ font-weight: normal;
+ font-style: normal;
+}
+
+[class^="icon-"]::before,
+[class*=" icon-"]::before {
+ font-family: "Icons";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1em;
+ margin-left: .2em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-spin4::before { content: "\e834"; }
+
+.icon-cancel::before { content: "\e800"; }
+
+.icon-upload::before { content: "\e801"; }
+
+.icon-spin3::before { content: "\e832"; }
+
+.icon-reply::before { content: "\f112"; }
+
+.icon-star::before { content: "\e802"; }
+
+.icon-star-empty::before { content: "\e803"; }
+
+.icon-retweet::before { content: "\e804"; }
+
+.icon-eye-off::before { content: "\e805"; }
+
+.icon-binoculars::before { content: "\f1e5"; }
+
+.icon-cog::before { content: "\e807"; }
+
+.icon-user-plus::before { content: "\f234"; }
+
+.icon-menu::before { content: "\f0c9"; }
+
+.icon-logout::before { content: "\e808"; }
+
+.icon-down-open::before { content: "\e809"; }
+
+.icon-attach::before { content: "\e80a"; }
+
+.icon-link-ext::before { content: "\f08e"; }
+
+.icon-link-ext-alt::before { content: "\f08f"; }
+
+.icon-picture::before { content: "\e80b"; }
+
+.icon-video::before { content: "\e80c"; }
+
+.icon-right-open::before { content: "\e80d"; }
+
+.icon-left-open::before { content: "\e80e"; }
+
+.icon-up-open::before { content: "\e80f"; }
+
+.icon-comment-empty::before { content: "\f0e5"; }
+
+.icon-mail-alt::before { content: "\f0e0"; }
+
+.icon-lock::before { content: "\e811"; }
+
+.icon-lock-open-alt::before { content: "\f13e"; }
+
+.icon-globe::before { content: "\e812"; }
+
+.icon-brush::before { content: "\e813"; }
+
+.icon-search::before { content: "\e806"; }
+
+.icon-adjust::before { content: "\e816"; }
+
+.icon-thumbs-up-alt::before { content: "\f164"; }
+
+.icon-attention::before { content: "\e814"; }
+
+.icon-plus-squared::before { content: "\f0fe"; }
+
+.icon-plus::before { content: "\e815"; }
+
+.icon-edit::before { content: "\e817"; }
+
+.icon-play-circled::before { content: "\f144"; }
+
+.icon-pencil::before { content: "\e818"; }
+
+.icon-chart-bar::before { content: "\e81b"; }
+
+.icon-smile::before { content: "\f118"; }
+
+.icon-bell-alt::before { content: "\f0f3"; }
+
+.icon-wrench::before { content: "\e81a"; }
+
+.icon-pin::before { content: "\e819"; }
+
+.icon-ellipsis::before { content: "\f141"; }
+
+.icon-bell-ringing-o::before { content: "\e810"; }
+
+.icon-zoom-in::before { content: "\e81c"; }
+
+.icon-gauge::before { content: "\f0e4"; }
+
+.icon-users::before { content: "\e81d"; }
+
+.icon-info-circled::before { content: "\e81f"; }
+
+.icon-home-2::before { content: "\e821"; }
+
+.icon-chat::before { content: "\e81e"; }
+
+.icon-login::before { content: "\e820"; }
+
+.icon-arrow-curved::before { content: "\e822"; }
+
+.icon-link::before { content: "\e823"; }
+
+.icon-user::before { content: "\e824"; }
+
+.icon-ok::before { content: "\e827"; }
diff --git a/priv/static/static/fontello.json b/priv/static/static/fontello.json
index 5a7086a23..5963b68b4 100755
--- a/priv/static/static/fontello.json
+++ b/priv/static/static/fontello.json
@@ -345,6 +345,18 @@
"css": "link",
"code": 59427,
"src": "fontawesome"
+ },
+ {
+ "uid": "8b80d36d4ef43889db10bc1f0dc9a862",
+ "css": "user",
+ "code": 59428,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "12f4ece88e46abd864e40b35e05b11cd",
+ "css": "ok",
+ "code": 59431,
+ "src": "fontawesome"
}
]
-}
+}
\ No newline at end of file
diff --git a/priv/static/static/js/2.1c407059cd79fca99e19.js b/priv/static/static/js/2.1c407059cd79fca99e19.js
new file mode 100644
index 000000000..14018d92a
Binary files /dev/null and b/priv/static/static/js/2.1c407059cd79fca99e19.js differ
diff --git a/priv/static/static/js/2.1c407059cd79fca99e19.js.map b/priv/static/static/js/2.1c407059cd79fca99e19.js.map
new file mode 100644
index 000000000..cfee79ea8
Binary files /dev/null and b/priv/static/static/js/2.1c407059cd79fca99e19.js.map differ
diff --git a/priv/static/static/js/2.f158cbd2b8770e467dfe.js b/priv/static/static/js/2.f158cbd2b8770e467dfe.js
deleted file mode 100644
index 24f80fe7b..000000000
Binary files a/priv/static/static/js/2.f158cbd2b8770e467dfe.js and /dev/null differ
diff --git a/priv/static/static/js/2.f158cbd2b8770e467dfe.js.map b/priv/static/static/js/2.f158cbd2b8770e467dfe.js.map
deleted file mode 100644
index 94ca6f090..000000000
Binary files a/priv/static/static/js/2.f158cbd2b8770e467dfe.js.map and /dev/null differ
diff --git a/priv/static/static/js/app.fa89b90e606f4facd209.js b/priv/static/static/js/app.fa89b90e606f4facd209.js
new file mode 100644
index 000000000..a2cbcc337
Binary files /dev/null and b/priv/static/static/js/app.fa89b90e606f4facd209.js differ
diff --git a/priv/static/static/js/app.fa89b90e606f4facd209.js.map b/priv/static/static/js/app.fa89b90e606f4facd209.js.map
new file mode 100644
index 000000000..5722844a9
Binary files /dev/null and b/priv/static/static/js/app.fa89b90e606f4facd209.js.map differ
diff --git a/priv/static/static/js/vendors~app.8aa781e6dd81307f544b.js b/priv/static/static/js/vendors~app.8aa781e6dd81307f544b.js
new file mode 100644
index 000000000..1d62bb0a4
Binary files /dev/null and b/priv/static/static/js/vendors~app.8aa781e6dd81307f544b.js differ
diff --git a/priv/static/static/js/vendors~app.8aa781e6dd81307f544b.js.map b/priv/static/static/js/vendors~app.8aa781e6dd81307f544b.js.map
new file mode 100644
index 000000000..ce0c86939
Binary files /dev/null and b/priv/static/static/js/vendors~app.8aa781e6dd81307f544b.js.map differ
diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js
index 0e037d43d..ac065e10c 100644
Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ
diff --git a/priv/static/sw-pleroma.js.map b/priv/static/sw-pleroma.js.map
index 5d9874693..c704cb951 100644
Binary files a/priv/static/sw-pleroma.js.map and b/priv/static/sw-pleroma.js.map differ
diff --git a/test/captcha_test.exs b/test/captcha_test.exs
index ac1d846e8..1ab9019ab 100644
--- a/test/captcha_test.exs
+++ b/test/captcha_test.exs
@@ -61,7 +61,7 @@ test "new and validate" do
assert is_binary(answer)
assert :ok = Native.validate(token, answer, answer)
- assert {:error, "Invalid CAPTCHA"} == Native.validate(token, answer, answer <> "foobar")
+ assert {:error, :invalid} == Native.validate(token, answer, answer <> "foobar")
end
end
@@ -78,6 +78,7 @@ test "validate" do
assert is_binary(answer)
assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer)
+ Cachex.del(:used_captcha_cache, token)
end
test "doesn't validate invalid answer" do
@@ -92,7 +93,7 @@ test "doesn't validate invalid answer" do
assert is_binary(answer)
- assert {:error, "Invalid answer data"} =
+ assert {:error, :invalid_answer_data} =
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar")
end
@@ -108,7 +109,7 @@ test "nil answer_data" do
assert is_binary(answer)
- assert {:error, "Invalid answer data"} =
+ assert {:error, :invalid_answer_data} =
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil)
end
end
diff --git a/test/filter_test.exs b/test/filter_test.exs
index b2a8330ee..63a30c736 100644
--- a/test/filter_test.exs
+++ b/test/filter_test.exs
@@ -141,17 +141,15 @@ test "updating a filter" do
context: ["home"]
}
- query_two = %Pleroma.Filter{
- user_id: user.id,
- filter_id: 1,
+ changes = %{
phrase: "who",
context: ["home", "timeline"]
}
{:ok, filter_one} = Pleroma.Filter.create(query_one)
- {:ok, filter_two} = Pleroma.Filter.update(query_two)
+ {:ok, filter_two} = Pleroma.Filter.update(filter_one, changes)
assert filter_one != filter_two
- assert filter_two.phrase == query_two.phrase
- assert filter_two.context == query_two.context
+ assert filter_two.phrase == changes.phrase
+ assert filter_two.context == changes.context
end
end
diff --git a/test/fixtures/tesla_mock/craigmaloney.json b/test/fixtures/tesla_mock/craigmaloney.json
new file mode 100644
index 000000000..56ea9c7c3
--- /dev/null
+++ b/test/fixtures/tesla_mock/craigmaloney.json
@@ -0,0 +1,112 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "CacheFile": "pt:CacheFile",
+ "Hashtag": "as:Hashtag",
+ "Infohash": "pt:Infohash",
+ "RsaSignature2017": "https://w3id.org/security#RsaSignature2017",
+ "category": "sc:category",
+ "commentsEnabled": {
+ "@id": "pt:commentsEnabled",
+ "@type": "sc:Boolean"
+ },
+ "downloadEnabled": {
+ "@id": "pt:downloadEnabled",
+ "@type": "sc:Boolean"
+ },
+ "expires": "sc:expires",
+ "fps": {
+ "@id": "pt:fps",
+ "@type": "sc:Number"
+ },
+ "language": "sc:inLanguage",
+ "licence": "sc:license",
+ "originallyPublishedAt": "sc:datePublished",
+ "position": {
+ "@id": "pt:position",
+ "@type": "sc:Number"
+ },
+ "pt": "https://joinpeertube.org/ns#",
+ "sc": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "size": {
+ "@id": "pt:size",
+ "@type": "sc:Number"
+ },
+ "startTimestamp": {
+ "@id": "pt:startTimestamp",
+ "@type": "sc:Number"
+ },
+ "state": {
+ "@id": "pt:state",
+ "@type": "sc:Number"
+ },
+ "stopTimestamp": {
+ "@id": "pt:stopTimestamp",
+ "@type": "sc:Number"
+ },
+ "subtitleLanguage": "sc:subtitleLanguage",
+ "support": {
+ "@id": "pt:support",
+ "@type": "sc:Text"
+ },
+ "uuid": "sc:identifier",
+ "views": {
+ "@id": "pt:views",
+ "@type": "sc:Number"
+ },
+ "waitTranscoding": {
+ "@id": "pt:waitTranscoding",
+ "@type": "sc:Boolean"
+ }
+ },
+ {
+ "comments": {
+ "@id": "as:comments",
+ "@type": "@id"
+ },
+ "dislikes": {
+ "@id": "as:dislikes",
+ "@type": "@id"
+ },
+ "likes": {
+ "@id": "as:likes",
+ "@type": "@id"
+ },
+ "playlists": {
+ "@id": "pt:playlists",
+ "@type": "@id"
+ },
+ "shares": {
+ "@id": "as:shares",
+ "@type": "@id"
+ }
+ }
+ ],
+ "endpoints": {
+ "sharedInbox": "https://peertube.social/inbox"
+ },
+ "followers": "https://peertube.social/accounts/craigmaloney/followers",
+ "following": "https://peertube.social/accounts/craigmaloney/following",
+ "icon": {
+ "mediaType": "image/png",
+ "type": "Image",
+ "url": "https://peertube.social/lazy-static/avatars/87bd694b-95bc-4066-83f4-bddfcd2b9caa.png"
+ },
+ "id": "https://peertube.social/accounts/craigmaloney",
+ "inbox": "https://peertube.social/accounts/craigmaloney/inbox",
+ "name": "Craig Maloney",
+ "outbox": "https://peertube.social/accounts/craigmaloney/outbox",
+ "playlists": "https://peertube.social/accounts/craigmaloney/playlists",
+ "preferredUsername": "craigmaloney",
+ "publicKey": {
+ "id": "https://peertube.social/accounts/craigmaloney#main-key",
+ "owner": "https://peertube.social/accounts/craigmaloney",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9qvGIYUW01yc8CCsrwxK\n5OXlV5s7EbNWY8tJr/p1oGuELZwAnG2XKxtdbvgcCT+YxL5uRXIdCFIIIKrzRFr/\nHfS0mOgNT9u3gu+SstCNgtatciT0RVP77yiC3b2NHq1NRRvvVhzQb4cpIWObIxqh\nb2ypDClTc7XaKtgmQCbwZlGyZMT+EKz/vustD6BlpGsglRkm7iES6s1PPGb1BU+n\nS94KhbS2DOFiLcXCVWt0QarokIIuKznp4+xP1axKyP+SkT5AHx08Nd5TYFb2C1Jl\nz0WD/1q0mAN62m7QrA3SQPUgB+wWD+S3Nzf7FwNPiP4srbBgxVEUnji/r9mQ6BXC\nrQIDAQAB\n-----END PUBLIC KEY-----"
+ },
+ "summary": null,
+ "type": "Person",
+ "url": "https://peertube.social/accounts/craigmaloney"
+}
diff --git a/test/fixtures/tesla_mock/peertube-social.json b/test/fixtures/tesla_mock/peertube-social.json
new file mode 100644
index 000000000..0e996ba35
--- /dev/null
+++ b/test/fixtures/tesla_mock/peertube-social.json
@@ -0,0 +1,234 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "CacheFile": "pt:CacheFile",
+ "Hashtag": "as:Hashtag",
+ "Infohash": "pt:Infohash",
+ "RsaSignature2017": "https://w3id.org/security#RsaSignature2017",
+ "category": "sc:category",
+ "commentsEnabled": {
+ "@id": "pt:commentsEnabled",
+ "@type": "sc:Boolean"
+ },
+ "downloadEnabled": {
+ "@id": "pt:downloadEnabled",
+ "@type": "sc:Boolean"
+ },
+ "expires": "sc:expires",
+ "fps": {
+ "@id": "pt:fps",
+ "@type": "sc:Number"
+ },
+ "language": "sc:inLanguage",
+ "licence": "sc:license",
+ "originallyPublishedAt": "sc:datePublished",
+ "position": {
+ "@id": "pt:position",
+ "@type": "sc:Number"
+ },
+ "pt": "https://joinpeertube.org/ns#",
+ "sc": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "size": {
+ "@id": "pt:size",
+ "@type": "sc:Number"
+ },
+ "startTimestamp": {
+ "@id": "pt:startTimestamp",
+ "@type": "sc:Number"
+ },
+ "state": {
+ "@id": "pt:state",
+ "@type": "sc:Number"
+ },
+ "stopTimestamp": {
+ "@id": "pt:stopTimestamp",
+ "@type": "sc:Number"
+ },
+ "subtitleLanguage": "sc:subtitleLanguage",
+ "support": {
+ "@id": "pt:support",
+ "@type": "sc:Text"
+ },
+ "uuid": "sc:identifier",
+ "views": {
+ "@id": "pt:views",
+ "@type": "sc:Number"
+ },
+ "waitTranscoding": {
+ "@id": "pt:waitTranscoding",
+ "@type": "sc:Boolean"
+ }
+ },
+ {
+ "comments": {
+ "@id": "as:comments",
+ "@type": "@id"
+ },
+ "dislikes": {
+ "@id": "as:dislikes",
+ "@type": "@id"
+ },
+ "likes": {
+ "@id": "as:likes",
+ "@type": "@id"
+ },
+ "playlists": {
+ "@id": "pt:playlists",
+ "@type": "@id"
+ },
+ "shares": {
+ "@id": "as:shares",
+ "@type": "@id"
+ }
+ }
+ ],
+ "attributedTo": [
+ {
+ "id": "https://peertube.social/accounts/craigmaloney",
+ "type": "Person"
+ },
+ {
+ "id": "https://peertube.social/video-channels/9909c7d9-6b5b-4aae-9164-c1af7229c91c",
+ "type": "Group"
+ }
+ ],
+ "category": {
+ "identifier": "15",
+ "name": "Science & Technology"
+ },
+ "cc": [
+ "https://peertube.social/accounts/craigmaloney/followers"
+ ],
+ "comments": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe/comments",
+ "commentsEnabled": true,
+ "content": "Support this and our other Michigan!/usr/group videos and meetings. Learn more at http://mug.org/membership\n\nTwenty Years in Jail: FreeBSD's Jails, Then and Now\n\nJails started as a limited virtualization system, but over the last two years they've...",
+ "dislikes": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe/dislikes",
+ "downloadEnabled": true,
+ "duration": "PT5151S",
+ "icon": {
+ "height": 122,
+ "mediaType": "image/jpeg",
+ "type": "Image",
+ "url": "https://peertube.social/static/thumbnails/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe.jpg",
+ "width": 223
+ },
+ "id": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe",
+ "language": {
+ "identifier": "en",
+ "name": "English"
+ },
+ "licence": {
+ "identifier": "1",
+ "name": "Attribution"
+ },
+ "likes": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe/likes",
+ "mediaType": "text/markdown",
+ "name": "Twenty Years in Jail: FreeBSD's Jails, Then and Now",
+ "originallyPublishedAt": "2019-08-13T00:00:00.000Z",
+ "published": "2020-02-12T01:06:08.054Z",
+ "sensitive": false,
+ "shares": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe/announces",
+ "state": 1,
+ "subtitleLanguage": [],
+ "support": "Learn more at http://mug.org",
+ "tag": [
+ {
+ "name": "linux",
+ "type": "Hashtag"
+ },
+ {
+ "name": "mug.org",
+ "type": "Hashtag"
+ },
+ {
+ "name": "open",
+ "type": "Hashtag"
+ },
+ {
+ "name": "oss",
+ "type": "Hashtag"
+ },
+ {
+ "name": "source",
+ "type": "Hashtag"
+ }
+ ],
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type": "Video",
+ "updated": "2020-02-15T15:01:09.474Z",
+ "url": [
+ {
+ "href": "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe",
+ "mediaType": "text/html",
+ "type": "Link"
+ },
+ {
+ "fps": 30,
+ "height": 240,
+ "href": "https://peertube.social/static/webseed/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-240.mp4",
+ "mediaType": "video/mp4",
+ "size": 119465800,
+ "type": "Link"
+ },
+ {
+ "height": 240,
+ "href": "https://peertube.social/static/torrents/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-240.torrent",
+ "mediaType": "application/x-bittorrent",
+ "type": "Link"
+ },
+ {
+ "height": 240,
+ "href": "magnet:?xs=https%3A%2F%2Fpeertube.social%2Fstatic%2Ftorrents%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-240.torrent&xt=urn:btih:b3365331a8543bf48d09add56d7fe4b1cbbb5659&dn=Twenty+Years+in+Jail%3A+FreeBSD's+Jails%2C+Then+and+Now&tr=wss%3A%2F%2Fpeertube.social%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.social%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fwebseed%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-240.mp4",
+ "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+ "type": "Link"
+ },
+ {
+ "fps": 30,
+ "height": 360,
+ "href": "https://peertube.social/static/webseed/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-360.mp4",
+ "mediaType": "video/mp4",
+ "size": 143930318,
+ "type": "Link"
+ },
+ {
+ "height": 360,
+ "href": "https://peertube.social/static/torrents/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-360.torrent",
+ "mediaType": "application/x-bittorrent",
+ "type": "Link"
+ },
+ {
+ "height": 360,
+ "href": "magnet:?xs=https%3A%2F%2Fpeertube.social%2Fstatic%2Ftorrents%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-360.torrent&xt=urn:btih:0d37b23c98cb0d89e28b5dc8f49b3c97a041e569&dn=Twenty+Years+in+Jail%3A+FreeBSD's+Jails%2C+Then+and+Now&tr=wss%3A%2F%2Fpeertube.social%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.social%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fwebseed%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-360.mp4",
+ "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+ "type": "Link"
+ },
+ {
+ "fps": 30,
+ "height": 480,
+ "href": "https://peertube.social/static/webseed/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-480.mp4",
+ "mediaType": "video/mp4",
+ "size": 130530754,
+ "type": "Link"
+ },
+ {
+ "height": 480,
+ "href": "https://peertube.social/static/torrents/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-480.torrent",
+ "mediaType": "application/x-bittorrent",
+ "type": "Link"
+ },
+ {
+ "height": 480,
+ "href": "magnet:?xs=https%3A%2F%2Fpeertube.social%2Fstatic%2Ftorrents%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-480.torrent&xt=urn:btih:3a13ff822ad9494165eff6167183ddaaabc1372a&dn=Twenty+Years+in+Jail%3A+FreeBSD's+Jails%2C+Then+and+Now&tr=wss%3A%2F%2Fpeertube.social%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.social%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fwebseed%2F278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe-480.mp4",
+ "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+ "type": "Link"
+ }
+ ],
+ "uuid": "278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe",
+ "views": 2,
+ "waitTranscoding": false
+}
diff --git a/test/instance_static/add/shortcode.png b/test/instance_static/add/shortcode.png
new file mode 100644
index 000000000..8f50fa023
Binary files /dev/null and b/test/instance_static/add/shortcode.png differ
diff --git a/test/instance_static/emoji/pack_bad_sha/blank.png b/test/instance_static/emoji/pack_bad_sha/blank.png
new file mode 100644
index 000000000..8f50fa023
Binary files /dev/null and b/test/instance_static/emoji/pack_bad_sha/blank.png differ
diff --git a/test/instance_static/emoji/pack_bad_sha/pack.json b/test/instance_static/emoji/pack_bad_sha/pack.json
new file mode 100644
index 000000000..35caf4298
--- /dev/null
+++ b/test/instance_static/emoji/pack_bad_sha/pack.json
@@ -0,0 +1,13 @@
+{
+ "pack": {
+ "license": "Test license",
+ "homepage": "https://pleroma.social",
+ "description": "Test description",
+ "can-download": true,
+ "share-files": true,
+ "download-sha256": "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238"
+ },
+ "files": {
+ "blank": "blank.png"
+ }
+}
\ No newline at end of file
diff --git a/test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip b/test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip
new file mode 100644
index 000000000..148446c64
Binary files /dev/null and b/test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip differ
diff --git a/test/instance_static/emoji/test_pack/pack.json b/test/instance_static/emoji/test_pack/pack.json
index 5a8ee75f9..481891b08 100644
--- a/test/instance_static/emoji/test_pack/pack.json
+++ b/test/instance_static/emoji/test_pack/pack.json
@@ -1,13 +1,11 @@
{
- "pack": {
- "license": "Test license",
- "homepage": "https://pleroma.social",
- "description": "Test description",
-
- "share-files": true
- },
-
"files": {
"blank": "blank.png"
+ },
+ "pack": {
+ "description": "Test description",
+ "homepage": "https://pleroma.social",
+ "license": "Test license",
+ "share-files": true
}
-}
+}
\ No newline at end of file
diff --git a/test/instance_static/emoji/test_pack_nonshared/pack.json b/test/instance_static/emoji/test_pack_nonshared/pack.json
index b96781f81..93d643a5f 100644
--- a/test/instance_static/emoji/test_pack_nonshared/pack.json
+++ b/test/instance_static/emoji/test_pack_nonshared/pack.json
@@ -3,14 +3,11 @@
"license": "Test license",
"homepage": "https://pleroma.social",
"description": "Test description",
-
"fallback-src": "https://nonshared-pack",
"fallback-src-sha256": "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF",
-
"share-files": false
},
-
"files": {
"blank": "blank.png"
}
-}
+}
\ No newline at end of file
diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs
index bd229c55f..109c7b4cb 100644
--- a/test/integration/mastodon_websocket_test.exs
+++ b/test/integration/mastodon_websocket_test.exs
@@ -12,17 +12,14 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth
+ @moduletag needs_streamer: true, capture_log: true
+
@path Pleroma.Web.Endpoint.url()
|> URI.parse()
|> Map.put(:scheme, "ws")
|> Map.put(:path, "/api/v1/streaming")
|> URI.to_string()
- setup_all do
- start_supervised(Pleroma.Web.Streamer.supervisor())
- :ok
- end
-
def start_socket(qs \\ nil, headers \\ []) do
path =
case qs do
diff --git a/test/marker_test.exs b/test/marker_test.exs
index c80ae16b6..5b6d0b4a4 100644
--- a/test/marker_test.exs
+++ b/test/marker_test.exs
@@ -8,12 +8,39 @@ defmodule Pleroma.MarkerTest do
import Pleroma.Factory
+ describe "multi_set_unread_count/3" do
+ test "returns multi" do
+ user = insert(:user)
+
+ assert %Ecto.Multi{
+ operations: [marker: {:run, _}, counters: {:run, _}]
+ } =
+ Marker.multi_set_last_read_id(
+ Ecto.Multi.new(),
+ user,
+ "notifications"
+ )
+ end
+
+ test "return empty multi" do
+ user = insert(:user)
+ multi = Ecto.Multi.new()
+ assert Marker.multi_set_last_read_id(multi, user, "home") == multi
+ end
+ end
+
describe "get_markers/2" do
test "returns user markers" do
user = insert(:user)
marker = insert(:marker, user: user)
+ insert(:notification, user: user)
+ insert(:notification, user: user)
insert(:marker, timeline: "home", user: user)
- assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)]
+
+ assert Marker.get_markers(
+ user,
+ ["notifications"]
+ ) == [%Marker{refresh_record(marker) | unread_count: 2}]
end
end
diff --git a/test/mfa/backup_codes_test.exs b/test/mfa/backup_codes_test.exs
new file mode 100644
index 000000000..7bc01b36b
--- /dev/null
+++ b/test/mfa/backup_codes_test.exs
@@ -0,0 +1,11 @@
+defmodule Pleroma.MFA.BackupCodesTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.MFA.BackupCodes
+
+ test "generate backup codes" do
+ codes = BackupCodes.generate(number_of_codes: 2, length: 4)
+
+ assert [<<_::bytes-size(4)>>, <<_::bytes-size(4)>>] = codes
+ end
+end
diff --git a/test/mfa/totp_test.exs b/test/mfa/totp_test.exs
new file mode 100644
index 000000000..50153d208
--- /dev/null
+++ b/test/mfa/totp_test.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.MFA.TOTPTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.MFA.TOTP
+
+ test "create provisioning_uri to generate qrcode" do
+ uri =
+ TOTP.provisioning_uri("test-secrcet", "test@example.com",
+ issuer: "Plerome-42",
+ digits: 8,
+ period: 60
+ )
+
+ assert uri ==
+ "otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet"
+ end
+end
diff --git a/test/mfa_test.exs b/test/mfa_test.exs
new file mode 100644
index 000000000..94bc48c26
--- /dev/null
+++ b/test/mfa_test.exs
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFATest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+ alias Comeonin.Pbkdf2
+ alias Pleroma.MFA
+
+ describe "mfa_settings" do
+ test "returns settings user's" do
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true}
+ }
+ )
+
+ settings = MFA.mfa_settings(user)
+ assert match?(^settings, %{enabled: true, totp: true})
+ end
+ end
+
+ describe "generate backup codes" do
+ test "returns backup codes" do
+ user = insert(:user)
+
+ {:ok, [code1, code2]} = MFA.generate_backup_codes(user)
+ updated_user = refresh_record(user)
+ [hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes
+ assert Pbkdf2.checkpw(code1, hash1)
+ assert Pbkdf2.checkpw(code2, hash2)
+ end
+ end
+
+ describe "invalidate_backup_code" do
+ test "invalid used code" do
+ user = insert(:user)
+
+ {:ok, _} = MFA.generate_backup_codes(user)
+ user = refresh_record(user)
+ assert length(user.multi_factor_authentication_settings.backup_codes) == 2
+ [hash_code | _] = user.multi_factor_authentication_settings.backup_codes
+
+ {:ok, user} = MFA.invalidate_backup_code(user, hash_code)
+
+ assert length(user.multi_factor_authentication_settings.backup_codes) == 1
+ end
+ end
+end
diff --git a/test/notification_test.exs b/test/notification_test.exs
index a7f53e319..0783c325d 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -24,7 +24,7 @@ test "creates a notification for an emoji reaction" do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
- {:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+ {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
{:ok, [notification]} = Notification.create_notifications(activity)
@@ -47,6 +47,9 @@ test "notifies someone when they are directly addressed" do
assert notified_ids == [other_user.id, third_user.id]
assert notification.activity_id == activity.id
assert other_notification.activity_id == activity.id
+
+ assert [%Pleroma.Marker{unread_count: 2}] =
+ Pleroma.Marker.get_markers(other_user, ["notifications"])
end
test "it creates a notification for subscribed users" do
@@ -162,14 +165,18 @@ test "does not create a notification for subscribed users if status is a reply"
@tag needs_streamer: true
test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
user = insert(:user)
- task = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
- task_user_notification = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
- Streamer.add_socket("user", %{transport_pid: task.pid, assigns: %{user: user}})
- Streamer.add_socket(
- "user:notification",
- %{transport_pid: task_user_notification.pid, assigns: %{user: user}}
- )
+ task =
+ Task.async(fn ->
+ Streamer.add_socket("user", user)
+ assert_receive {:render_with_user, _, _, _}, 4_000
+ end)
+
+ task_user_notification =
+ Task.async(fn ->
+ Streamer.add_socket("user:notification", user)
+ assert_receive {:render_with_user, _, _, _}, 4_000
+ end)
activity = insert(:note_activity)
@@ -312,9 +319,7 @@ test "it creates `follow` notification for approved Follow activity" do
})
end
- test "if `follow_request` notifications are enabled, " <>
- "it creates `follow_request` notification for pending Follow activity" do
- clear_config([:notifications, :enable_follow_request_notifications], true)
+ test "it creates `follow_request` notification for pending Follow activity" do
user = insert(:user)
followed_user = insert(:user, locked: true)
@@ -333,21 +338,6 @@ test "if `follow_request` notifications are enabled, " <>
assert %{type: "follow"} = NotificationView.render("show.json", render_opts)
end
- test "if `follow_request` notifications are disabled, " <>
- "it does NOT create `follow*` notification for pending Follow activity" do
- clear_config([:notifications, :enable_follow_request_notifications], false)
- user = insert(:user)
- followed_user = insert(:user, locked: true)
-
- {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
- refute FollowingRelationship.following?(user, followed_user)
- assert [] = Notification.for_user(followed_user)
-
- # After request is accepted, no new notifications are generated:
- assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
- assert [] = Notification.for_user(followed_user)
- end
-
test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user)
followed_user = insert(:user, locked: false)
@@ -362,6 +352,15 @@ test "it doesn't create a notification for follow-unfollow-follow chains" do
notification_id = notification.id
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
end
+
+ test "dismisses the notification on follow request rejection" do
+ user = insert(:user, locked: true)
+ follower = insert(:user)
+ {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user)
+ assert [notification] = Notification.for_user(user)
+ {:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
+ assert [] = Notification.for_user(user)
+ end
end
describe "get notification" do
@@ -470,6 +469,16 @@ test "it sets all notifications as read up to a specified notification ID" do
assert n1.seen == true
assert n2.seen == true
assert n3.seen == false
+
+ assert %Pleroma.Marker{} =
+ m =
+ Pleroma.Repo.get_by(
+ Pleroma.Marker,
+ user_id: other_user.id,
+ timeline: "notifications"
+ )
+
+ assert m.last_read_id == to_string(n2.id)
end
end
@@ -669,6 +678,37 @@ test "it returns thread-muting recipient in disabled recipients list" do
assert [other_user] == disabled_receivers
refute other_user in enabled_receivers
end
+
+ test "it returns non-following domain-blocking recipient in disabled recipients list" do
+ blocked_domain = "blocked.domain"
+ user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"})
+ other_user = insert(:user)
+
+ {:ok, other_user} = User.block_domain(other_user, blocked_domain)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}!"})
+
+ {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
+
+ assert [] == enabled_receivers
+ assert [other_user] == disabled_receivers
+ end
+
+ test "it returns following domain-blocking recipient in enabled recipients list" do
+ blocked_domain = "blocked.domain"
+ user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"})
+ other_user = insert(:user)
+
+ {:ok, other_user} = User.block_domain(other_user, blocked_domain)
+ {:ok, other_user} = User.follow(other_user, user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}!"})
+
+ {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
+
+ assert [other_user] == enabled_receivers
+ assert [] == disabled_receivers
+ end
end
describe "notification lifecycle" do
@@ -701,7 +741,7 @@ test "liking an activity results in 1 notification, then 0 if the activity is un
assert length(Notification.for_user(user)) == 1
- {:ok, _, _, _} = CommonAPI.unfavorite(activity.id, other_user)
+ {:ok, _} = CommonAPI.unfavorite(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
@@ -735,7 +775,7 @@ test "repeating an activity results in 1 notification, then 0 if the activity is
assert length(Notification.for_user(user)) == 1
- {:ok, _, _} = CommonAPI.unrepeat(activity.id, other_user)
+ {:ok, _} = CommonAPI.unrepeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
@@ -931,7 +971,7 @@ test "it doesn't return notifications for blocked user" do
assert Notification.for_user(user) == []
end
- test "it doesn't return notifications for blocked domain" do
+ test "it doesn't return notifications for domain-blocked non-followed user" do
user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
@@ -941,6 +981,18 @@ test "it doesn't return notifications for blocked domain" do
assert Notification.for_user(user) == []
end
+ test "it returns notifications for domain-blocked but followed user" do
+ user = insert(:user)
+ blocked = insert(:user, ap_id: "http://some-domain.com")
+
+ {:ok, user} = User.block_domain(user, "some-domain.com")
+ {:ok, _} = User.follow(user, blocked)
+
+ {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
+
+ assert length(Notification.for_user(user)) == 1
+ end
+
test "it doesn't return notifications for muted thread" do
user = insert(:user)
another_user = insert(:user)
@@ -971,7 +1023,8 @@ test "it doesn't return notifications from a blocked user when with_muted is set
assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
- test "it doesn't return notifications from a domain-blocked user when with_muted is set" do
+ test "when with_muted is set, " <>
+ "it doesn't return notifications from a domain-blocked non-followed user" do
user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
diff --git a/test/plugs/ensure_authenticated_plug_test.exs b/test/plugs/ensure_authenticated_plug_test.exs
index 689fe757f..a0667c5e0 100644
--- a/test/plugs/ensure_authenticated_plug_test.exs
+++ b/test/plugs/ensure_authenticated_plug_test.exs
@@ -24,11 +24,36 @@ test "it continues if a user is assigned", %{conn: conn} do
end
end
+ test "it halts if user is assigned and MFA enabled", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: true}})
+ |> assign(:auth_credentials, %{password: "xd-42"})
+ |> EnsureAuthenticatedPlug.call(%{})
+
+ assert conn.status == 403
+ assert conn.halted == true
+
+ assert conn.resp_body ==
+ "{\"error\":\"Two-factor authentication enabled, you must use a access token.\"}"
+ end
+
+ test "it continues if user is assigned and MFA disabled", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: false}})
+ |> assign(:auth_credentials, %{password: "xd-42"})
+ |> EnsureAuthenticatedPlug.call(%{})
+
+ refute conn.status == 403
+ refute conn.halted
+ end
+
describe "with :if_func / :unless_func options" do
setup do
%{
- true_fn: fn -> true end,
- false_fn: fn -> false end
+ true_fn: fn _conn -> true end,
+ false_fn: fn _conn -> false end
}
end
diff --git a/test/signature_test.exs b/test/signature_test.exs
index d5a2a62c4..a7a75aa4d 100644
--- a/test/signature_test.exs
+++ b/test/signature_test.exs
@@ -44,7 +44,8 @@ test "it returns key" do
test "it returns error when not found user" do
assert capture_log(fn ->
- assert Signature.fetch_public_key(make_fake_conn("test-ap_id")) == {:error, :error}
+ assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) ==
+ {:error, :error}
end) =~ "[error] Could not decode user"
end
@@ -64,7 +65,7 @@ test "it returns key" do
test "it returns error when not found user" do
assert capture_log(fn ->
- {:error, _} = Signature.refetch_public_key(make_fake_conn("test-ap_id"))
+ {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id"))
end) =~ "[error] Could not decode user"
end
end
@@ -100,12 +101,21 @@ test "it returns error" do
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
- "https://example.com/users/1234"
+ {:ok, "https://example.com/users/1234"}
end
test "it properly deduces the actor id for mastodon and pleroma" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
- "https://example.com/users/1234"
+ {:ok, "https://example.com/users/1234"}
+ end
+
+ test "it calls webfinger for 'acct:' accounts" do
+ with_mock(Pleroma.Web.WebFinger,
+ finger: fn _ -> %{"ap_id" => "https://gensokyo.2hu/users/raymoo"} end
+ ) do
+ assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") ==
+ {:ok, "https://gensokyo.2hu/users/raymoo"}
+ end
end
end
diff --git a/test/support/builders/activity_builder.ex b/test/support/builders/activity_builder.ex
index 6e5a8e059..7c4950bfa 100644
--- a/test/support/builders/activity_builder.ex
+++ b/test/support/builders/activity_builder.ex
@@ -21,7 +21,15 @@ def build(data \\ %{}, opts \\ %{}) do
def insert(data \\ %{}, opts \\ %{}) do
activity = build(data, opts)
- ActivityPub.insert(activity)
+
+ case ActivityPub.insert(activity) do
+ ok = {:ok, activity} ->
+ ActivityPub.notify_and_stream(activity)
+ ok
+
+ error ->
+ error
+ end
end
def insert_list(times, data \\ %{}, opts \\ %{}) do
diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex
index fcfea666f..0d0490714 100644
--- a/test/support/builders/user_builder.ex
+++ b/test/support/builders/user_builder.ex
@@ -11,6 +11,7 @@ def build(data \\ %{}) do
bio: "A tester.",
ap_id: "some id",
last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
+ multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
notification_settings: %Pleroma.User.NotificationSetting{}
}
diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex
index 6dae94edf..7b0c1d5af 100644
--- a/test/support/captcha_mock.ex
+++ b/test/support/captcha_mock.ex
@@ -6,12 +6,16 @@ defmodule Pleroma.Captcha.Mock do
alias Pleroma.Captcha.Service
@behaviour Service
+ @solution "63615261b77f5354fb8c4e4986477555"
+
+ def solution, do: @solution
+
@impl Service
def new,
do: %{
type: :mock,
token: "afa1815e14e29355e6c8f6b143a39fa2",
- answer_data: "63615261b77f5354fb8c4e4986477555",
+ answer_data: @solution,
url: "https://example.org/captcha.png"
}
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index fa30a0c41..b23918dd1 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -74,7 +74,7 @@ defp json_response_and_validate_schema(
status = Plug.Conn.Status.code(status)
unless lookup[op_id].responses[status] do
- err = "Response schema not found for #{conn.status} #{conn.method} #{conn.request_path}"
+ err = "Response schema not found for #{status} #{conn.method} #{conn.request_path}"
flunk(err)
end
@@ -139,7 +139,11 @@ defp ensure_federating_or_authenticated(conn, url, user) do
end
if tags[:needs_streamer] do
- start_supervised(Pleroma.Web.Streamer.supervisor())
+ start_supervised(%{
+ id: Pleroma.Web.Streamer.registry(),
+ start:
+ {Registry, :start_link, [[keys: :duplicate, name: Pleroma.Web.Streamer.registry()]]}
+ })
end
{:ok, conn: Phoenix.ConnTest.build_conn()}
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 1669f2520..ba8848952 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -40,7 +40,11 @@ defmodule Pleroma.DataCase do
end
if tags[:needs_streamer] do
- start_supervised(Pleroma.Web.Streamer.supervisor())
+ start_supervised(%{
+ id: Pleroma.Web.Streamer.registry(),
+ start:
+ {Registry, :start_link, [[keys: :duplicate, name: Pleroma.Web.Streamer.registry()]]}
+ })
end
:ok
diff --git a/test/support/factory.ex b/test/support/factory.ex
index f0b797fd4..c8c45e2a7 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -32,7 +32,9 @@ def user_factory do
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"),
last_digest_emailed_at: NaiveDateTime.utc_now(),
- notification_settings: %Pleroma.User.NotificationSetting{}
+ last_refreshed_at: NaiveDateTime.utc_now(),
+ notification_settings: %Pleroma.User.NotificationSetting{},
+ multi_factor_authentication_settings: %Pleroma.MFA.Settings{}
}
%{
@@ -421,4 +423,13 @@ def marker_factory do
last_read_id: "1"
}
end
+
+ def mfa_token_factory do
+ %Pleroma.MFA.Token{
+ token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
+ authorization: build(:oauth_authorization),
+ valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10),
+ user: build(:user)
+ }
+ end
end
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index e68e9bfd2..26281b45e 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -40,12 +40,18 @@ defmacro __using__(_opts) do
clear_config: 2
]
- def to_datetime(naive_datetime) do
+ def to_datetime(%NaiveDateTime{} = naive_datetime) do
naive_datetime
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.truncate(:second)
end
+ def to_datetime(datetime) when is_binary(datetime) do
+ datetime
+ |> NaiveDateTime.from_iso8601!()
+ |> to_datetime()
+ end
+
def collect_ids(collection) do
collection
|> Enum.map(& &1.id)
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 20cb2b3d1..3a95e92da 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -211,7 +211,7 @@ def get(
end
def get(
- "https://squeet.me/xrd/?uri=lain@squeet.me",
+ "https://squeet.me/xrd/?uri=acct:lain@squeet.me",
_,
_,
[{"accept", "application/xrd+xml,application/jrd+json"}]
@@ -308,6 +308,22 @@ def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
}}
end
+ def get("https://peertube.social/accounts/craigmaloney", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/craigmaloney.json")
+ }}
+ end
+
+ def get("https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/peertube-social.json")
+ }}
+ end
+
def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, _, [
{"accept", "application/activity+json"}
]) do
@@ -854,7 +870,7 @@ def get(
end
def get(
- "https://social.heldscal.la/.well-known/webfinger?resource=shp@social.heldscal.la",
+ "https://social.heldscal.la/.well-known/webfinger?resource=acct:shp@social.heldscal.la",
_,
_,
[{"accept", "application/xrd+xml,application/jrd+json"}]
@@ -867,7 +883,7 @@ def get(
end
def get(
- "https://social.heldscal.la/.well-known/webfinger?resource=invalid_content@social.heldscal.la",
+ "https://social.heldscal.la/.well-known/webfinger?resource=acct:invalid_content@social.heldscal.la",
_,
_,
[{"accept", "application/xrd+xml,application/jrd+json"}]
@@ -884,7 +900,7 @@ def get("http://framatube.org/.well-known/host-meta", _, _, _) do
end
def get(
- "http://framatube.org/main/xrd?uri=framasoft@framatube.org",
+ "http://framatube.org/main/xrd?uri=acct:framasoft@framatube.org",
_,
_,
[{"accept", "application/xrd+xml,application/jrd+json"}]
@@ -943,7 +959,7 @@ def get("http://gerzilla.de/.well-known/host-meta", _, _, _) do
end
def get(
- "https://gerzilla.de/xrd/?uri=kaniini@gerzilla.de",
+ "https://gerzilla.de/xrd/?uri=acct:kaniini@gerzilla.de",
_,
_,
[{"accept", "application/xrd+xml,application/jrd+json"}]
@@ -1139,7 +1155,7 @@ def get("http://404.site" <> _, _, _, _) do
end
def get(
- "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=lain@zetsubou.xn--q9jyb4c",
+ "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=acct:lain@zetsubou.xn--q9jyb4c",
_,
_,
[{"accept", "application/xrd+xml,application/jrd+json"}]
@@ -1152,7 +1168,7 @@ def get(
end
def get(
- "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=https://zetsubou.xn--q9jyb4c/users/lain",
+ "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=acct:https://zetsubou.xn--q9jyb4c/users/lain",
_,
_,
[{"accept", "application/xrd+xml,application/jrd+json"}]
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index 8df835b56..e0fee7290 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -4,14 +4,17 @@
defmodule Mix.Tasks.Pleroma.UserTest do
alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
- import Pleroma.Factory
import ExUnit.CaptureIO
+ import Mock
+ import Pleroma.Factory
setup_all do
Mix.shell(Mix.Shell.Process)
@@ -87,12 +90,17 @@ test "user is not created" do
test "user is deleted" do
user = insert(:user)
- Mix.Tasks.Pleroma.User.run(["rm", user.nickname])
+ with_mock Pleroma.Web.Federator,
+ publish: fn _ -> nil end do
+ Mix.Tasks.Pleroma.User.run(["rm", user.nickname])
+ ObanHelpers.perform_all()
- assert_received {:mix_shell, :info, [message]}
- assert message =~ " deleted"
+ assert_received {:mix_shell, :info, [message]}
+ assert message =~ " deleted"
+ assert %{deactivated: true} = User.get_by_nickname(user.nickname)
- refute User.get_by_nickname(user.nickname)
+ assert called(Pleroma.Web.Federator.publish(:_))
+ end
end
test "no user to delete" do
diff --git a/test/user_search_test.exs b/test/user_search_test.exs
index cb847b516..17c63322a 100644
--- a/test/user_search_test.exs
+++ b/test/user_search_test.exs
@@ -172,6 +172,7 @@ test "works with URIs" do
|> Map.put(:search_rank, nil)
|> Map.put(:search_type, nil)
|> Map.put(:last_digest_emailed_at, nil)
+ |> Map.put(:multi_factor_authentication_settings, nil)
|> Map.put(:notification_settings, nil)
assert user == expected
diff --git a/test/user_test.exs b/test/user_test.exs
index 347c5be72..a3c75aa9b 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -15,7 +15,6 @@ defmodule Pleroma.UserTest do
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
- import Mock
import Pleroma.Factory
import ExUnit.CaptureLog
@@ -1131,20 +1130,11 @@ test ".delete_user_activities deletes all create activities", %{user: user} do
User.delete_user_activities(user)
- # TODO: Remove favorites, repeats, delete activities.
+ # TODO: Test removal favorites, repeats, delete activities.
refute Activity.get_by_id(activity.id)
end
- test "it deletes deactivated user" do
- {:ok, user} = insert(:user, deactivated: true) |> User.set_cache()
-
- {:ok, job} = User.delete(user)
- {:ok, _user} = ObanHelpers.perform(job)
-
- refute User.get_by_id(user.id)
- end
-
- test "it deletes a user, all follow relationships and all activities", %{user: user} do
+ test "it deactivates a user, all follow relationships and all activities", %{user: user} do
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
@@ -1164,8 +1154,7 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
- refute User.get_by_id(user.id)
- assert {:ok, nil} == Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
+ assert %{deactivated: true} = User.get_by_id(user.id)
user_activities =
user.ap_id
@@ -1180,31 +1169,6 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
refute Activity.get_by_id(like_two.id)
refute Activity.get_by_id(repeat.id)
end
-
- test_with_mock "it sends out User Delete activity",
- %{user: user},
- Pleroma.Web.ActivityPub.Publisher,
- [:passthrough],
- [] do
- Pleroma.Config.put([:instance, :federating], true)
-
- {:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
- {:ok, _} = User.follow(follower, user)
-
- {:ok, job} = User.delete(user)
- {:ok, _user} = ObanHelpers.perform(job)
-
- assert ObanHelpers.member?(
- %{
- "op" => "publish_one",
- "params" => %{
- "inbox" => "http://mastodon.example.org/inbox",
- "id" => "pleroma:fakeid"
- }
- },
- all_enqueued(worker: Pleroma.Workers.PublisherWorker)
- )
- end
end
test "get_public_key_for_ap_id fetches a user that's not in the db" do
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index eca526604..776ddc8d4 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -765,51 +765,110 @@ test "it requires authentication if instance is NOT federating", %{
end
end
- describe "POST /users/:nickname/outbox" do
- test "it rejects posts from other users / unauthenticated users", %{conn: conn} do
- data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
+ describe "POST /users/:nickname/outbox (C2S)" do
+ setup do
+ [
+ activity: %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "type" => "Create",
+ "object" => %{"type" => "Note", "content" => "AP C2S test"},
+ "to" => "https://www.w3.org/ns/activitystreams#Public",
+ "cc" => []
+ }
+ ]
+ end
+
+ test "it rejects posts from other users / unauthenticated users", %{
+ conn: conn,
+ activity: activity
+ } do
user = insert(:user)
other_user = insert(:user)
conn = put_req_header(conn, "content-type", "application/activity+json")
conn
- |> post("/users/#{user.nickname}/outbox", data)
+ |> post("/users/#{user.nickname}/outbox", activity)
|> json_response(403)
conn
|> assign(:user, other_user)
- |> post("/users/#{user.nickname}/outbox", data)
+ |> post("/users/#{user.nickname}/outbox", activity)
|> json_response(403)
end
- test "it inserts an incoming create activity into the database", %{conn: conn} do
- data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
+ test "it inserts an incoming create activity into the database", %{
+ conn: conn,
+ activity: activity
+ } do
user = insert(:user)
- conn =
+ result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
- |> post("/users/#{user.nickname}/outbox", data)
-
- result = json_response(conn, 201)
+ |> post("/users/#{user.nickname}/outbox", activity)
+ |> json_response(201)
assert Activity.get_by_ap_id(result["id"])
+ assert result["object"]
+ assert %Object{data: object} = Object.normalize(result["object"])
+ assert object["content"] == activity["object"]["content"]
end
- test "it rejects an incoming activity with bogus type", %{conn: conn} do
- data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
+ test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
user = insert(:user)
- data =
- data
- |> Map.put("type", "BadType")
+ activity =
+ activity
+ |> put_in(["object", "type"], "Benis")
+
+ _result =
+ conn
+ |> assign(:user, user)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/outbox", activity)
+ |> json_response(400)
+ end
+
+ test "it inserts an incoming sensitive activity into the database", %{
+ conn: conn,
+ activity: activity
+ } do
+ user = insert(:user)
+ conn = assign(conn, :user, user)
+ object = Map.put(activity["object"], "sensitive", true)
+ activity = Map.put(activity, "object", object)
+
+ response =
+ conn
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/outbox", activity)
+ |> json_response(201)
+
+ assert Activity.get_by_ap_id(response["id"])
+ assert response["object"]
+ assert %Object{data: response_object} = Object.normalize(response["object"])
+ assert response_object["sensitive"] == true
+ assert response_object["content"] == activity["object"]["content"]
+
+ representation =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get(response["id"])
+ |> json_response(200)
+
+ assert representation["object"]["sensitive"] == true
+ end
+
+ test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
+ user = insert(:user)
+ activity = Map.put(activity, "type", "BadType")
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
- |> post("/users/#{user.nickname}/outbox", data)
+ |> post("/users/#{user.nickname}/outbox", activity)
assert json_response(conn, 400)
end
@@ -1019,12 +1078,12 @@ test "it works for more than 10 users", %{conn: conn} do
assert result["totalItems"] == 15
end
- test "returns 403 if requester is not logged in", %{conn: conn} do
+ test "does not require authentication", %{conn: conn} do
user = insert(:user)
conn
|> get("/users/#{user.nickname}/followers")
- |> json_response(403)
+ |> json_response(200)
end
end
@@ -1116,12 +1175,12 @@ test "it works for more than 10 users", %{conn: conn} do
assert result["totalItems"] == 15
end
- test "returns 403 if requester is not logged in", %{conn: conn} do
+ test "does not require authentication", %{conn: conn} do
user = insert(:user)
conn
|> get("/users/#{user.nickname}/following")
- |> json_response(403)
+ |> json_response(200)
end
end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index edd7dfb22..0739cbfef 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -16,11 +16,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.Federator
+ import ExUnit.CaptureLog
+ import Mock
import Pleroma.Factory
import Tesla.Mock
- import Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -873,187 +873,6 @@ test "returns reblogs for users for whom reblogs have not been muted" do
end
end
- describe "react to an object" do
- test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
- Config.put([:instance, :federating], true)
- user = insert(:user)
- reactor = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
- assert object = Object.normalize(activity)
-
- {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
-
- assert called(Federator.publish(reaction_activity))
- end
-
- test "adds an emoji reaction activity to the db" do
- user = insert(:user)
- reactor = insert(:user)
- third_user = insert(:user)
- fourth_user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
- assert object = Object.normalize(activity)
-
- {:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
-
- assert reaction_activity
-
- assert reaction_activity.data["actor"] == reactor.ap_id
- assert reaction_activity.data["type"] == "EmojiReact"
- assert reaction_activity.data["content"] == "🔥"
- assert reaction_activity.data["object"] == object.data["id"]
- assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
- assert reaction_activity.data["context"] == object.data["context"]
- assert object.data["reaction_count"] == 1
- assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
-
- {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕")
-
- assert object.data["reaction_count"] == 2
- assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]]
-
- {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
-
- assert object.data["reaction_count"] == 3
-
- assert object.data["reactions"] == [
- ["🔥", [fourth_user.ap_id, reactor.ap_id]],
- ["☕", [third_user.ap_id]]
- ]
- end
-
- test "reverts emoji reaction on error" do
- [user, reactor] = insert_list(2, :user)
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
- object = Object.normalize(activity)
-
- with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
- assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
- end
-
- object = Object.get_by_ap_id(object.data["id"])
- refute object.data["reaction_count"]
- refute object.data["reactions"]
- end
- end
-
- describe "unreacting to an object" do
- test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
- Config.put([:instance, :federating], true)
- user = insert(:user)
- reactor = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
- assert object = Object.normalize(activity)
-
- {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
-
- assert called(Federator.publish(reaction_activity))
-
- {:ok, unreaction_activity, _object} =
- ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
-
- assert called(Federator.publish(unreaction_activity))
- end
-
- test "adds an undo activity to the db" do
- user = insert(:user)
- reactor = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
- assert object = Object.normalize(activity)
-
- {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
-
- {:ok, unreaction_activity, _object} =
- ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
-
- assert unreaction_activity.actor == reactor.ap_id
- assert unreaction_activity.data["object"] == reaction_activity.data["id"]
-
- object = Object.get_by_ap_id(object.data["id"])
- assert object.data["reaction_count"] == 0
- assert object.data["reactions"] == []
- end
-
- test "reverts emoji unreact on error" do
- [user, reactor] = insert_list(2, :user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
- object = Object.normalize(activity)
-
- {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀")
-
- with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
- assert {:error, :reverted} =
- ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
- end
-
- object = Object.get_by_ap_id(object.data["id"])
-
- assert object.data["reaction_count"] == 1
- assert object.data["reactions"] == [["😀", [reactor.ap_id]]]
- end
- end
-
- describe "unliking" do
- test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
- Config.put([:instance, :federating], true)
-
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = insert(:user)
-
- {:ok, object} = ActivityPub.unlike(user, object)
- refute called(Federator.publish())
-
- {:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id)
- object = Object.get_by_id(object.id)
- assert object.data["like_count"] == 1
-
- {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
- assert object.data["like_count"] == 0
-
- assert called(Federator.publish(unlike_activity))
- end
-
- test "reverts unliking on error" do
- note_activity = insert(:note_activity)
- user = insert(:user)
-
- {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
- object = Object.normalize(note_activity)
- assert object.data["like_count"] == 1
-
- with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
- assert {:error, :reverted} = ActivityPub.unlike(user, object)
- end
-
- assert Object.get_by_ap_id(object.data["id"]) == object
- assert object.data["like_count"] == 1
- assert Activity.get_by_id(like_activity.id)
- end
-
- test "unliking a previously liked object" do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = insert(:user)
-
- # Unliking something that hasn't been liked does nothing
- {:ok, object} = ActivityPub.unlike(user, object)
- assert object.data["like_count"] == 0
-
- {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
-
- object = Object.get_by_id(object.id)
- assert object.data["like_count"] == 1
-
- {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
- assert object.data["like_count"] == 0
-
- assert Activity.get_by_id(like_activity.id) == nil
- assert note_activity.actor in unlike_activity.recipients
- end
- end
-
describe "announcing an object" do
test "adds an announce activity to the db" do
note_activity = insert(:note_activity)
@@ -1123,52 +942,6 @@ test "does not add an announce activity to the db if the announcer is not the au
end
end
- describe "unannouncing an object" do
- test "unannouncing a previously announced object" do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = insert(:user)
-
- # Unannouncing an object that is not announced does nothing
- {:ok, object} = ActivityPub.unannounce(user, object)
- refute object.data["announcement_count"]
-
- {:ok, announce_activity, object} = ActivityPub.announce(user, object)
- assert object.data["announcement_count"] == 1
-
- {:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
- assert object.data["announcement_count"] == 0
-
- assert unannounce_activity.data["to"] == [
- User.ap_followers(user),
- object.data["actor"]
- ]
-
- assert unannounce_activity.data["type"] == "Undo"
- assert unannounce_activity.data["object"] == announce_activity.data
- assert unannounce_activity.data["actor"] == user.ap_id
- assert unannounce_activity.data["context"] == announce_activity.data["context"]
-
- assert Activity.get_by_id(announce_activity.id) == nil
- end
-
- test "reverts unannouncing on error" do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = insert(:user)
-
- {:ok, _announce_activity, object} = ActivityPub.announce(user, object)
- assert object.data["announcement_count"] == 1
-
- with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
- assert {:error, :reverted} = ActivityPub.unannounce(user, object)
- end
-
- object = Object.get_by_ap_id(object.data["id"])
- assert object.data["announcement_count"] == 1
- end
- end
-
describe "uploading files" do
test "copies the file to the configured folder" do
file = %Plug.Upload{
@@ -1275,7 +1048,7 @@ test "creates an undo activity for a pending follow request" do
end
end
- describe "blocking / unblocking" do
+ describe "blocking" do
test "reverts block activity on error" do
[blocker, blocked] = insert_list(2, :user)
@@ -1297,175 +1070,6 @@ test "creates a block activity" do
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
end
-
- test "reverts unblock activity on error" do
- [blocker, blocked] = insert_list(2, :user)
- {:ok, block_activity} = ActivityPub.block(blocker, blocked)
-
- with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
- assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
- end
-
- assert block_activity.data["type"] == "Block"
- assert block_activity.data["actor"] == blocker.ap_id
-
- assert Repo.aggregate(Activity, :count, :id) == 1
- assert Repo.aggregate(Object, :count, :id) == 1
- end
-
- test "creates an undo activity for the last block" do
- blocker = insert(:user)
- blocked = insert(:user)
-
- {:ok, block_activity} = ActivityPub.block(blocker, blocked)
- {:ok, activity} = ActivityPub.unblock(blocker, blocked)
-
- assert activity.data["type"] == "Undo"
- assert activity.data["actor"] == blocker.ap_id
-
- embedded_object = activity.data["object"]
- assert is_map(embedded_object)
- assert embedded_object["type"] == "Block"
- assert embedded_object["object"] == blocked.ap_id
- assert embedded_object["id"] == block_activity.data["id"]
- end
- end
-
- describe "deletion" do
- setup do: clear_config([:instance, :rewrite_policy])
-
- test "it reverts deletion on error" do
- note = insert(:note_activity)
- object = Object.normalize(note)
-
- with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
- assert {:error, :reverted} = ActivityPub.delete(object)
- end
-
- assert Repo.aggregate(Activity, :count, :id) == 1
- assert Repo.get(Object, object.id) == object
- assert Activity.get_by_id(note.id) == note
- end
-
- test "it creates a delete activity and deletes the original object" do
- note = insert(:note_activity)
- object = Object.normalize(note)
- {:ok, delete} = ActivityPub.delete(object)
-
- assert delete.data["type"] == "Delete"
- assert delete.data["actor"] == note.data["actor"]
- assert delete.data["object"] == object.data["id"]
-
- assert Activity.get_by_id(delete.id) != nil
-
- assert Repo.get(Object, object.id).data["type"] == "Tombstone"
- end
-
- test "it doesn't fail when an activity was already deleted" do
- {:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete()
-
- assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete()
- end
-
- test "decrements user note count only for public activities" do
- user = insert(:user, note_count: 10)
-
- {:ok, a1} =
- CommonAPI.post(User.get_cached_by_id(user.id), %{
- "status" => "yeah",
- "visibility" => "public"
- })
-
- {:ok, a2} =
- CommonAPI.post(User.get_cached_by_id(user.id), %{
- "status" => "yeah",
- "visibility" => "unlisted"
- })
-
- {:ok, a3} =
- CommonAPI.post(User.get_cached_by_id(user.id), %{
- "status" => "yeah",
- "visibility" => "private"
- })
-
- {:ok, a4} =
- CommonAPI.post(User.get_cached_by_id(user.id), %{
- "status" => "yeah",
- "visibility" => "direct"
- })
-
- {:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
- {:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
- {:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
- {:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
-
- user = User.get_cached_by_id(user.id)
- assert user.note_count == 10
- end
-
- test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
- user = insert(:user)
- note = insert(:note_activity)
- object = Object.normalize(note)
-
- {:ok, object} =
- object
- |> Object.change(%{
- data: %{
- "actor" => object.data["actor"],
- "id" => object.data["id"],
- "to" => [user.ap_id],
- "type" => "Note"
- }
- })
- |> Object.update_and_set_cache()
-
- {:ok, delete} = ActivityPub.delete(object)
-
- assert user.ap_id in delete.data["to"]
- end
-
- test "decreases reply count" do
- user = insert(:user)
- user2 = insert(:user)
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
- reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
- ap_id = activity.data["id"]
-
- {:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
- {:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
- {:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
- {:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
-
- _ = CommonAPI.delete(direct_reply.id, user2)
- assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
- assert object.data["repliesCount"] == 2
-
- _ = CommonAPI.delete(private_reply.id, user2)
- assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
- assert object.data["repliesCount"] == 2
-
- _ = CommonAPI.delete(public_reply.id, user2)
- assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
- assert object.data["repliesCount"] == 1
-
- _ = CommonAPI.delete(unlisted_reply.id, user2)
- assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
- assert object.data["repliesCount"] == 0
- end
-
- test "it passes delete activity through MRF before deleting the object" do
- Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy)
-
- note = insert(:note_activity)
- object = Object.normalize(note)
-
- {:error, {:reject, _}} = ActivityPub.delete(object)
-
- assert Activity.get_by_id(note.id)
- assert Repo.get(Object, object.id).data["type"] == object.data["type"]
- end
end
describe "timeline post-processing" do
@@ -2403,4 +2007,51 @@ defp private_messages(_) do
u3: %{r1: r3_1.id, r2: r3_2.id},
u4: %{r1: r4_1.id}}
end
+
+ describe "maybe_update_follow_information/1" do
+ setup do
+ clear_config([:instance, :external_user_synchronization], true)
+
+ user = %{
+ local: false,
+ ap_id: "https://gensokyo.2hu/users/raymoo",
+ following_address: "https://gensokyo.2hu/users/following",
+ follower_address: "https://gensokyo.2hu/users/followers",
+ type: "Person"
+ }
+
+ %{user: user}
+ end
+
+ test "logs an error when it can't fetch the info", %{user: user} do
+ assert capture_log(fn ->
+ ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+
+ test "just returns the input if the user type is Application", %{
+ user: user
+ } do
+ user =
+ user
+ |> Map.put(:type, "Application")
+
+ refute capture_log(fn ->
+ assert ^user = ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+
+ test "it just returns the input if the user has no following/follower addresses", %{
+ user: user
+ } do
+ user =
+ user
+ |> Map.put(:following_address, nil)
+ |> Map.put(:follower_address, nil)
+
+ refute capture_log(fn ->
+ assert ^user = ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+ end
end
diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs
index 3c5c3696e..f382adf3e 100644
--- a/test/web/activity_pub/object_validator_test.exs
+++ b/test/web/activity_pub/object_validator_test.exs
@@ -1,6 +1,8 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
use Pleroma.DataCase
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.Utils
@@ -8,6 +10,178 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
import Pleroma.Factory
+ describe "EmojiReacts" do
+ setup do
+ user = insert(:user)
+ {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
+
+ object = Pleroma.Object.get_by_ap_id(post_activity.data["object"])
+
+ {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌")
+
+ %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react}
+ end
+
+ test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do
+ assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, [])
+ end
+
+ test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do
+ without_content =
+ valid_emoji_react
+ |> Map.delete("content")
+
+ {:error, cng} = ObjectValidator.validate(without_content, [])
+
+ refute cng.valid?
+ assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
+ end
+
+ test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
+ without_emoji_content =
+ valid_emoji_react
+ |> Map.put("content", "x")
+
+ {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
+
+ refute cng.valid?
+
+ assert {:content, {"must be a single character emoji", []}} in cng.errors
+ end
+ end
+
+ describe "Undos" do
+ setup do
+ user = insert(:user)
+ {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
+ {:ok, like} = CommonAPI.favorite(user, post_activity.id)
+ {:ok, valid_like_undo, []} = Builder.undo(user, like)
+
+ %{user: user, like: like, valid_like_undo: valid_like_undo}
+ end
+
+ test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do
+ assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, [])
+ end
+
+ test "it does not validate if the actor of the undo is not the actor of the object", %{
+ valid_like_undo: valid_like_undo
+ } do
+ other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
+
+ bad_actor =
+ valid_like_undo
+ |> Map.put("actor", other_user.ap_id)
+
+ {:error, cng} = ObjectValidator.validate(bad_actor, [])
+
+ assert {:actor, {"not the same as object actor", []}} in cng.errors
+ end
+
+ test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do
+ missing_object =
+ valid_like_undo
+ |> Map.put("object", "https://gensokyo.2hu/objects/1")
+
+ {:error, cng} = ObjectValidator.validate(missing_object, [])
+
+ assert {:object, {"can't find object", []}} in cng.errors
+ assert length(cng.errors) == 1
+ end
+ end
+
+ describe "deletes" do
+ setup do
+ user = insert(:user)
+ {:ok, post_activity} = CommonAPI.post(user, %{"status" => "cancel me daddy"})
+
+ {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"])
+ {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id)
+
+ %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete}
+ end
+
+ test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do
+ {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, [])
+
+ assert valid_post_delete["deleted_activity_id"]
+ end
+
+ test "it is invalid if the object isn't in a list of certain types", %{
+ valid_post_delete: valid_post_delete
+ } do
+ object = Object.get_by_ap_id(valid_post_delete["object"])
+
+ data =
+ object.data
+ |> Map.put("type", "Like")
+
+ {:ok, _object} =
+ object
+ |> Ecto.Changeset.change(%{data: data})
+ |> Object.update_and_set_cache()
+
+ {:error, cng} = ObjectValidator.validate(valid_post_delete, [])
+ assert {:object, {"object not in allowed types", []}} in cng.errors
+ end
+
+ test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do
+ assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, []))
+ end
+
+ test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do
+ no_id =
+ valid_post_delete
+ |> Map.delete("id")
+
+ {:error, cng} = ObjectValidator.validate(no_id, [])
+
+ assert {:id, {"can't be blank", [validation: :required]}} in cng.errors
+ end
+
+ test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do
+ missing_object =
+ valid_post_delete
+ |> Map.put("object", "http://does.not/exist")
+
+ {:error, cng} = ObjectValidator.validate(missing_object, [])
+
+ assert {:object, {"can't find object", []}} in cng.errors
+ end
+
+ test "it's invalid if the actor of the object and the actor of delete are from different domains",
+ %{valid_post_delete: valid_post_delete} do
+ valid_user = insert(:user)
+
+ valid_other_actor =
+ valid_post_delete
+ |> Map.put("actor", valid_user.ap_id)
+
+ assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, []))
+
+ invalid_other_actor =
+ valid_post_delete
+ |> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
+
+ {:error, cng} = ObjectValidator.validate(invalid_other_actor, [])
+
+ assert {:actor, {"is not allowed to delete object", []}} in cng.errors
+ end
+
+ test "it's valid if the actor of the object is a local superuser",
+ %{valid_post_delete: valid_post_delete} do
+ user =
+ insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo")
+
+ valid_other_actor =
+ valid_post_delete
+ |> Map.put("actor", user.ap_id)
+
+ {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, [])
+ assert meta[:do_not_federate]
+ end
+ end
+
describe "likes" do
setup do
user = insert(:user)
@@ -36,6 +210,32 @@ test "is valid for a valid object", %{valid_like: valid_like} do
assert LikeValidator.cast_and_validate(valid_like).valid?
end
+ test "sets the 'to' field to the object actor if no recipients are given", %{
+ valid_like: valid_like,
+ user: user
+ } do
+ without_recipients =
+ valid_like
+ |> Map.delete("to")
+
+ {:ok, object, _meta} = ObjectValidator.validate(without_recipients, [])
+
+ assert object["to"] == [user.ap_id]
+ end
+
+ test "sets the context field to the context of the object if no context is given", %{
+ valid_like: valid_like,
+ post_activity: post_activity
+ } do
+ without_context =
+ valid_like
+ |> Map.delete("context")
+
+ {:ok, object, _meta} = ObjectValidator.validate(without_context, [])
+
+ assert object["context"] == post_activity.data["context"]
+ end
+
test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
without_actor = Map.delete(valid_like, "actor")
diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs
new file mode 100644
index 000000000..f278f039b
--- /dev/null
+++ b/test/web/activity_pub/object_validators/types/recipients_test.exs
@@ -0,0 +1,27 @@
+defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients
+ use Pleroma.DataCase
+
+ test "it asserts that all elements of the list are object ids" do
+ list = ["https://lain.com/users/lain", "invalid"]
+
+ assert :error == Recipients.cast(list)
+ end
+
+ test "it works with a list" do
+ list = ["https://lain.com/users/lain"]
+ assert {:ok, list} == Recipients.cast(list)
+ end
+
+ test "it works with a list with whole objects" do
+ list = ["https://lain.com/users/lain", %{"id" => "https://gensokyo.2hu/users/raymoo"}]
+ resulting_list = ["https://gensokyo.2hu/users/raymoo", "https://lain.com/users/lain"]
+ assert {:ok, resulting_list} == Recipients.cast(list)
+ end
+
+ test "it turns a single string into a list" do
+ recipient = "https://lain.com/users/lain"
+
+ assert {:ok, [recipient]} == Recipients.cast(recipient)
+ end
+end
diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs
index 0b6b55156..b29a7a7be 100644
--- a/test/web/activity_pub/side_effects_test.exs
+++ b/test/web/activity_pub/side_effects_test.exs
@@ -3,17 +3,201 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
+ use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
+ alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
+ import Mock
+
+ describe "delete objects" do
+ setup do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, op} = CommonAPI.post(other_user, %{"status" => "big oof"})
+ {:ok, post} = CommonAPI.post(user, %{"status" => "hey", "in_reply_to_id" => op})
+ object = Object.normalize(post)
+ {:ok, delete_data, _meta} = Builder.delete(user, object.data["id"])
+ {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id)
+ {:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true)
+ {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true)
+ %{user: user, delete: delete, post: post, object: object, delete_user: delete_user, op: op}
+ end
+
+ test "it handles object deletions", %{
+ delete: delete,
+ post: post,
+ object: object,
+ user: user,
+ op: op
+ } do
+ with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough],
+ stream_out: fn _ -> nil end,
+ stream_out_participations: fn _, _ -> nil end do
+ {:ok, delete, _} = SideEffects.handle(delete)
+ user = User.get_cached_by_ap_id(object.data["actor"])
+
+ assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete))
+ assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user))
+ end
+
+ object = Object.get_by_id(object.id)
+ assert object.data["type"] == "Tombstone"
+ refute Activity.get_by_id(post.id)
+
+ user = User.get_by_id(user.id)
+ assert user.note_count == 0
+
+ object = Object.normalize(op.data["object"], false)
+
+ assert object.data["repliesCount"] == 0
+ end
+
+ test "it handles user deletions", %{delete_user: delete, user: user} do
+ {:ok, _delete, _} = SideEffects.handle(delete)
+ ObanHelpers.perform_all()
+
+ assert User.get_cached_by_ap_id(user.ap_id).deactivated
+ end
+ end
+
+ describe "EmojiReact objects" do
+ setup do
+ poster = insert(:user)
+ user = insert(:user)
+
+ {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
+
+ {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌")
+ {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true)
+
+ %{emoji_react: emoji_react, user: user, poster: poster}
+ end
+
+ test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do
+ {:ok, emoji_react, _} = SideEffects.handle(emoji_react)
+ object = Object.get_by_ap_id(emoji_react.data["object"])
+
+ assert object.data["reaction_count"] == 1
+ assert ["👌", [user.ap_id]] in object.data["reactions"]
+ end
+
+ test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
+ {:ok, emoji_react, _} = SideEffects.handle(emoji_react)
+ assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id)
+ end
+ end
+
+ describe "Undo objects" do
+ setup do
+ poster = insert(:user)
+ user = insert(:user)
+ {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
+ {:ok, like} = CommonAPI.favorite(user, post.id)
+ {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍")
+ {:ok, announce, _} = CommonAPI.repeat(post.id, user)
+ {:ok, block} = ActivityPub.block(user, poster)
+ User.block(user, poster)
+
+ {:ok, undo_data, _meta} = Builder.undo(user, like)
+ {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true)
+
+ {:ok, undo_data, _meta} = Builder.undo(user, reaction)
+ {:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true)
+
+ {:ok, undo_data, _meta} = Builder.undo(user, announce)
+ {:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true)
+
+ {:ok, undo_data, _meta} = Builder.undo(user, block)
+ {:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true)
+
+ %{
+ like_undo: like_undo,
+ post: post,
+ like: like,
+ reaction_undo: reaction_undo,
+ reaction: reaction,
+ announce_undo: announce_undo,
+ announce: announce,
+ block_undo: block_undo,
+ block: block,
+ poster: poster,
+ user: user
+ }
+ end
+
+ test "deletes the original block", %{block_undo: block_undo, block: block} do
+ {:ok, _block_undo, _} = SideEffects.handle(block_undo)
+ refute Activity.get_by_id(block.id)
+ end
+
+ test "unblocks the blocked user", %{block_undo: block_undo, block: block} do
+ blocker = User.get_by_ap_id(block.data["actor"])
+ blocked = User.get_by_ap_id(block.data["object"])
+
+ {:ok, _block_undo, _} = SideEffects.handle(block_undo)
+ refute User.blocks?(blocker, blocked)
+ end
+
+ test "an announce undo removes the announce from the object", %{
+ announce_undo: announce_undo,
+ post: post
+ } do
+ {:ok, _announce_undo, _} = SideEffects.handle(announce_undo)
+
+ object = Object.get_by_ap_id(post.data["object"])
+
+ assert object.data["announcement_count"] == 0
+ assert object.data["announcements"] == []
+ end
+
+ test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do
+ {:ok, _announce_undo, _} = SideEffects.handle(announce_undo)
+ refute Activity.get_by_id(announce.id)
+ end
+
+ test "a reaction undo removes the reaction from the object", %{
+ reaction_undo: reaction_undo,
+ post: post
+ } do
+ {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo)
+
+ object = Object.get_by_ap_id(post.data["object"])
+
+ assert object.data["reaction_count"] == 0
+ assert object.data["reactions"] == []
+ end
+
+ test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do
+ {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo)
+ refute Activity.get_by_id(reaction.id)
+ end
+
+ test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do
+ {:ok, _like_undo, _} = SideEffects.handle(like_undo)
+
+ object = Object.get_by_ap_id(post.data["object"])
+
+ assert object.data["like_count"] == 0
+ assert object.data["likes"] == []
+ end
+
+ test "deletes the original like", %{like_undo: like_undo, like: like} do
+ {:ok, _like_undo, _} = SideEffects.handle(like_undo)
+ refute Activity.get_by_id(like.id)
+ end
+ end
describe "like objects" do
setup do
diff --git a/test/web/activity_pub/transmogrifier/delete_handling_test.exs b/test/web/activity_pub/transmogrifier/delete_handling_test.exs
new file mode 100644
index 000000000..f235a8e63
--- /dev/null
+++ b/test/web/activity_pub/transmogrifier/delete_handling_test.exs
@@ -0,0 +1,86 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.DeleteHandlingTest do
+ use Oban.Testing, repo: Pleroma.Repo
+ use Pleroma.DataCase
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+
+ import Pleroma.Factory
+
+ setup_all do
+ Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ test "it works for incoming deletes" do
+ activity = insert(:note_activity)
+ deleting_user = insert(:user)
+
+ data =
+ File.read!("test/fixtures/mastodon-delete.json")
+ |> Poison.decode!()
+ |> Map.put("actor", deleting_user.ap_id)
+ |> put_in(["object", "id"], activity.data["object"])
+
+ {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
+ Transmogrifier.handle_incoming(data)
+
+ assert id == data["id"]
+
+ # We delete the Create activity because we base our timelines on it.
+ # This should be changed after we unify objects and activities
+ refute Activity.get_by_id(activity.id)
+ assert actor == deleting_user.ap_id
+
+ # Objects are replaced by a tombstone object.
+ object = Object.normalize(activity.data["object"])
+ assert object.data["type"] == "Tombstone"
+ end
+
+ test "it fails for incoming deletes with spoofed origin" do
+ activity = insert(:note_activity)
+ %{ap_id: ap_id} = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
+
+ data =
+ File.read!("test/fixtures/mastodon-delete.json")
+ |> Poison.decode!()
+ |> Map.put("actor", ap_id)
+ |> put_in(["object", "id"], activity.data["object"])
+
+ assert match?({:error, _}, Transmogrifier.handle_incoming(data))
+ end
+
+ @tag capture_log: true
+ test "it works for incoming user deletes" do
+ %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
+
+ data =
+ File.read!("test/fixtures/mastodon-delete-user.json")
+ |> Poison.decode!()
+
+ {:ok, _} = Transmogrifier.handle_incoming(data)
+ ObanHelpers.perform_all()
+
+ assert User.get_cached_by_ap_id(ap_id).deactivated
+ end
+
+ test "it fails for incoming user deletes with spoofed origin" do
+ %{ap_id: ap_id} = insert(:user)
+
+ data =
+ File.read!("test/fixtures/mastodon-delete-user.json")
+ |> Poison.decode!()
+ |> Map.put("actor", ap_id)
+
+ assert match?({:error, _}, Transmogrifier.handle_incoming(data))
+
+ assert User.get_cached_by_ap_id(ap_id)
+ end
+end
diff --git a/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs
new file mode 100644
index 000000000..6988e3e0a
--- /dev/null
+++ b/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs
@@ -0,0 +1,61 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ test "it works for incoming emoji reactions" do
+ user = insert(:user)
+ other_user = insert(:user, local: false)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/emoji-reaction.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+ |> Map.put("actor", other_user.ap_id)
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == other_user.ap_id
+ assert data["type"] == "EmojiReact"
+ assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
+ assert data["object"] == activity.data["object"]
+ assert data["content"] == "👌"
+
+ object = Object.get_by_ap_id(data["object"])
+
+ assert object.data["reaction_count"] == 1
+ assert match?([["👌", _]], object.data["reactions"])
+ end
+
+ test "it reject invalid emoji reactions" do
+ user = insert(:user)
+ other_user = insert(:user, local: false)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/emoji-reaction-too-long.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+ |> Map.put("actor", other_user.ap_id)
+
+ assert {:error, _} = Transmogrifier.handle_incoming(data)
+
+ data =
+ File.read!("test/fixtures/emoji-reaction-no-emoji.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+ |> Map.put("actor", other_user.ap_id)
+
+ assert {:error, _} = Transmogrifier.handle_incoming(data)
+ end
+end
diff --git a/test/web/activity_pub/transmogrifier/like_handling_test.exs b/test/web/activity_pub/transmogrifier/like_handling_test.exs
new file mode 100644
index 000000000..54a5c1dbc
--- /dev/null
+++ b/test/web/activity_pub/transmogrifier/like_handling_test.exs
@@ -0,0 +1,78 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Activity
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ test "it works for incoming likes" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/mastodon-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+ refute Enum.empty?(activity.recipients)
+
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+ assert data["type"] == "Like"
+ assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
+ assert data["object"] == activity.data["object"]
+ end
+
+ test "it works for incoming misskey likes, turning them into EmojiReacts" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/misskey-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert activity_data["actor"] == data["actor"]
+ assert activity_data["type"] == "EmojiReact"
+ assert activity_data["id"] == data["id"]
+ assert activity_data["object"] == activity.data["object"]
+ assert activity_data["content"] == "🍮"
+ end
+
+ test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+ data =
+ File.read!("test/fixtures/misskey-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+ |> Map.put("_misskey_reaction", "⭐")
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert activity_data["actor"] == data["actor"]
+ assert activity_data["type"] == "EmojiReact"
+ assert activity_data["id"] == data["id"]
+ assert activity_data["object"] == activity.data["object"]
+ assert activity_data["content"] == "⭐"
+ end
+end
diff --git a/test/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/web/activity_pub/transmogrifier/undo_handling_test.exs
new file mode 100644
index 000000000..eaf58adf7
--- /dev/null
+++ b/test/web/activity_pub/transmogrifier/undo_handling_test.exs
@@ -0,0 +1,185 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ test "it works for incoming emoji reaction undos" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+ {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌")
+
+ data =
+ File.read!("test/fixtures/mastodon-undo-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", reaction_activity.data["id"])
+ |> Map.put("actor", user.ap_id)
+
+ {:ok, activity} = Transmogrifier.handle_incoming(data)
+
+ assert activity.actor == user.ap_id
+ assert activity.data["id"] == data["id"]
+ assert activity.data["type"] == "Undo"
+ end
+
+ test "it returns an error for incoming unlikes wihout a like activity" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
+
+ data =
+ File.read!("test/fixtures/mastodon-undo-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ assert Transmogrifier.handle_incoming(data) == :error
+ end
+
+ test "it works for incoming unlikes with an existing like activity" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
+
+ like_data =
+ File.read!("test/fixtures/mastodon-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _liker = insert(:user, ap_id: like_data["actor"], local: false)
+
+ {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-undo-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", like_data)
+ |> Map.put("actor", like_data["actor"])
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+ assert data["type"] == "Undo"
+ assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
+ assert data["object"] == "http://mastodon.example.org/users/admin#likes/2"
+
+ note = Object.get_by_ap_id(like_data["object"])
+ assert note.data["like_count"] == 0
+ assert note.data["likes"] == []
+ end
+
+ test "it works for incoming unlikes with an existing like activity and a compact object" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
+
+ like_data =
+ File.read!("test/fixtures/mastodon-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _liker = insert(:user, ap_id: like_data["actor"], local: false)
+
+ {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-undo-like.json")
+ |> Poison.decode!()
+ |> Map.put("object", like_data["id"])
+ |> Map.put("actor", like_data["actor"])
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+ assert data["type"] == "Undo"
+ assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
+ assert data["object"] == "http://mastodon.example.org/users/admin#likes/2"
+ end
+
+ test "it works for incoming unannounces with an existing notice" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
+
+ announce_data =
+ File.read!("test/fixtures/mastodon-announce.json")
+ |> Poison.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _announcer = insert(:user, ap_id: announce_data["actor"], local: false)
+
+ {:ok, %Activity{data: announce_data, local: false}} =
+ Transmogrifier.handle_incoming(announce_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-undo-announce.json")
+ |> Poison.decode!()
+ |> Map.put("object", announce_data)
+ |> Map.put("actor", announce_data["actor"])
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["type"] == "Undo"
+
+ assert data["object"] ==
+ "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
+ end
+
+ test "it works for incomming unfollows with an existing follow" do
+ user = insert(:user)
+
+ follow_data =
+ File.read!("test/fixtures/mastodon-follow-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", user.ap_id)
+
+ _follower = insert(:user, ap_id: follow_data["actor"], local: false)
+
+ {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-unfollow-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", follow_data)
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["type"] == "Undo"
+ assert data["object"]["type"] == "Follow"
+ assert data["object"]["object"] == user.ap_id
+ assert data["actor"] == "http://mastodon.example.org/users/admin"
+
+ refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
+ end
+
+ test "it works for incoming unblocks with an existing block" do
+ user = insert(:user)
+
+ block_data =
+ File.read!("test/fixtures/mastodon-block-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", user.ap_id)
+
+ _blocker = insert(:user, ap_id: block_data["actor"], local: false)
+
+ {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-unblock-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", block_data)
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+ assert data["type"] == "Undo"
+ assert data["object"] == block_data["id"]
+
+ blocker = User.get_cached_by_ap_id(data["actor"])
+
+ refute User.blocks?(blocker, user)
+ end
+end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 6057e360a..2914c90ea 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -325,180 +325,6 @@ test "it cleans up incoming notices which are not really DMs" do
assert object_data["cc"] == to
end
- test "it works for incoming likes" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
-
- data =
- File.read!("test/fixtures/mastodon-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
-
- refute Enum.empty?(activity.recipients)
-
- assert data["actor"] == "http://mastodon.example.org/users/admin"
- assert data["type"] == "Like"
- assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
- assert data["object"] == activity.data["object"]
- end
-
- test "it works for incoming misskey likes, turning them into EmojiReacts" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
-
- data =
- File.read!("test/fixtures/misskey-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- assert data["actor"] == data["actor"]
- assert data["type"] == "EmojiReact"
- assert data["id"] == data["id"]
- assert data["object"] == activity.data["object"]
- assert data["content"] == "🍮"
- end
-
- test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
-
- data =
- File.read!("test/fixtures/misskey-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
- |> Map.put("_misskey_reaction", "⭐")
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- assert data["actor"] == data["actor"]
- assert data["type"] == "EmojiReact"
- assert data["id"] == data["id"]
- assert data["object"] == activity.data["object"]
- assert data["content"] == "⭐"
- end
-
- test "it works for incoming emoji reactions" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
-
- data =
- File.read!("test/fixtures/emoji-reaction.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- assert data["actor"] == "http://mastodon.example.org/users/admin"
- assert data["type"] == "EmojiReact"
- assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
- assert data["object"] == activity.data["object"]
- assert data["content"] == "👌"
- end
-
- test "it reject invalid emoji reactions" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
-
- data =
- File.read!("test/fixtures/emoji-reaction-too-long.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- assert :error = Transmogrifier.handle_incoming(data)
-
- data =
- File.read!("test/fixtures/emoji-reaction-no-emoji.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- assert :error = Transmogrifier.handle_incoming(data)
- end
-
- test "it works for incoming emoji reaction undos" do
- user = insert(:user)
-
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
- {:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
-
- data =
- File.read!("test/fixtures/mastodon-undo-like.json")
- |> Poison.decode!()
- |> Map.put("object", reaction_activity.data["id"])
- |> Map.put("actor", user.ap_id)
-
- {:ok, activity} = Transmogrifier.handle_incoming(data)
-
- assert activity.actor == user.ap_id
- assert activity.data["id"] == data["id"]
- assert activity.data["type"] == "Undo"
- end
-
- test "it returns an error for incoming unlikes wihout a like activity" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
-
- data =
- File.read!("test/fixtures/mastodon-undo-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- assert Transmogrifier.handle_incoming(data) == :error
- end
-
- test "it works for incoming unlikes with an existing like activity" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
-
- like_data =
- File.read!("test/fixtures/mastodon-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
-
- data =
- File.read!("test/fixtures/mastodon-undo-like.json")
- |> Poison.decode!()
- |> Map.put("object", like_data)
- |> Map.put("actor", like_data["actor"])
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- assert data["actor"] == "http://mastodon.example.org/users/admin"
- assert data["type"] == "Undo"
- assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
- assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
- end
-
- test "it works for incoming unlikes with an existing like activity and a compact object" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
-
- like_data =
- File.read!("test/fixtures/mastodon-like.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
-
- data =
- File.read!("test/fixtures/mastodon-undo-like.json")
- |> Poison.decode!()
- |> Map.put("object", like_data["id"])
- |> Map.put("actor", like_data["actor"])
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- assert data["actor"] == "http://mastodon.example.org/users/admin"
- assert data["type"] == "Undo"
- assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
- assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
- end
-
test "it works for incoming announces" do
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
@@ -655,7 +481,7 @@ test "it strips internal likes" do
test "it strips internal reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
- {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
%{object: object} = Activity.get_by_id_with_object(activity.id)
assert Map.has_key?(object.data, "reactions")
@@ -822,112 +648,6 @@ test "it works for incoming update activities which lock the account" do
assert user.locked == true
end
- test "it works for incoming deletes" do
- activity = insert(:note_activity)
- deleting_user = insert(:user)
-
- data =
- File.read!("test/fixtures/mastodon-delete.json")
- |> Poison.decode!()
-
- object =
- data["object"]
- |> Map.put("id", activity.data["object"])
-
- data =
- data
- |> Map.put("object", object)
- |> Map.put("actor", deleting_user.ap_id)
-
- {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
- Transmogrifier.handle_incoming(data)
-
- assert id == data["id"]
- refute Activity.get_by_id(activity.id)
- assert actor == deleting_user.ap_id
- end
-
- test "it fails for incoming deletes with spoofed origin" do
- activity = insert(:note_activity)
-
- data =
- File.read!("test/fixtures/mastodon-delete.json")
- |> Poison.decode!()
-
- object =
- data["object"]
- |> Map.put("id", activity.data["object"])
-
- data =
- data
- |> Map.put("object", object)
-
- assert capture_log(fn ->
- :error = Transmogrifier.handle_incoming(data)
- end) =~
- "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
-
- assert Activity.get_by_id(activity.id)
- end
-
- @tag capture_log: true
- test "it works for incoming user deletes" do
- %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
-
- data =
- File.read!("test/fixtures/mastodon-delete-user.json")
- |> Poison.decode!()
-
- {:ok, _} = Transmogrifier.handle_incoming(data)
- ObanHelpers.perform_all()
-
- refute User.get_cached_by_ap_id(ap_id)
- end
-
- test "it fails for incoming user deletes with spoofed origin" do
- %{ap_id: ap_id} = insert(:user)
-
- data =
- File.read!("test/fixtures/mastodon-delete-user.json")
- |> Poison.decode!()
- |> Map.put("actor", ap_id)
-
- assert capture_log(fn ->
- assert :error == Transmogrifier.handle_incoming(data)
- end) =~ "Object containment failed"
-
- assert User.get_cached_by_ap_id(ap_id)
- end
-
- test "it works for incoming unannounces with an existing notice" do
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
-
- announce_data =
- File.read!("test/fixtures/mastodon-announce.json")
- |> Poison.decode!()
- |> Map.put("object", activity.data["object"])
-
- {:ok, %Activity{data: announce_data, local: false}} =
- Transmogrifier.handle_incoming(announce_data)
-
- data =
- File.read!("test/fixtures/mastodon-undo-announce.json")
- |> Poison.decode!()
- |> Map.put("object", announce_data)
- |> Map.put("actor", announce_data["actor"])
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-
- assert data["type"] == "Undo"
- assert object_data = data["object"]
- assert object_data["type"] == "Announce"
- assert object_data["object"] == activity.data["object"]
-
- assert object_data["id"] ==
- "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
- end
-
test "it works for incomming unfollows with an existing follow" do
user = insert(:user)
@@ -1022,32 +742,6 @@ test "incoming blocks successfully tear down any follow relationship" do
refute User.following?(blocked, blocker)
end
- test "it works for incoming unblocks with an existing block" do
- user = insert(:user)
-
- block_data =
- File.read!("test/fixtures/mastodon-block-activity.json")
- |> Poison.decode!()
- |> Map.put("object", user.ap_id)
-
- {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
-
- data =
- File.read!("test/fixtures/mastodon-unblock-activity.json")
- |> Poison.decode!()
- |> Map.put("object", block_data)
-
- {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
- assert data["type"] == "Undo"
- assert data["object"]["type"] == "Block"
- assert data["object"]["object"] == user.ap_id
- assert data["actor"] == "http://mastodon.example.org/users/admin"
-
- blocker = User.get_cached_by_ap_id(data["actor"])
-
- refute User.blocks?(blocker, user)
- end
-
test "it works for incoming accepts which were pre-accepted" do
follower = insert(:user)
followed = insert(:user)
@@ -1221,6 +915,35 @@ test "it rejects activities without a valid ID" do
:error = Transmogrifier.handle_incoming(data)
end
+ test "skip converting the content when it is nil" do
+ object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe"
+
+ {:ok, object} = Fetcher.fetch_and_contain_remote_object_from_id(object_id)
+
+ result =
+ Pleroma.Web.ActivityPub.Transmogrifier.fix_object(Map.merge(object, %{"content" => nil}))
+
+ assert result["content"] == nil
+ end
+
+ test "it converts content of object to html" do
+ object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe"
+
+ {:ok, %{"content" => content_markdown}} =
+ Fetcher.fetch_and_contain_remote_object_from_id(object_id)
+
+ {:ok, %Pleroma.Object{data: %{"content" => content}} = object} =
+ Fetcher.fetch_object_from_id(object_id)
+
+ assert content_markdown ==
+ "Support this and our other Michigan!/usr/group videos and meetings. Learn more at http://mug.org/membership\n\nTwenty Years in Jail: FreeBSD's Jails, Then and Now\n\nJails started as a limited virtualization system, but over the last two years they've..."
+
+ assert content ==
+ "
Support this and our other Michigan!/usr/group videos and meetings. Learn more at http://mug.org/membership
Twenty Years in Jail: FreeBSD’s Jails, Then and Now
Jails started as a limited virtualization system, but over the last two years they’ve…
"
+
+ assert object.data["mediaType"] == "text/html"
+ end
+
test "it remaps video URLs as attachments if necessary" do
{:ok, object} =
Fetcher.fetch_object_from_id(
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index b0bfed917..b8d811c73 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -102,34 +102,6 @@ test "works with an object has tags as map" do
end
end
- describe "make_unlike_data/3" do
- test "returns data for unlike activity" do
- user = insert(:user)
- like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"})
-
- object = Object.normalize(like_activity.data["object"])
-
- assert Utils.make_unlike_data(user, like_activity, nil) == %{
- "type" => "Undo",
- "actor" => user.ap_id,
- "object" => like_activity.data,
- "to" => [user.follower_address, object.data["actor"]],
- "cc" => [Pleroma.Constants.as_public()],
- "context" => like_activity.data["context"]
- }
-
- assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{
- "type" => "Undo",
- "actor" => user.ap_id,
- "object" => like_activity.data,
- "to" => [user.follower_address, object.data["actor"]],
- "cc" => [Pleroma.Constants.as_public()],
- "context" => like_activity.data["context"],
- "id" => "9mJEZK0tky1w2xD2vY"
- }
- end
- end
-
describe "make_like_data" do
setup do
user = insert(:user)
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index f80dbf8dd..4697af50e 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -6,19 +6,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
- import Pleroma.Factory
import ExUnit.CaptureLog
+ import Mock
+ import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.HTML
+ alias Pleroma.MFA
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
+ alias Pleroma.Web
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MediaProxy
@@ -146,17 +149,26 @@ test "GET /api/pleroma/admin/users/:nickname requires " <>
test "single user", %{admin: admin, conn: conn} do
user = insert(:user)
- conn =
- conn
- |> put_req_header("accept", "application/json")
- |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
+ with_mock Pleroma.Web.Federator,
+ publish: fn _ -> nil end do
+ conn =
+ conn
+ |> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
- log_entry = Repo.one(ModerationLog)
+ ObanHelpers.perform_all()
- assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} deleted users: @#{user.nickname}"
+ assert User.get_by_nickname(user.nickname).deactivated
- assert json_response(conn, 200) == user.nickname
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deleted users: @#{user.nickname}"
+
+ assert json_response(conn, 200) == [user.nickname]
+
+ assert called(Pleroma.Web.Federator.publish(:_))
+ end
end
test "multiple users", %{admin: admin, conn: conn} do
@@ -737,6 +749,39 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
}
end
+ test "pagination works correctly with service users", %{conn: conn} do
+ service1 = insert(:user, ap_id: Web.base_url() <> "/relay")
+ service2 = insert(:user, ap_id: Web.base_url() <> "/internal/fetch")
+ insert_list(25, :user)
+
+ assert %{"count" => 26, "page_size" => 10, "users" => users1} =
+ conn
+ |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"})
+ |> json_response(200)
+
+ assert Enum.count(users1) == 10
+ assert service1 not in [users1]
+ assert service2 not in [users1]
+
+ assert %{"count" => 26, "page_size" => 10, "users" => users2} =
+ conn
+ |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"})
+ |> json_response(200)
+
+ assert Enum.count(users2) == 10
+ assert service1 not in [users2]
+ assert service2 not in [users2]
+
+ assert %{"count" => 26, "page_size" => 10, "users" => users3} =
+ conn
+ |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"})
+ |> json_response(200)
+
+ assert Enum.count(users3) == 6
+ assert service1 not in [users3]
+ assert service2 not in [users3]
+ end
+
test "renders empty array for the second page", %{conn: conn} do
insert(:user)
@@ -1234,6 +1279,38 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admi
"@#{admin.nickname} deactivated users: @#{user.nickname}"
end
+ describe "PUT disable_mfa" do
+ test "returns 200 and disable 2fa", %{conn: conn} do
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true}
+ }
+ )
+
+ response =
+ conn
+ |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: user.nickname})
+ |> json_response(200)
+
+ assert response == user.nickname
+ mfa_settings = refresh_record(user).multi_factor_authentication_settings
+
+ refute mfa_settings.enabled
+ refute mfa_settings.totp.confirmed
+ end
+
+ test "returns 404 if user not found", %{conn: conn} do
+ response =
+ conn
+ |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"})
+ |> json_response(404)
+
+ assert response == "Not found"
+ end
+ end
+
describe "POST /api/pleroma/admin/users/invite_token" do
test "without options", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token")
@@ -1347,9 +1424,9 @@ test "returns report by its id", %{conn: conn} do
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id]
})
response =
@@ -1374,16 +1451,16 @@ test "returns 404 when report id is invalid", %{conn: conn} do
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel very offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel very offended",
+ status_ids: [activity.id]
})
%{
@@ -1523,9 +1600,9 @@ test "returns reports", %{conn: conn} do
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id]
})
response =
@@ -1547,15 +1624,15 @@ test "returns reports with specified state", %{conn: conn} do
{:ok, %{id: first_report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I don't like this user"
+ account_id: target_user.id,
+ comment: "I don't like this user"
})
CommonAPI.update_report_state(second_report_id, "closed")
@@ -1620,6 +1697,25 @@ test "returns 403 when requested by anonymous" do
end
end
+ describe "GET /api/pleroma/admin/statuses/:id" do
+ test "not found", %{conn: conn} do
+ assert conn
+ |> get("/api/pleroma/admin/statuses/not_found")
+ |> json_response(:not_found)
+ end
+
+ test "shows activity", %{conn: conn} do
+ activity = insert(:note_activity)
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/statuses/#{activity.id}")
+ |> json_response(200)
+
+ assert response["id"] == activity.id
+ end
+ end
+
describe "PUT /api/pleroma/admin/statuses/:id" do
setup do
activity = insert(:note_activity)
@@ -3431,9 +3527,9 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id]
})
post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
@@ -3526,7 +3622,7 @@ test "errors", %{conn: conn} do
end
test "success", %{conn: conn} do
- base_url = Pleroma.Web.base_url()
+ base_url = Web.base_url()
app_name = "Trusted app"
response =
@@ -3547,7 +3643,7 @@ test "success", %{conn: conn} do
end
test "with trusted", %{conn: conn} do
- base_url = Pleroma.Web.base_url()
+ base_url = Web.base_url()
app_name = "Trusted app"
response =
diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs
index 5db6629f2..8cfa1dcfa 100644
--- a/test/web/admin_api/views/report_view_test.exs
+++ b/test/web/admin_api/views/report_view_test.exs
@@ -15,7 +15,7 @@ test "renders a report" do
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id})
+ {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id})
expected = %{
content: nil,
@@ -48,7 +48,7 @@ test "includes reported statuses" do
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "toot"})
{:ok, report_activity} =
- CommonAPI.report(user, %{"account_id" => other_user.id, "status_ids" => [activity.id]})
+ CommonAPI.report(user, %{account_id: other_user.id, status_ids: [activity.id]})
other_user = Pleroma.User.get_by_id(other_user.id)
@@ -81,7 +81,7 @@ test "renders report's state" do
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id})
+ {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id})
{:ok, activity} = CommonAPI.update_report_state(activity.id, "closed")
assert %{state: "closed"} =
@@ -94,8 +94,8 @@ test "renders report description" do
{:ok, activity} =
CommonAPI.report(user, %{
- "account_id" => other_user.id,
- "comment" => "posts are too good for this instance"
+ account_id: other_user.id,
+ comment: "posts are too good for this instance"
})
assert %{content: "posts are too good for this instance"} =
@@ -108,8 +108,8 @@ test "sanitizes report description" do
{:ok, activity} =
CommonAPI.report(user, %{
- "account_id" => other_user.id,
- "comment" => ""
+ account_id: other_user.id,
+ comment: ""
})
data = Map.put(activity.data, "content", "")
@@ -125,8 +125,8 @@ test "doesn't error out when the user doesn't exists" do
{:ok, activity} =
CommonAPI.report(user, %{
- "account_id" => other_user.id,
- "comment" => ""
+ account_id: other_user.id,
+ comment: ""
})
Pleroma.User.delete(other_user)
diff --git a/test/web/auth/pleroma_authenticator_test.exs b/test/web/auth/pleroma_authenticator_test.exs
new file mode 100644
index 000000000..7125c5081
--- /dev/null
+++ b/test/web/auth/pleroma_authenticator_test.exs
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Web.Auth.PleromaAuthenticator
+ import Pleroma.Factory
+
+ setup do
+ password = "testpassword"
+ name = "AgentSmith"
+ user = insert(:user, nickname: name, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
+ {:ok, [user: user, name: name, password: password]}
+ end
+
+ test "get_user/authorization", %{user: user, name: name, password: password} do
+ params = %{"authorization" => %{"name" => name, "password" => password}}
+ res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
+
+ assert {:ok, user} == res
+ end
+
+ test "get_user/authorization with invalid password", %{name: name} do
+ params = %{"authorization" => %{"name" => name, "password" => "password"}}
+ res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
+
+ assert {:error, {:checkpw, false}} == res
+ end
+
+ test "get_user/grant_type_password", %{user: user, name: name, password: password} do
+ params = %{"grant_type" => "password", "username" => name, "password" => password}
+ res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
+
+ assert {:ok, user} == res
+ end
+
+ test "error credintails" do
+ res = PleromaAuthenticator.get_user(%Plug.Conn{params: %{}})
+ assert {:error, :invalid_credentials} == res
+ end
+end
diff --git a/test/web/auth/totp_authenticator_test.exs b/test/web/auth/totp_authenticator_test.exs
new file mode 100644
index 000000000..e08069490
--- /dev/null
+++ b/test/web/auth/totp_authenticator_test.exs
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.TOTPAuthenticatorTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.MFA
+ alias Pleroma.MFA.BackupCodes
+ alias Pleroma.MFA.TOTP
+ alias Pleroma.Web.Auth.TOTPAuthenticator
+
+ import Pleroma.Factory
+
+ test "verify token" do
+ otp_secret = TOTP.generate_secret()
+ otp_token = TOTP.generate_token(otp_secret)
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ assert TOTPAuthenticator.verify(otp_token, user) == {:ok, :pass}
+ assert TOTPAuthenticator.verify(nil, user) == {:error, :invalid_token}
+ assert TOTPAuthenticator.verify("", user) == {:error, :invalid_token}
+ end
+
+ test "checks backup codes" do
+ [code | _] = backup_codes = BackupCodes.generate()
+
+ hashed_codes =
+ backup_codes
+ |> Enum.map(&Comeonin.Pbkdf2.hashpwsalt(&1))
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ backup_codes: hashed_codes,
+ totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true}
+ }
+ )
+
+ assert TOTPAuthenticator.verify_recovery_code(user, code) == {:ok, :pass}
+ refute TOTPAuthenticator.verify_recovery_code(code, refresh_record(user)) == {:ok, :pass}
+ end
+end
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 1758662b0..2fd17a1b8 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -9,11 +9,13 @@ defmodule Pleroma.Web.CommonAPITest do
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
+ import Mock
require Pleroma.Constants
@@ -21,6 +23,84 @@ defmodule Pleroma.Web.CommonAPITest do
setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses])
+ describe "deletion" do
+ test "it allows users to delete their posts" do
+ user = insert(:user)
+
+ {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"})
+
+ with_mock Pleroma.Web.Federator,
+ publish: fn _ -> nil end do
+ assert {:ok, delete} = CommonAPI.delete(post.id, user)
+ assert delete.local
+ assert called(Pleroma.Web.Federator.publish(delete))
+ end
+
+ refute Activity.get_by_id(post.id)
+ end
+
+ test "it does not allow a user to delete their posts" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"})
+
+ assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user)
+ assert Activity.get_by_id(post.id)
+ end
+
+ test "it allows moderators to delete other user's posts" do
+ user = insert(:user)
+ moderator = insert(:user, is_moderator: true)
+
+ {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"})
+
+ assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
+ assert delete.local
+
+ refute Activity.get_by_id(post.id)
+ end
+
+ test "it allows admins to delete other user's posts" do
+ user = insert(:user)
+ moderator = insert(:user, is_admin: true)
+
+ {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"})
+
+ assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
+ assert delete.local
+
+ refute Activity.get_by_id(post.id)
+ end
+
+ test "superusers deleting non-local posts won't federate the delete" do
+ # This is the user of the ingested activity
+ _user =
+ insert(:user,
+ local: false,
+ ap_id: "http://mastodon.example.org/users/admin",
+ last_refreshed_at: NaiveDateTime.utc_now()
+ )
+
+ moderator = insert(:user, is_admin: true)
+
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Jason.decode!()
+
+ {:ok, post} = Transmogrifier.handle_incoming(data)
+
+ with_mock Pleroma.Web.Federator,
+ publish: fn _ -> nil end do
+ assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
+ assert delete.local
+ refute called(Pleroma.Web.Federator.publish(:_))
+ end
+
+ refute Activity.get_by_id(post.id)
+ end
+ end
+
test "favoriting race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
@@ -278,7 +358,7 @@ test "reacting to a status with an emoji" do
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
- {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
+ {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
assert reaction.data["actor"] == user.ap_id
assert reaction.data["content"] == "👍"
@@ -293,12 +373,13 @@ test "unreacting to a status with an emoji" do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
- {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
+ {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
- {:ok, unreaction, _} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
+ {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert unreaction.data["type"] == "Undo"
assert unreaction.data["object"] == reaction.data["id"]
+ assert unreaction.local
end
test "repeating a status" do
@@ -485,9 +566,9 @@ test "creates a report" do
comment = "foobar"
report_data = %{
- "account_id" => target_user.id,
- "comment" => comment,
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: comment,
+ status_ids: [activity.id]
}
note_obj = %{
@@ -517,9 +598,9 @@ test "updates report state" do
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
@@ -538,9 +619,9 @@ test "does not update report state when state is unsupported" do
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id]
})
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
@@ -552,16 +633,16 @@ test "updates state of multiple reports" do
{:ok, %Activity{id: first_report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel offended",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel offended",
+ status_ids: [activity.id]
})
{:ok, %Activity{id: second_report_id}} =
CommonAPI.report(reporter, %{
- "account_id" => target_user.id,
- "comment" => "I feel very offended!",
- "status_ids" => [activity.id]
+ account_id: target_user.id,
+ comment: "I feel very offended!",
+ status_ids: [activity.id]
})
{:ok, report_ids} =
@@ -697,6 +778,14 @@ test "after rejection, it sets all existing pending follow request states to 're
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
+
+ test "doesn't create a following relationship if the corresponding follow request doesn't exist" do
+ user = insert(:user, locked: true)
+ not_follower = insert(:user)
+ CommonAPI.accept_follow_request(not_follower, user)
+
+ assert Pleroma.FollowingRelationship.following?(not_follower, user) == false
+ end
end
describe "vote/3" do
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
index ba70ba66c..256a8b304 100644
--- a/test/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -925,7 +925,8 @@ test "returns bad_request if missing email params when :account_activation_requi
|> Map.put(:remote_ip, {127, 0, 0, 5})
|> post("/api/v1/accounts", Map.delete(valid_params, :email))
- assert json_response_and_validate_schema(res, 400) == %{"error" => "Missing parameters"}
+ assert json_response_and_validate_schema(res, 400) ==
+ %{"error" => "Missing parameter: email"}
res =
conn
@@ -1093,6 +1094,91 @@ test "respects rate limit setting", %{conn: conn} do
end
end
+ describe "create account with enabled captcha" do
+ setup %{conn: conn} do
+ app_token = insert(:oauth_token, user: nil)
+
+ conn =
+ conn
+ |> put_req_header("authorization", "Bearer " <> app_token.token)
+ |> put_req_header("content-type", "multipart/form-data")
+
+ [conn: conn]
+ end
+
+ setup do: clear_config([Pleroma.Captcha, :enabled], true)
+
+ test "creates an account and returns 200 if captcha is valid", %{conn: conn} do
+ %{token: token, answer_data: answer_data} = Pleroma.Captcha.new()
+
+ params = %{
+ username: "lain",
+ email: "lain@example.org",
+ password: "PlzDontHackLain",
+ agreement: true,
+ captcha_solution: Pleroma.Captcha.Mock.solution(),
+ captcha_token: token,
+ captcha_answer_data: answer_data
+ }
+
+ assert %{
+ "access_token" => access_token,
+ "created_at" => _,
+ "scope" => ["read"],
+ "token_type" => "Bearer"
+ } =
+ conn
+ |> post("/api/v1/accounts", params)
+ |> json_response_and_validate_schema(:ok)
+
+ assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user)
+
+ Cachex.del(:used_captcha_cache, token)
+ end
+
+ test "returns 400 if any captcha field is not provided", %{conn: conn} do
+ captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data]
+
+ valid_params = %{
+ username: "lain",
+ email: "lain@example.org",
+ password: "PlzDontHackLain",
+ agreement: true,
+ captcha_solution: "xx",
+ captcha_token: "xx",
+ captcha_answer_data: "xx"
+ }
+
+ for field <- captcha_fields do
+ expected = %{
+ "error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}"
+ }
+
+ assert expected ==
+ conn
+ |> post("/api/v1/accounts", Map.delete(valid_params, field))
+ |> json_response_and_validate_schema(:bad_request)
+ end
+ end
+
+ test "returns an error if captcha is invalid", %{conn: conn} do
+ params = %{
+ username: "lain",
+ email: "lain@example.org",
+ password: "PlzDontHackLain",
+ agreement: true,
+ captcha_solution: "cofe",
+ captcha_token: "cofe",
+ captcha_answer_data: "cofe"
+ }
+
+ assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} ==
+ conn
+ |> post("/api/v1/accounts", params)
+ |> json_response_and_validate_schema(:bad_request)
+ end
+ end
+
describe "GET /api/v1/accounts/:id/lists - account_lists" do
test "returns lists to which the account belongs" do
%{user: user, conn: conn} = oauth_access(["read:lists"])
@@ -1110,12 +1196,15 @@ test "returns lists to which the account belongs" do
describe "verify_credentials" do
test "verify_credentials" do
%{user: user, conn: conn} = oauth_access(["read:accounts"])
+ [notification | _] = insert_list(7, :notification, user: user)
+ Pleroma.Notification.set_read_up_to(user, notification.id)
conn = get(conn, "/api/v1/accounts/verify_credentials")
response = json_response_and_validate_schema(conn, 200)
assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
assert response["pleroma"]["chat_token"]
+ assert response["pleroma"]["unread_notifications_count"] == 6
assert id == to_string(user.id)
end
diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs
index 801b0259b..04695572e 100644
--- a/test/web/mastodon_api/controllers/conversation_controller_test.exs
+++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs
@@ -36,7 +36,7 @@ test "returns a list of conversations", %{user: user_one, conn: conn} do
res_conn = get(conn, "/api/v1/conversations")
- assert response = json_response(res_conn, 200)
+ assert response = json_response_and_validate_schema(res_conn, 200)
assert [
%{
@@ -91,18 +91,18 @@ test "filters conversations by recipients", %{user: user_one, conn: conn} do
"visibility" => "direct"
})
- [conversation1, conversation2] =
- conn
- |> get("/api/v1/conversations", %{"recipients" => [user_two.id]})
- |> json_response(200)
+ assert [conversation1, conversation2] =
+ conn
+ |> get("/api/v1/conversations?recipients[]=#{user_two.id}")
+ |> json_response_and_validate_schema(200)
assert conversation1["last_status"]["id"] == direct5.id
assert conversation2["last_status"]["id"] == direct1.id
[conversation1] =
conn
- |> get("/api/v1/conversations", %{"recipients" => [user_two.id, user_three.id]})
- |> json_response(200)
+ |> get("/api/v1/conversations?recipients[]=#{user_two.id}&recipients[]=#{user_three.id}")
+ |> json_response_and_validate_schema(200)
assert conversation1["last_status"]["id"] == direct3.id
end
@@ -126,7 +126,7 @@ test "updates the last_status on reply", %{user: user_one, conn: conn} do
[%{"last_status" => res_last_status}] =
conn
|> get("/api/v1/conversations")
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
assert res_last_status["id"] == direct_reply.id
end
@@ -154,12 +154,12 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do
[%{"id" => direct_conversation_id, "unread" => true}] =
user_two_conn
|> get("/api/v1/conversations")
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
%{"unread" => false} =
user_two_conn
|> post("/api/v1/conversations/#{direct_conversation_id}/read")
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
@@ -175,7 +175,7 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do
[%{"unread" => true}] =
conn
|> get("/api/v1/conversations")
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs
index 97ab005e0..f29547d13 100644
--- a/test/web/mastodon_api/controllers/filter_controller_test.exs
+++ b/test/web/mastodon_api/controllers/filter_controller_test.exs
@@ -15,9 +15,12 @@ test "creating a filter" do
context: ["home"]
}
- conn = post(conn, "/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
- assert response = json_response(conn, 200)
+ assert response = json_response_and_validate_schema(conn, 200)
assert response["phrase"] == filter.phrase
assert response["context"] == filter.context
assert response["irreversible"] == false
@@ -48,12 +51,12 @@ test "fetching a list of filters" do
response =
conn
|> get("/api/v1/filters")
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
assert response ==
render_json(
FilterView,
- "filters.json",
+ "index.json",
filters: [filter_two, filter_one]
)
end
@@ -72,7 +75,7 @@ test "get a filter" do
conn = get(conn, "/api/v1/filters/#{filter.filter_id}")
- assert _response = json_response(conn, 200)
+ assert response = json_response_and_validate_schema(conn, 200)
end
test "update a filter" do
@@ -82,7 +85,8 @@ test "update a filter" do
user_id: user.id,
filter_id: 2,
phrase: "knight",
- context: ["home"]
+ context: ["home"],
+ hide: true
}
{:ok, _filter} = Pleroma.Filter.create(query)
@@ -93,14 +97,17 @@ test "update a filter" do
}
conn =
- put(conn, "/api/v1/filters/#{query.filter_id}", %{
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/v1/filters/#{query.filter_id}", %{
phrase: new.phrase,
context: new.context
})
- assert response = json_response(conn, 200)
+ assert response = json_response_and_validate_schema(conn, 200)
assert response["phrase"] == new.phrase
assert response["context"] == new.context
+ assert response["irreversible"] == true
end
test "delete a filter" do
@@ -117,7 +124,6 @@ test "delete a filter" do
conn = delete(conn, "/api/v1/filters/#{filter.filter_id}")
- assert response = json_response(conn, 200)
- assert response == %{}
+ assert json_response_and_validate_schema(conn, 200) == %{}
end
end
diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
index d8dbe4800..44e12d15a 100644
--- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs
+++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
@@ -27,7 +27,7 @@ test "/api/v1/follow_requests works", %{user: user, conn: conn} do
conn = get(conn, "/api/v1/follow_requests")
- assert [relationship] = json_response(conn, 200)
+ assert [relationship] = json_response_and_validate_schema(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
@@ -44,7 +44,7 @@ test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do
conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize")
- assert relationship = json_response(conn, 200)
+ assert relationship = json_response_and_validate_schema(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = User.get_cached_by_id(user.id)
@@ -62,7 +62,7 @@ test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do
conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject")
- assert relationship = json_response(conn, 200)
+ assert relationship = json_response_and_validate_schema(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = User.get_cached_by_id(user.id)
diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs
index 2737dcaba..90840d5ab 100644
--- a/test/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/web/mastodon_api/controllers/instance_controller_test.exs
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
test "get instance information", %{conn: conn} do
conn = get(conn, "/api/v1/instance")
- assert result = json_response(conn, 200)
+ assert result = json_response_and_validate_schema(conn, 200)
email = Pleroma.Config.get([:instance, :email])
# Note: not checking for "max_toot_chars" since it's optional
@@ -34,6 +34,10 @@ test "get instance information", %{conn: conn} do
"banner_upload_limit" => _
} = result
+ assert result["pleroma"]["metadata"]["features"]
+ assert result["pleroma"]["metadata"]["federation"]
+ assert result["pleroma"]["vapid_public_key"]
+
assert email == from_config_email
end
@@ -52,7 +56,7 @@ test "get instance stats", %{conn: conn} do
conn = get(conn, "/api/v1/instance")
- assert result = json_response(conn, 200)
+ assert result = json_response_and_validate_schema(conn, 200)
stats = result["stats"]
@@ -70,7 +74,7 @@ test "get peers", %{conn: conn} do
conn = get(conn, "/api/v1/instance/peers")
- assert result = json_response(conn, 200)
+ assert result = json_response_and_validate_schema(conn, 200)
assert ["peer1.com", "peer2.com"] == Enum.sort(result)
end
diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs
index c9c4cbb49..57a9ef4a4 100644
--- a/test/web/mastodon_api/controllers/list_controller_test.exs
+++ b/test/web/mastodon_api/controllers/list_controller_test.exs
@@ -12,37 +12,44 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
test "creating a list" do
%{conn: conn} = oauth_access(["write:lists"])
- conn = post(conn, "/api/v1/lists", %{"title" => "cuties"})
-
- assert %{"title" => title} = json_response(conn, 200)
- assert title == "cuties"
+ assert %{"title" => "cuties"} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/lists", %{"title" => "cuties"})
+ |> json_response_and_validate_schema(:ok)
end
test "renders error for invalid params" do
%{conn: conn} = oauth_access(["write:lists"])
- conn = post(conn, "/api/v1/lists", %{"title" => nil})
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/lists", %{"title" => nil})
- assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity)
+ assert %{"error" => "title - null value where string expected."} =
+ json_response_and_validate_schema(conn, 400)
end
test "listing a user's lists" do
%{conn: conn} = oauth_access(["read:lists", "write:lists"])
conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/lists", %{"title" => "cuties"})
- |> json_response(:ok)
+ |> json_response_and_validate_schema(:ok)
conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/lists", %{"title" => "cofe"})
- |> json_response(:ok)
+ |> json_response_and_validate_schema(:ok)
conn = get(conn, "/api/v1/lists")
assert [
%{"id" => _, "title" => "cofe"},
%{"id" => _, "title" => "cuties"}
- ] = json_response(conn, :ok)
+ ] = json_response_and_validate_schema(conn, :ok)
end
test "adding users to a list" do
@@ -50,9 +57,12 @@ test "adding users to a list" do
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
- conn = post(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+ assert %{} ==
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+ |> json_response_and_validate_schema(:ok)
- assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [other_user.follower_address]
end
@@ -65,9 +75,12 @@ test "removing users from a list" do
{:ok, list} = Pleroma.List.follow(list, other_user)
{:ok, list} = Pleroma.List.follow(list, third_user)
- conn = delete(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+ assert %{} ==
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
+ |> json_response_and_validate_schema(:ok)
- assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [third_user.follower_address]
end
@@ -83,7 +96,7 @@ test "listing users in a list" do
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
- assert [%{"id" => id}] = json_response(conn, 200)
+ assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200)
assert id == to_string(other_user.id)
end
@@ -96,7 +109,7 @@ test "retrieving a list" do
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}")
- assert %{"id" => id} = json_response(conn, 200)
+ assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
assert id == to_string(list.id)
end
@@ -105,17 +118,18 @@ test "renders 404 if list is not found" do
conn = get(conn, "/api/v1/lists/666")
- assert %{"error" => "List not found"} = json_response(conn, :not_found)
+ assert %{"error" => "List not found"} = json_response_and_validate_schema(conn, :not_found)
end
test "renaming a list" do
%{user: user, conn: conn} = oauth_access(["write:lists"])
{:ok, list} = Pleroma.List.create("name", user)
- conn = put(conn, "/api/v1/lists/#{list.id}", %{"title" => "newname"})
-
- assert %{"title" => name} = json_response(conn, 200)
- assert name == "newname"
+ assert %{"title" => "newname"} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"})
+ |> json_response_and_validate_schema(:ok)
end
test "validates title when renaming a list" do
@@ -125,9 +139,11 @@ test "validates title when renaming a list" do
conn =
conn
|> assign(:user, user)
+ |> put_req_header("content-type", "application/json")
|> put("/api/v1/lists/#{list.id}", %{"title" => " "})
- assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity)
+ assert %{"error" => "can't be blank"} ==
+ json_response_and_validate_schema(conn, :unprocessable_entity)
end
test "deleting a list" do
@@ -136,7 +152,7 @@ test "deleting a list" do
conn = delete(conn, "/api/v1/lists/#{list.id}")
- assert %{} = json_response(conn, 200)
+ assert %{} = json_response_and_validate_schema(conn, 200)
assert is_nil(Repo.get(Pleroma.List, list.id))
end
end
diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs
index 919f295bd..6dd40fb4a 100644
--- a/test/web/mastodon_api/controllers/marker_controller_test.exs
+++ b/test/web/mastodon_api/controllers/marker_controller_test.exs
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
test "gets markers with correct scopes", %{conn: conn} do
user = insert(:user)
token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
+ insert_list(7, :notification, user: user)
{:ok, %{"notifications" => marker}} =
Pleroma.Marker.upsert(
@@ -22,14 +23,15 @@ test "gets markers with correct scopes", %{conn: conn} do
conn
|> assign(:user, user)
|> assign(:token, token)
- |> get("/api/v1/markers", %{timeline: ["notifications"]})
- |> json_response(200)
+ |> get("/api/v1/markers?timeline[]=notifications")
+ |> json_response_and_validate_schema(200)
assert response == %{
"notifications" => %{
"last_read_id" => "69420",
"updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
- "version" => 0
+ "version" => 0,
+ "pleroma" => %{"unread_count" => 7}
}
}
end
@@ -45,7 +47,7 @@ test "gets markers with missed scopes", %{conn: conn} do
|> assign(:user, user)
|> assign(:token, token)
|> get("/api/v1/markers", %{timeline: ["notifications"]})
- |> json_response(403)
+ |> json_response_and_validate_schema(403)
assert response == %{"error" => "Insufficient permissions: read:statuses."}
end
@@ -60,17 +62,19 @@ test "creates a marker with correct scopes", %{conn: conn} do
conn
|> assign(:user, user)
|> assign(:token, token)
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/markers", %{
home: %{last_read_id: "777"},
notifications: %{"last_read_id" => "69420"}
})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
assert %{
"notifications" => %{
"last_read_id" => "69420",
"updated_at" => _,
- "version" => 0
+ "version" => 0,
+ "pleroma" => %{"unread_count" => 0}
}
} = response
end
@@ -89,17 +93,19 @@ test "updates exist marker", %{conn: conn} do
conn
|> assign(:user, user)
|> assign(:token, token)
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/markers", %{
home: %{last_read_id: "777"},
notifications: %{"last_read_id" => "69888"}
})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
assert response == %{
"notifications" => %{
"last_read_id" => "69888",
"updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
- "version" => 0
+ "version" => 0,
+ "pleroma" => %{"unread_count" => 0}
}
}
end
@@ -112,11 +118,12 @@ test "creates a marker with missed scopes", %{conn: conn} do
conn
|> assign(:user, user)
|> assign(:token, token)
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/markers", %{
home: %{last_read_id: "777"},
notifications: %{"last_read_id" => "69420"}
})
- |> json_response(403)
+ |> json_response_and_validate_schema(403)
assert response == %{"error" => "Insufficient permissions: write:statuses."}
end
diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs
index 8c815b415..db380f76a 100644
--- a/test/web/mastodon_api/controllers/notification_controller_test.exs
+++ b/test/web/mastodon_api/controllers/notification_controller_test.exs
@@ -25,7 +25,7 @@ test "does NOT render account/pleroma/relationship if this is disabled by defaul
conn
|> assign(:user, user)
|> get("/api/v1/notifications")
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
assert Enum.all?(response, fn n ->
get_in(n, ["account", "pleroma", "relationship"]) == %{}
@@ -50,7 +50,9 @@ test "list of notifications" do
user.ap_id
}\" rel=\"ugc\">@
#{user.nickname}"
- assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
+ assert [%{"status" => %{"content" => response}} | _rest] =
+ json_response_and_validate_schema(conn, 200)
+
assert response == expected_response
end
@@ -69,7 +71,7 @@ test "getting a single notification" do
user.ap_id
}\" rel=\"ugc\">@
#{user.nickname}"
- assert %{"status" => %{"content" => response}} = json_response(conn, 200)
+ assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200)
assert response == expected_response
end
@@ -84,9 +86,10 @@ test "dismissing a single notification (deprecated endpoint)" do
conn =
conn
|> assign(:user, user)
- |> post("/api/v1/notifications/dismiss", %{"id" => notification.id})
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)})
- assert %{} = json_response(conn, 200)
+ assert %{} = json_response_and_validate_schema(conn, 200)
end
test "dismissing a single notification" do
@@ -102,7 +105,7 @@ test "dismissing a single notification" do
|> assign(:user, user)
|> post("/api/v1/notifications/#{notification.id}/dismiss")
- assert %{} = json_response(conn, 200)
+ assert %{} = json_response_and_validate_schema(conn, 200)
end
test "clearing all notifications" do
@@ -115,11 +118,11 @@ test "clearing all notifications" do
ret_conn = post(conn, "/api/v1/notifications/clear")
- assert %{} = json_response(ret_conn, 200)
+ assert %{} = json_response_and_validate_schema(ret_conn, 200)
ret_conn = get(conn, "/api/v1/notifications")
- assert all = json_response(ret_conn, 200)
+ assert all = json_response_and_validate_schema(ret_conn, 200)
assert all == []
end
@@ -143,7 +146,7 @@ test "paginates notifications using min_id, since_id, max_id, and limit" do
result =
conn
|> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
- |> json_response(:ok)
+ |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
@@ -151,7 +154,7 @@ test "paginates notifications using min_id, since_id, max_id, and limit" do
result =
conn
|> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
- |> json_response(:ok)
+ |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
@@ -159,7 +162,7 @@ test "paginates notifications using min_id, since_id, max_id, and limit" do
result =
conn
|> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
- |> json_response(:ok)
+ |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end
@@ -181,36 +184,28 @@ test "filters notifications for mentions" do
{:ok, private_activity} =
CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"})
- conn_res =
- get(conn, "/api/v1/notifications", %{
- exclude_visibilities: ["public", "unlisted", "private"]
- })
+ query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]})
+ conn_res = get(conn, "/api/v1/notifications?" <> query)
- assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == direct_activity.id
- conn_res =
- get(conn, "/api/v1/notifications", %{
- exclude_visibilities: ["public", "unlisted", "direct"]
- })
+ query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]})
+ conn_res = get(conn, "/api/v1/notifications?" <> query)
- assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == private_activity.id
- conn_res =
- get(conn, "/api/v1/notifications", %{
- exclude_visibilities: ["public", "private", "direct"]
- })
+ query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]})
+ conn_res = get(conn, "/api/v1/notifications?" <> query)
- assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == unlisted_activity.id
- conn_res =
- get(conn, "/api/v1/notifications", %{
- exclude_visibilities: ["unlisted", "private", "direct"]
- })
+ query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]})
+ conn_res = get(conn, "/api/v1/notifications?" <> query)
- assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
+ assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == public_activity.id
end
@@ -237,8 +232,8 @@ test "filters notifications for Like activities" do
activity_ids =
conn
- |> get("/api/v1/notifications", %{exclude_visibilities: ["direct"]})
- |> json_response(200)
+ |> get("/api/v1/notifications?exclude_visibilities[]=direct")
+ |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids
@@ -248,8 +243,8 @@ test "filters notifications for Like activities" do
activity_ids =
conn
- |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]})
- |> json_response(200)
+ |> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
+ |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids
@@ -259,8 +254,8 @@ test "filters notifications for Like activities" do
activity_ids =
conn
- |> get("/api/v1/notifications", %{exclude_visibilities: ["private"]})
- |> json_response(200)
+ |> get("/api/v1/notifications?exclude_visibilities[]=private")
+ |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids
@@ -270,8 +265,8 @@ test "filters notifications for Like activities" do
activity_ids =
conn
- |> get("/api/v1/notifications", %{exclude_visibilities: ["public"]})
- |> json_response(200)
+ |> get("/api/v1/notifications?exclude_visibilities[]=public")
+ |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
refute public_activity.id in activity_ids
@@ -295,8 +290,8 @@ test "filters notifications for Announce activities" do
activity_ids =
conn
- |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]})
- |> json_response(200)
+ |> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
+ |> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids
@@ -319,25 +314,27 @@ test "filters notifications using exclude_types" do
reblog_notification_id = get_notification_id_by_activity(reblog_activity)
follow_notification_id = get_notification_id_by_activity(follow_activity)
- conn_res =
- get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]})
+ query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]})
+ conn_res = get(conn, "/api/v1/notifications?" <> query)
- assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200)
+ assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
- conn_res =
- get(conn, "/api/v1/notifications", %{exclude_types: ["favourite", "reblog", "follow"]})
+ query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]})
+ conn_res = get(conn, "/api/v1/notifications?" <> query)
- assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200)
+ assert [%{"id" => ^mention_notification_id}] =
+ json_response_and_validate_schema(conn_res, 200)
- conn_res =
- get(conn, "/api/v1/notifications", %{exclude_types: ["reblog", "follow", "mention"]})
+ query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]})
+ conn_res = get(conn, "/api/v1/notifications?" <> query)
- assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200)
+ assert [%{"id" => ^favorite_notification_id}] =
+ json_response_and_validate_schema(conn_res, 200)
- conn_res =
- get(conn, "/api/v1/notifications", %{exclude_types: ["follow", "mention", "favourite"]})
+ query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]})
+ conn_res = get(conn, "/api/v1/notifications?" <> query)
- assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
+ assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
end
test "filters notifications using include_types" do
@@ -355,32 +352,34 @@ test "filters notifications using include_types" do
reblog_notification_id = get_notification_id_by_activity(reblog_activity)
follow_notification_id = get_notification_id_by_activity(follow_activity)
- conn_res = get(conn, "/api/v1/notifications", %{include_types: ["follow"]})
+ conn_res = get(conn, "/api/v1/notifications?include_types[]=follow")
- assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200)
+ assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
- conn_res = get(conn, "/api/v1/notifications", %{include_types: ["mention"]})
+ conn_res = get(conn, "/api/v1/notifications?include_types[]=mention")
- assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200)
+ assert [%{"id" => ^mention_notification_id}] =
+ json_response_and_validate_schema(conn_res, 200)
- conn_res = get(conn, "/api/v1/notifications", %{include_types: ["favourite"]})
+ conn_res = get(conn, "/api/v1/notifications?include_types[]=favourite")
- assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200)
+ assert [%{"id" => ^favorite_notification_id}] =
+ json_response_and_validate_schema(conn_res, 200)
- conn_res = get(conn, "/api/v1/notifications", %{include_types: ["reblog"]})
+ conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog")
- assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
+ assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
- result = conn |> get("/api/v1/notifications") |> json_response(200)
+ result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200)
assert length(result) == 4
+ query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]})
+
result =
conn
- |> get("/api/v1/notifications", %{
- include_types: ["follow", "mention", "favourite", "reblog"]
- })
- |> json_response(200)
+ |> get("/api/v1/notifications?" <> query)
+ |> json_response_and_validate_schema(200)
assert length(result) == 4
end
@@ -402,7 +401,7 @@ test "destroy multiple" do
result =
conn
|> get("/api/v1/notifications")
- |> json_response(:ok)
+ |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
@@ -414,22 +413,19 @@ test "destroy multiple" do
result =
conn2
|> get("/api/v1/notifications")
- |> json_response(:ok)
+ |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
- conn_destroy =
- conn
- |> delete("/api/v1/notifications/destroy_multiple", %{
- "ids" => [notification1_id, notification2_id]
- })
+ query = params_to_query(%{ids: [notification1_id, notification2_id]})
+ conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query)
- assert json_response(conn_destroy, 200) == %{}
+ assert json_response_and_validate_schema(conn_destroy, 200) == %{}
result =
conn2
|> get("/api/v1/notifications")
- |> json_response(:ok)
+ |> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
end
@@ -443,13 +439,13 @@ test "doesn't see notifications after muting user with notifications" do
ret_conn = get(conn, "/api/v1/notifications")
- assert length(json_response(ret_conn, 200)) == 1
+ assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2)
conn = get(conn, "/api/v1/notifications")
- assert json_response(conn, 200) == []
+ assert json_response_and_validate_schema(conn, 200) == []
end
test "see notifications after muting user without notifications" do
@@ -461,13 +457,13 @@ test "see notifications after muting user without notifications" do
ret_conn = get(conn, "/api/v1/notifications")
- assert length(json_response(ret_conn, 200)) == 1
+ assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2, false)
conn = get(conn, "/api/v1/notifications")
- assert length(json_response(conn, 200)) == 1
+ assert length(json_response_and_validate_schema(conn, 200)) == 1
end
test "see notifications after muting user with notifications and with_muted parameter" do
@@ -479,13 +475,13 @@ test "see notifications after muting user with notifications and with_muted para
ret_conn = get(conn, "/api/v1/notifications")
- assert length(json_response(ret_conn, 200)) == 1
+ assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2)
- conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})
+ conn = get(conn, "/api/v1/notifications?with_muted=true")
- assert length(json_response(conn, 200)) == 1
+ assert length(json_response_and_validate_schema(conn, 200)) == 1
end
@tag capture_log: true
@@ -512,7 +508,7 @@ test "see move notifications" do
conn = get(conn, "/api/v1/notifications")
- assert length(json_response(conn, 200)) == 1
+ assert length(json_response_and_validate_schema(conn, 200)) == 1
end
describe "link headers" do
@@ -538,10 +534,10 @@ test "preserves parameters in link headers" do
conn =
conn
|> assign(:user, user)
- |> get("/api/v1/notifications", %{media_only: true})
+ |> get("/api/v1/notifications?limit=5")
assert [link_header] = get_resp_header(conn, "link")
- assert link_header =~ ~r/media_only=true/
+ assert link_header =~ ~r/limit=5/
assert link_header =~ ~r/min_id=#{notification2.id}/
assert link_header =~ ~r/max_id=#{notification1.id}/
end
@@ -560,14 +556,14 @@ test "account_id" do
assert [%{"account" => %{"id" => ^account_id}}] =
conn
|> assign(:user, user)
- |> get("/api/v1/notifications", %{account_id: account_id})
- |> json_response(200)
+ |> get("/api/v1/notifications?account_id=#{account_id}")
+ |> json_response_and_validate_schema(200)
assert %{"error" => "Account is not found"} =
conn
|> assign(:user, user)
- |> get("/api/v1/notifications", %{account_id: "cofe"})
- |> json_response(404)
+ |> get("/api/v1/notifications?account_id=cofe")
+ |> json_response_and_validate_schema(404)
end
end
@@ -577,4 +573,11 @@ defp get_notification_id_by_activity(%{id: id}) do
|> Map.get(:id)
|> to_string()
end
+
+ defp params_to_query(%{} = params) do
+ Enum.map_join(params, "&", fn
+ {k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")
+ {k, v} -> k <> "=" <> v
+ end)
+ end
end
diff --git a/test/web/mastodon_api/controllers/poll_controller_test.exs b/test/web/mastodon_api/controllers/poll_controller_test.exs
index 88b13a25a..d8f34aa86 100644
--- a/test/web/mastodon_api/controllers/poll_controller_test.exs
+++ b/test/web/mastodon_api/controllers/poll_controller_test.exs
@@ -24,7 +24,7 @@ test "returns poll entity for object id", %{user: user, conn: conn} do
conn = get(conn, "/api/v1/polls/#{object.id}")
- response = json_response(conn, 200)
+ response = json_response_and_validate_schema(conn, 200)
id = to_string(object.id)
assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
end
@@ -43,7 +43,7 @@ test "does not expose polls for private statuses", %{conn: conn} do
conn = get(conn, "/api/v1/polls/#{object.id}")
- assert json_response(conn, 404)
+ assert json_response_and_validate_schema(conn, 404)
end
end
@@ -65,9 +65,12 @@ test "votes are added to the poll", %{conn: conn} do
object = Object.normalize(activity)
- conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
- assert json_response(conn, 200)
+ assert json_response_and_validate_schema(conn, 200)
object = Object.get_by_id(object.id)
assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
@@ -85,8 +88,9 @@ test "author can't vote", %{user: user, conn: conn} do
object = Object.normalize(activity)
assert conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
- |> json_response(422) == %{"error" => "Poll's author can't vote"}
+ |> json_response_and_validate_schema(422) == %{"error" => "Poll's author can't vote"}
object = Object.get_by_id(object.id)
@@ -105,8 +109,9 @@ test "does not allow multiple choices on a single-choice question", %{conn: conn
object = Object.normalize(activity)
assert conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
- |> json_response(422) == %{"error" => "Too many choices"}
+ |> json_response_and_validate_schema(422) == %{"error" => "Too many choices"}
object = Object.get_by_id(object.id)
@@ -126,15 +131,21 @@ test "does not allow choice index to be greater than options count", %{conn: con
object = Object.normalize(activity)
- conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
- assert json_response(conn, 422) == %{"error" => "Invalid indices"}
+ assert json_response_and_validate_schema(conn, 422) == %{"error" => "Invalid indices"}
end
test "returns 404 error when object is not exist", %{conn: conn} do
- conn = post(conn, "/api/v1/polls/1/votes", %{"choices" => [0]})
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/polls/1/votes", %{"choices" => [0]})
- assert json_response(conn, 404) == %{"error" => "Record not found"}
+ assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
test "returns 404 when poll is private and not available for user", %{conn: conn} do
@@ -149,9 +160,12 @@ test "returns 404 when poll is private and not available for user", %{conn: conn
object = Object.normalize(activity)
- conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
- assert json_response(conn, 404) == %{"error" => "Record not found"}
+ assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
end
diff --git a/test/web/mastodon_api/controllers/report_controller_test.exs b/test/web/mastodon_api/controllers/report_controller_test.exs
index 34ec8119e..21b037237 100644
--- a/test/web/mastodon_api/controllers/report_controller_test.exs
+++ b/test/web/mastodon_api/controllers/report_controller_test.exs
@@ -22,8 +22,9 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
test "submit a basic report", %{conn: conn, target_user: target_user} do
assert %{"action_taken" => false, "id" => _} =
conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
end
test "submit a report with statuses and comment", %{
@@ -33,23 +34,25 @@ test "submit a report with statuses and comment", %{
} do
assert %{"action_taken" => false, "id" => _} =
conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{
"account_id" => target_user.id,
"status_ids" => [activity.id],
"comment" => "bad status!",
"forward" => "false"
})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
end
test "account_id is required", %{
conn: conn,
activity: activity
} do
- assert %{"error" => "Valid `account_id` required"} =
+ assert %{"error" => "Missing field: account_id."} =
conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"status_ids" => [activity.id]})
- |> json_response(400)
+ |> json_response_and_validate_schema(400)
end
test "comment must be up to the size specified in the config", %{
@@ -63,17 +66,21 @@ test "comment must be up to the size specified in the config", %{
assert ^error =
conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
- |> json_response(400)
+ |> json_response_and_validate_schema(400)
end
test "returns error when account is not exist", %{
conn: conn,
activity: activity
} do
- conn = post(conn, "/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
- assert json_response(conn, 400) == %{"error" => "Account not found"}
+ assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"}
end
test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do
@@ -81,7 +88,8 @@ test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_
assert %{"action_taken" => false, "id" => _} =
conn
+ |> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{"account_id" => target_user.id})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
end
end
diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
index f86274d57..1ff871c89 100644
--- a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
+++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
@@ -24,19 +24,19 @@ test "shows scheduled activities" do
# min_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
- result = json_response(conn_res, 200)
+ result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
# since_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
- result = json_response(conn_res, 200)
+ result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
# max_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
- result = json_response(conn_res, 200)
+ result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
end
@@ -46,12 +46,12 @@ test "shows a scheduled activity" do
res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}")
- assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200)
+ assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200)
assert scheduled_activity_id == scheduled_activity.id |> to_string()
res_conn = get(conn, "/api/v1/scheduled_statuses/404")
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end
test "updates a scheduled activity" do
@@ -74,22 +74,32 @@ test "updates a scheduled activity" do
assert job.args == %{"activity_id" => scheduled_activity.id}
assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at)
- new_scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 120)
+ new_scheduled_at =
+ NaiveDateTime.utc_now()
+ |> Timex.shift(minutes: 120)
+ |> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime)
res_conn =
- put(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
scheduled_at: new_scheduled_at
})
- assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200)
+ assert %{"scheduled_at" => expected_scheduled_at} =
+ json_response_and_validate_schema(res_conn, 200)
+
assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
job = refresh_record(job)
assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at)
- res_conn = put(conn, "/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
+ res_conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end
test "deletes a scheduled activity" do
@@ -115,7 +125,7 @@ test "deletes a scheduled activity" do
|> assign(:user, user)
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
- assert %{} = json_response(res_conn, 200)
+ assert %{} = json_response_and_validate_schema(res_conn, 200)
refute Repo.get(ScheduledActivity, scheduled_activity.id)
refute Repo.get(Oban.Job, job.id)
@@ -124,6 +134,6 @@ test "deletes a scheduled activity" do
|> assign(:user, user)
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
- assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+ assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end
end
diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs
index 11133ff66..02476acb6 100644
--- a/test/web/mastodon_api/controllers/search_controller_test.exs
+++ b/test/web/mastodon_api/controllers/search_controller_test.exs
@@ -27,8 +27,8 @@ test "it returns empty result if user or status search return undefined error",
capture_log(fn ->
results =
conn
- |> get("/api/v2/search", %{"q" => "2hu"})
- |> json_response(200)
+ |> get("/api/v2/search?q=2hu")
+ |> json_response_and_validate_schema(200)
assert results["accounts"] == []
assert results["statuses"] == []
@@ -54,8 +54,8 @@ test "search", %{conn: conn} do
results =
conn
- |> get("/api/v2/search", %{"q" => "2hu #private"})
- |> json_response(200)
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}")
+ |> json_response_and_validate_schema(200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
@@ -68,8 +68,8 @@ test "search", %{conn: conn} do
assert status["id"] == to_string(activity.id)
results =
- get(conn, "/api/v2/search", %{"q" => "天子"})
- |> json_response(200)
+ get(conn, "/api/v2/search?q=天子")
+ |> json_response_and_validate_schema(200)
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
@@ -89,8 +89,8 @@ test "excludes a blocked users from search results", %{conn: conn} do
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
- |> get("/api/v2/search", %{"q" => "Agent"})
- |> json_response(200)
+ |> get("/api/v2/search?q=Agent")
+ |> json_response_and_validate_schema(200)
status_ids = Enum.map(results["statuses"], fn g -> g["id"] end)
@@ -107,8 +107,8 @@ test "account search", %{conn: conn} do
results =
conn
- |> get("/api/v1/accounts/search", %{"q" => "shp"})
- |> json_response(200)
+ |> get("/api/v1/accounts/search?q=shp")
+ |> json_response_and_validate_schema(200)
result_ids = for result <- results, do: result["acct"]
@@ -117,8 +117,8 @@ test "account search", %{conn: conn} do
results =
conn
- |> get("/api/v1/accounts/search", %{"q" => "2hu"})
- |> json_response(200)
+ |> get("/api/v1/accounts/search?q=2hu")
+ |> json_response_and_validate_schema(200)
result_ids = for result <- results, do: result["acct"]
@@ -130,8 +130,8 @@ test "returns account if query contains a space", %{conn: conn} do
results =
conn
- |> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "})
- |> json_response(200)
+ |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx")
+ |> json_response_and_validate_schema(200)
assert length(results) == 1
end
@@ -146,8 +146,8 @@ test "it returns empty result if user or status search return undefined error",
capture_log(fn ->
results =
conn
- |> get("/api/v1/search", %{"q" => "2hu"})
- |> json_response(200)
+ |> get("/api/v1/search?q=2hu")
+ |> json_response_and_validate_schema(200)
assert results["accounts"] == []
assert results["statuses"] == []
@@ -173,8 +173,8 @@ test "search", %{conn: conn} do
results =
conn
- |> get("/api/v1/search", %{"q" => "2hu"})
- |> json_response(200)
+ |> get("/api/v1/search?q=2hu")
+ |> json_response_and_validate_schema(200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
@@ -194,8 +194,8 @@ test "search fetches remote statuses and prefers them over other results", %{con
results =
conn
- |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
- |> json_response(200)
+ |> get("/api/v1/search?q=https://shitposter.club/notice/2827873")
+ |> json_response_and_validate_schema(200)
[status, %{"id" => ^activity_id}] = results["statuses"]
@@ -212,10 +212,12 @@ test "search doesn't show statuses that it shouldn't", %{conn: conn} do
})
capture_log(fn ->
+ q = Object.normalize(activity).data["id"]
+
results =
conn
- |> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]})
- |> json_response(200)
+ |> get("/api/v1/search?q=#{q}")
+ |> json_response_and_validate_schema(200)
[] = results["statuses"]
end)
@@ -228,8 +230,8 @@ test "search fetches remote accounts", %{conn: conn} do
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
- |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"})
- |> json_response(200)
+ |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=true")
+ |> json_response_and_validate_schema(200)
[account] = results["accounts"]
assert account["acct"] == "mike@osada.macgirvin.com"
@@ -238,8 +240,8 @@ test "search fetches remote accounts", %{conn: conn} do
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
results =
conn
- |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"})
- |> json_response(200)
+ |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false")
+ |> json_response_and_validate_schema(200)
assert [] == results["accounts"]
end
@@ -254,16 +256,16 @@ test "search with limit and offset", %{conn: conn} do
result =
conn
- |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1})
+ |> get("/api/v1/search?q=2hu&limit=1")
- assert results = json_response(result, 200)
+ assert results = json_response_and_validate_schema(result, 200)
assert [%{"id" => activity_id1}] = results["statuses"]
assert [_] = results["accounts"]
results =
conn
- |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1, "offset" => 1})
- |> json_response(200)
+ |> get("/api/v1/search?q=2hu&limit=1&offset=1")
+ |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id2}] = results["statuses"]
assert [] = results["accounts"]
@@ -279,13 +281,13 @@ test "search returns results only for the given type", %{conn: conn} do
assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} =
conn
- |> get("/api/v1/search", %{"q" => "2hu", "type" => "statuses"})
- |> json_response(200)
+ |> get("/api/v1/search?q=2hu&type=statuses")
+ |> json_response_and_validate_schema(200)
assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} =
conn
- |> get("/api/v1/search", %{"q" => "2hu", "type" => "accounts"})
- |> json_response(200)
+ |> get("/api/v1/search?q=2hu&type=accounts")
+ |> json_response_and_validate_schema(200)
end
test "search uses account_id to filter statuses by the author", %{conn: conn} do
@@ -297,8 +299,8 @@ test "search uses account_id to filter statuses by the author", %{conn: conn} do
results =
conn
- |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user.id})
- |> json_response(200)
+ |> get("/api/v1/search?q=2hu&account_id=#{user.id}")
+ |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id1}] = results["statuses"]
assert activity_id1 == activity1.id
@@ -306,8 +308,8 @@ test "search uses account_id to filter statuses by the author", %{conn: conn} do
results =
conn
- |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user_two.id})
- |> json_response(200)
+ |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}")
+ |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id2}] = results["statuses"]
assert activity_id2 == activity2.id
diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs
index 5682498c0..4aa260663 100644
--- a/test/web/mastodon_api/controllers/subscription_controller_test.exs
+++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
+
alias Pleroma.Web.Push
alias Pleroma.Web.Push.Subscription
@@ -27,6 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
build_conn()
|> assign(:user, user)
|> assign(:token, token)
+ |> put_req_header("content-type", "application/json")
%{conn: conn, user: user, token: token}
end
@@ -47,8 +49,8 @@ defmacro assert_error_when_disable_push(do: yield) do
test "returns error when push disabled ", %{conn: conn} do
assert_error_when_disable_push do
conn
- |> post("/api/v1/push/subscription", %{})
- |> json_response(403)
+ |> post("/api/v1/push/subscription", %{subscription: @sub})
+ |> json_response_and_validate_schema(403)
end
end
@@ -59,7 +61,7 @@ test "successful creation", %{conn: conn} do
"data" => %{"alerts" => %{"mention" => true, "test" => true}},
"subscription" => @sub
})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
[subscription] = Pleroma.Repo.all(Subscription)
@@ -77,7 +79,7 @@ test "returns error when push disabled ", %{conn: conn} do
assert_error_when_disable_push do
conn
|> get("/api/v1/push/subscription", %{})
- |> json_response(403)
+ |> json_response_and_validate_schema(403)
end
end
@@ -85,9 +87,9 @@ test "returns error when user hasn't subscription", %{conn: conn} do
res =
conn
|> get("/api/v1/push/subscription", %{})
- |> json_response(404)
+ |> json_response_and_validate_schema(404)
- assert "Not found" == res
+ assert %{"error" => "Record not found"} == res
end
test "returns a user subsciption", %{conn: conn, user: user, token: token} do
@@ -101,7 +103,7 @@ test "returns a user subsciption", %{conn: conn, user: user, token: token} do
res =
conn
|> get("/api/v1/push/subscription", %{})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
expect = %{
"alerts" => %{"mention" => true},
@@ -130,7 +132,7 @@ test "returns error when push disabled ", %{conn: conn} do
assert_error_when_disable_push do
conn
|> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}})
- |> json_response(403)
+ |> json_response_and_validate_schema(403)
end
end
@@ -140,7 +142,7 @@ test "returns updated subsciption", %{conn: conn, subscription: subscription} do
|> put("/api/v1/push/subscription", %{
data: %{"alerts" => %{"mention" => false, "follow" => true}}
})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
expect = %{
"alerts" => %{"follow" => true, "mention" => false},
@@ -158,7 +160,7 @@ test "returns error when push disabled ", %{conn: conn} do
assert_error_when_disable_push do
conn
|> delete("/api/v1/push/subscription", %{})
- |> json_response(403)
+ |> json_response_and_validate_schema(403)
end
end
@@ -166,9 +168,9 @@ test "returns error when user hasn't subscription", %{conn: conn} do
res =
conn
|> delete("/api/v1/push/subscription", %{})
- |> json_response(404)
+ |> json_response_and_validate_schema(404)
- assert "Not found" == res
+ assert %{"error" => "Record not found"} == res
end
test "returns empty result and delete user subsciption", %{
@@ -186,7 +188,7 @@ test "returns empty result and delete user subsciption", %{
res =
conn
|> delete("/api/v1/push/subscription", %{})
- |> json_response(200)
+ |> json_response_and_validate_schema(200)
assert %{} == res
refute Pleroma.Repo.get(Subscription, subscription.id)
diff --git a/test/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/web/mastodon_api/controllers/suggestion_controller_test.exs
index 8d0e70db8..f120bd0cd 100644
--- a/test/web/mastodon_api/controllers/suggestion_controller_test.exs
+++ b/test/web/mastodon_api/controllers/suggestion_controller_test.exs
@@ -5,8 +5,6 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
use Pleroma.Web.ConnCase
- alias Pleroma.Config
-
setup do: oauth_access(["read"])
test "returns empty result", %{conn: conn} do
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 85fa4f6a2..5fb162141 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -466,6 +466,24 @@ test "shows unread_conversation_count only to the account owner" do
:unread_conversation_count
] == 1
end
+
+ test "shows unread_count only to the account owner" do
+ user = insert(:user)
+ insert_list(7, :notification, user: user)
+ other_user = insert(:user)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert AccountView.render(
+ "show.json",
+ %{user: user, for: other_user}
+ )[:pleroma][:unread_notifications_count] == nil
+
+ assert AccountView.render(
+ "show.json",
+ %{user: user, for: user}
+ )[:pleroma][:unread_notifications_count] == 7
+ end
end
describe "follow requests counter" do
diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs
index 893cf8857..48a0a6d33 100644
--- a/test/web/mastodon_api/views/marker_view_test.exs
+++ b/test/web/mastodon_api/views/marker_view_test.exs
@@ -8,19 +8,21 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
import Pleroma.Factory
test "returns markers" do
- marker1 = insert(:marker, timeline: "notifications", last_read_id: "17")
+ marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5)
marker2 = insert(:marker, timeline: "home", last_read_id: "42")
assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
"home" => %{
last_read_id: "42",
updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
- version: 0
+ version: 0,
+ pleroma: %{unread_count: 0}
},
"notifications" => %{
last_read_id: "17",
updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
- version: 0
+ version: 0,
+ pleroma: %{unread_count: 5}
}
}
end
diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs
index c3ec9dfec..0806269a2 100644
--- a/test/web/mastodon_api/views/notification_view_test.exs
+++ b/test/web/mastodon_api/views/notification_view_test.exs
@@ -156,7 +156,7 @@ test "EmojiReact notification" do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
- {:ok, _activity, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+ {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
activity = Repo.get(Activity, activity.id)
diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs
index 6791c2fb0..b5e7dc317 100644
--- a/test/web/mastodon_api/views/status_view_test.exs
+++ b/test/web/mastodon_api/views/status_view_test.exs
@@ -32,9 +32,9 @@ test "has an emoji reaction list" do
third_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"})
- {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
- {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
- {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity)
@@ -402,11 +402,17 @@ test "attachments" do
pleroma: %{mime_type: "image/png"}
}
+ api_spec = Pleroma.Web.ApiSpec.spec()
+
assert expected == StatusView.render("attachment.json", %{attachment: object})
+ OpenApiSpex.TestAssertions.assert_schema(expected, "Attachment", api_spec)
# If theres a "id", use that instead of the generated one
object = Map.put(object, "id", 2)
- assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object})
+ result = StatusView.render("attachment.json", %{attachment: object})
+
+ assert %{id: "2"} = result
+ OpenApiSpex.TestAssertions.assert_schema(result, "Attachment", api_spec)
end
test "put the url advertised in the Activity in to the url attribute" do
diff --git a/test/web/oauth/mfa_controller_test.exs b/test/web/oauth/mfa_controller_test.exs
new file mode 100644
index 000000000..ce4a07320
--- /dev/null
+++ b/test/web/oauth/mfa_controller_test.exs
@@ -0,0 +1,306 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.MFAControllerTest do
+ use Pleroma.Web.ConnCase
+ import Pleroma.Factory
+
+ alias Pleroma.MFA
+ alias Pleroma.MFA.BackupCodes
+ alias Pleroma.MFA.TOTP
+ alias Pleroma.Repo
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.OAuthController
+
+ setup %{conn: conn} do
+ otp_secret = TOTP.generate_secret()
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ backup_codes: [Comeonin.Pbkdf2.hashpwsalt("test-code")],
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ app = insert(:oauth_app)
+ {:ok, conn: conn, user: user, app: app}
+ end
+
+ describe "show" do
+ setup %{conn: conn, user: user, app: app} do
+ mfa_token =
+ insert(:mfa_token,
+ user: user,
+ authorization: build(:oauth_authorization, app: app, scopes: ["write"])
+ )
+
+ {:ok, conn: conn, mfa_token: mfa_token}
+ end
+
+ test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do
+ conn =
+ get(
+ conn,
+ "/oauth/mfa",
+ %{
+ "mfa_token" => mfa_token.token,
+ "state" => "a_state",
+ "redirect_uri" => "http://localhost:8080/callback"
+ }
+ )
+
+ assert response = html_response(conn, 200)
+ assert response =~ "Two-factor authentication"
+ assert response =~ mfa_token.token
+ assert response =~ "http://localhost:8080/callback"
+ end
+
+ test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do
+ conn =
+ get(
+ conn,
+ "/oauth/mfa",
+ %{
+ "mfa_token" => mfa_token.token,
+ "state" => "a_state",
+ "redirect_uri" => "http://localhost:8080/callback",
+ "challenge_type" => "recovery"
+ }
+ )
+
+ assert response = html_response(conn, 200)
+ assert response =~ "Two-factor recovery"
+ assert response =~ mfa_token.token
+ assert response =~ "http://localhost:8080/callback"
+ end
+ end
+
+ describe "verify" do
+ setup %{conn: conn, user: user, app: app} do
+ mfa_token =
+ insert(:mfa_token,
+ user: user,
+ authorization: build(:oauth_authorization, app: app, scopes: ["write"])
+ )
+
+ {:ok, conn: conn, user: user, mfa_token: mfa_token, app: app}
+ end
+
+ test "POST /oauth/mfa/verify, verify totp code", %{
+ conn: conn,
+ user: user,
+ mfa_token: mfa_token,
+ app: app
+ } do
+ otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
+
+ conn =
+ conn
+ |> post("/oauth/mfa/verify", %{
+ "mfa" => %{
+ "mfa_token" => mfa_token.token,
+ "challenge_type" => "totp",
+ "code" => otp_token,
+ "state" => "a_state",
+ "redirect_uri" => OAuthController.default_redirect_uri(app)
+ }
+ })
+
+ target = redirected_to(conn)
+ target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string()
+ query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
+ assert %{"state" => "a_state", "code" => code} = query
+ assert target_url == OAuthController.default_redirect_uri(app)
+ auth = Repo.get_by(Authorization, token: code)
+ assert auth.scopes == ["write"]
+ end
+
+ test "POST /oauth/mfa/verify, verify recovery code", %{
+ conn: conn,
+ mfa_token: mfa_token,
+ app: app
+ } do
+ conn =
+ conn
+ |> post("/oauth/mfa/verify", %{
+ "mfa" => %{
+ "mfa_token" => mfa_token.token,
+ "challenge_type" => "recovery",
+ "code" => "test-code",
+ "state" => "a_state",
+ "redirect_uri" => OAuthController.default_redirect_uri(app)
+ }
+ })
+
+ target = redirected_to(conn)
+ target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string()
+ query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
+ assert %{"state" => "a_state", "code" => code} = query
+ assert target_url == OAuthController.default_redirect_uri(app)
+ auth = Repo.get_by(Authorization, token: code)
+ assert auth.scopes == ["write"]
+ end
+ end
+
+ describe "challenge/totp" do
+ test "returns access token with valid code", %{conn: conn, user: user, app: app} do
+ otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
+
+ mfa_token =
+ insert(:mfa_token,
+ user: user,
+ authorization: build(:oauth_authorization, app: app, scopes: ["write"])
+ )
+
+ response =
+ conn
+ |> post("/oauth/mfa/challenge", %{
+ "mfa_token" => mfa_token.token,
+ "challenge_type" => "totp",
+ "code" => otp_token,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+ |> json_response(:ok)
+
+ ap_id = user.ap_id
+
+ assert match?(
+ %{
+ "access_token" => _,
+ "expires_in" => 600,
+ "me" => ^ap_id,
+ "refresh_token" => _,
+ "scope" => "write",
+ "token_type" => "Bearer"
+ },
+ response
+ )
+ end
+
+ test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do
+ otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
+
+ response =
+ conn
+ |> post("/oauth/mfa/challenge", %{
+ "mfa_token" => "XXX",
+ "challenge_type" => "totp",
+ "code" => otp_token,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+ |> json_response(400)
+
+ assert response == %{"error" => "Invalid code"}
+ end
+
+ test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do
+ mfa_token = insert(:mfa_token, user: user)
+
+ response =
+ conn
+ |> post("/oauth/mfa/challenge", %{
+ "mfa_token" => mfa_token.token,
+ "challenge_type" => "totp",
+ "code" => "XXX",
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+ |> json_response(400)
+
+ assert response == %{"error" => "Invalid code"}
+ end
+
+ test "returns error when client credentails is wrong ", %{conn: conn, user: user} do
+ otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
+ mfa_token = insert(:mfa_token, user: user)
+
+ response =
+ conn
+ |> post("/oauth/mfa/challenge", %{
+ "mfa_token" => mfa_token.token,
+ "challenge_type" => "totp",
+ "code" => otp_token,
+ "client_id" => "xxx",
+ "client_secret" => "xxx"
+ })
+ |> json_response(400)
+
+ assert response == %{"error" => "Invalid code"}
+ end
+ end
+
+ describe "challenge/recovery" do
+ setup %{conn: conn} do
+ app = insert(:oauth_app)
+ {:ok, conn: conn, app: app}
+ end
+
+ test "returns access token with valid code", %{conn: conn, app: app} do
+ otp_secret = TOTP.generate_secret()
+
+ [code | _] = backup_codes = BackupCodes.generate()
+
+ hashed_codes =
+ backup_codes
+ |> Enum.map(&Comeonin.Pbkdf2.hashpwsalt(&1))
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ backup_codes: hashed_codes,
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ mfa_token =
+ insert(:mfa_token,
+ user: user,
+ authorization: build(:oauth_authorization, app: app, scopes: ["write"])
+ )
+
+ response =
+ conn
+ |> post("/oauth/mfa/challenge", %{
+ "mfa_token" => mfa_token.token,
+ "challenge_type" => "recovery",
+ "code" => code,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+ |> json_response(:ok)
+
+ ap_id = user.ap_id
+
+ assert match?(
+ %{
+ "access_token" => _,
+ "expires_in" => 600,
+ "me" => ^ap_id,
+ "refresh_token" => _,
+ "scope" => "write",
+ "token_type" => "Bearer"
+ },
+ response
+ )
+
+ error_response =
+ conn
+ |> post("/oauth/mfa/challenge", %{
+ "mfa_token" => mfa_token.token,
+ "challenge_type" => "recovery",
+ "code" => code,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+ |> json_response(400)
+
+ assert error_response == %{"error" => "Invalid code"}
+ end
+ end
+end
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index f2f98d768..7a107584d 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
+ alias Pleroma.MFA
+ alias Pleroma.MFA.TOTP
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
@@ -604,6 +606,41 @@ test "redirects with oauth authorization, " <>
end
end
+ test "redirect to on two-factor auth page" do
+ otp_secret = TOTP.generate_secret()
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ app = insert(:oauth_app, scopes: ["read", "write", "follow"])
+
+ conn =
+ build_conn()
+ |> post("/oauth/authorize", %{
+ "authorization" => %{
+ "name" => user.nickname,
+ "password" => "test",
+ "client_id" => app.client_id,
+ "redirect_uri" => app.redirect_uris,
+ "scope" => "read write",
+ "state" => "statepassed"
+ }
+ })
+
+ result = html_response(conn, 200)
+
+ mfa_token = Repo.get_by(MFA.Token, user_id: user.id)
+ assert result =~ app.redirect_uris
+ assert result =~ "statepassed"
+ assert result =~ mfa_token.token
+ assert result =~ "Two-factor authentication"
+ end
+
test "returns 401 for wrong credentials", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app)
@@ -735,6 +772,46 @@ test "issues a token for `password` grant_type with valid credentials, with full
assert token.scopes == app.scopes
end
+ test "issues a mfa token for `password` grant_type, when MFA enabled" do
+ password = "testpassword"
+ otp_secret = TOTP.generate_secret()
+
+ user =
+ insert(:user,
+ password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ app = insert(:oauth_app, scopes: ["read", "write"])
+
+ response =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "password",
+ "username" => user.nickname,
+ "password" => password,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+ |> json_response(403)
+
+ assert match?(
+ %{
+ "supported_challenge_types" => "totp",
+ "mfa_token" => _,
+ "error" => "mfa_required"
+ },
+ response
+ )
+
+ token = Repo.get_by(MFA.Token, token: response["mfa_token"])
+ assert token.user_id == user.id
+ assert token.authorization_id
+ end
+
test "issues a token for request with HTTP basic auth client credentials" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
index 4246eb400..d343256fe 100644
--- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
@@ -8,212 +8,298 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
import Tesla.Mock
import Pleroma.Factory
- @emoji_dir_path Path.join(
- Pleroma.Config.get!([:instance, :static_dir]),
- "emoji"
- )
+ @emoji_path Path.join(
+ Pleroma.Config.get!([:instance, :static_dir]),
+ "emoji"
+ )
setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false)
- test "shared & non-shared pack information in list_packs is ok" do
- conn = build_conn()
- resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
-
- assert Map.has_key?(resp, "test_pack")
-
- pack = resp["test_pack"]
-
- assert Map.has_key?(pack["pack"], "download-sha256")
- assert pack["pack"]["can-download"]
-
- assert pack["files"] == %{"blank" => "blank.png"}
-
- # Non-shared pack
-
- assert Map.has_key?(resp, "test_pack_nonshared")
-
- pack = resp["test_pack_nonshared"]
-
- refute pack["pack"]["shared"]
- refute pack["pack"]["can-download"]
- end
-
- test "listing remote packs" do
- conn = build_conn()
-
- resp =
- build_conn()
- |> get(emoji_api_path(conn, :list_packs))
- |> json_response(200)
-
- mock(fn
- %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
- json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
-
- %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
- json(%{metadata: %{features: ["shareable_emoji_packs"]}})
-
- %{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} ->
- json(resp)
- end)
-
- assert conn
- |> post(emoji_api_path(conn, :list_from), %{instance_address: "https://example.com"})
- |> json_response(200) == resp
- end
-
- test "downloading a shared pack from download_shared" do
- conn = build_conn()
-
- resp =
- conn
- |> get(emoji_api_path(conn, :download_shared, "test_pack"))
- |> response(200)
-
- {:ok, arch} = :zip.unzip(resp, [:memory])
-
- assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end)
- assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
- end
-
- test "downloading shared & unshared packs from another instance, deleting them" do
- on_exit(fn ->
- File.rm_rf!("#{@emoji_dir_path}/test_pack2")
- File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2")
- end)
-
- mock(fn
- %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} ->
- json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]})
-
- %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
- json(%{metadata: %{features: []}})
-
- %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
- json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
-
- %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
- json(%{metadata: %{features: ["shareable_emoji_packs"]}})
-
- %{
- method: :get,
- url: "https://example.com/api/pleroma/emoji/packs/list"
- } ->
- conn = build_conn()
-
- conn
- |> get(emoji_api_path(conn, :list_packs))
- |> json_response(200)
- |> json()
-
- %{
- method: :get,
- url: "https://example.com/api/pleroma/emoji/packs/download_shared/test_pack"
- } ->
- conn = build_conn()
-
- conn
- |> get(emoji_api_path(conn, :download_shared, "test_pack"))
- |> response(200)
- |> text()
-
- %{
- method: :get,
- url: "https://nonshared-pack"
- } ->
- text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
- end)
-
+ setup do
admin = insert(:user, is_admin: true)
+ token = insert(:oauth_admin_token, user: admin)
- conn =
+ admin_conn =
build_conn()
|> assign(:user, admin)
- |> assign(:token, insert(:oauth_admin_token, user: admin, scopes: ["admin:write"]))
+ |> assign(:token, token)
- assert (conn
- |> put_req_header("content-type", "application/json")
- |> post(
- emoji_api_path(
- conn,
- :save_from
- ),
- %{
- instance_address: "https://old-instance",
- pack_name: "test_pack",
- as: "test_pack2"
- }
- |> Jason.encode!()
- )
- |> json_response(500))["error"] =~ "does not support"
-
- assert conn
- |> put_req_header("content-type", "application/json")
- |> post(
- emoji_api_path(
- conn,
- :save_from
- ),
- %{
- instance_address: "https://example.com",
- pack_name: "test_pack",
- as: "test_pack2"
- }
- |> Jason.encode!()
- )
- |> json_response(200) == "ok"
-
- assert File.exists?("#{@emoji_dir_path}/test_pack2/pack.json")
- assert File.exists?("#{@emoji_dir_path}/test_pack2/blank.png")
-
- assert conn
- |> delete(emoji_api_path(conn, :delete, "test_pack2"))
- |> json_response(200) == "ok"
-
- refute File.exists?("#{@emoji_dir_path}/test_pack2")
-
- # non-shared, downloaded from the fallback URL
-
- assert conn
- |> put_req_header("content-type", "application/json")
- |> post(
- emoji_api_path(
- conn,
- :save_from
- ),
- %{
- instance_address: "https://example.com",
- pack_name: "test_pack_nonshared",
- as: "test_pack_nonshared2"
- }
- |> Jason.encode!()
- )
- |> json_response(200) == "ok"
-
- assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/pack.json")
- assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/blank.png")
-
- assert conn
- |> delete(emoji_api_path(conn, :delete, "test_pack_nonshared2"))
- |> json_response(200) == "ok"
-
- refute File.exists?("#{@emoji_dir_path}/test_pack_nonshared2")
+ Pleroma.Emoji.reload()
+ {:ok, %{admin_conn: admin_conn}}
end
- describe "updating pack metadata" do
+ test "GET /api/pleroma/emoji/packs", %{conn: conn} do
+ resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200)
+
+ shared = resp["test_pack"]
+ assert shared["files"] == %{"blank" => "blank.png"}
+ assert Map.has_key?(shared["pack"], "download-sha256")
+ assert shared["pack"]["can-download"]
+ assert shared["pack"]["share-files"]
+
+ non_shared = resp["test_pack_nonshared"]
+ assert non_shared["pack"]["share-files"] == false
+ assert non_shared["pack"]["can-download"] == false
+ end
+
+ describe "GET /api/pleroma/emoji/packs/remote" do
+ test "shareable instance", %{admin_conn: admin_conn, conn: conn} do
+ resp =
+ conn
+ |> get("/api/pleroma/emoji/packs")
+ |> json_response(200)
+
+ mock(fn
+ %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: ["shareable_emoji_packs"]}})
+
+ %{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} ->
+ json(resp)
+ end)
+
+ assert admin_conn
+ |> get("/api/pleroma/emoji/packs/remote", %{
+ url: "https://example.com"
+ })
+ |> json_response(200) == resp
+ end
+
+ test "non shareable instance", %{admin_conn: admin_conn} do
+ mock(fn
+ %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: []}})
+ end)
+
+ assert admin_conn
+ |> get("/api/pleroma/emoji/packs/remote", %{url: "https://example.com"})
+ |> json_response(500) == %{
+ "error" => "The requested instance does not support sharing emoji packs"
+ }
+ end
+ end
+
+ describe "GET /api/pleroma/emoji/packs/:name/archive" do
+ test "download shared pack", %{conn: conn} do
+ resp =
+ conn
+ |> get("/api/pleroma/emoji/packs/test_pack/archive")
+ |> response(200)
+
+ {:ok, arch} = :zip.unzip(resp, [:memory])
+
+ assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end)
+ assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
+ end
+
+ test "non existing pack", %{conn: conn} do
+ assert conn
+ |> get("/api/pleroma/emoji/packs/test_pack_for_import/archive")
+ |> json_response(:not_found) == %{
+ "error" => "Pack test_pack_for_import does not exist"
+ }
+ end
+
+ test "non downloadable pack", %{conn: conn} do
+ assert conn
+ |> get("/api/pleroma/emoji/packs/test_pack_nonshared/archive")
+ |> json_response(:forbidden) == %{
+ "error" =>
+ "Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
+ }
+ end
+ end
+
+ describe "POST /api/pleroma/emoji/packs/download" do
+ test "shared pack from remote and non shared from fallback-src", %{
+ admin_conn: admin_conn,
+ conn: conn
+ } do
+ mock(fn
+ %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: ["shareable_emoji_packs"]}})
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/test_pack"
+ } ->
+ conn
+ |> get("/api/pleroma/emoji/packs/test_pack")
+ |> json_response(200)
+ |> json()
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/test_pack/archive"
+ } ->
+ conn
+ |> get("/api/pleroma/emoji/packs/test_pack/archive")
+ |> response(200)
+ |> text()
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/test_pack_nonshared"
+ } ->
+ conn
+ |> get("/api/pleroma/emoji/packs/test_pack_nonshared")
+ |> json_response(200)
+ |> json()
+
+ %{
+ method: :get,
+ url: "https://nonshared-pack"
+ } ->
+ text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip"))
+ end)
+
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/download", %{
+ url: "https://example.com",
+ name: "test_pack",
+ as: "test_pack2"
+ })
+ |> json_response(200) == "ok"
+
+ assert File.exists?("#{@emoji_path}/test_pack2/pack.json")
+ assert File.exists?("#{@emoji_path}/test_pack2/blank.png")
+
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/test_pack2")
+ |> json_response(200) == "ok"
+
+ refute File.exists?("#{@emoji_path}/test_pack2")
+
+ assert admin_conn
+ |> post(
+ "/api/pleroma/emoji/packs/download",
+ %{
+ url: "https://example.com",
+ name: "test_pack_nonshared",
+ as: "test_pack_nonshared2"
+ }
+ )
+ |> json_response(200) == "ok"
+
+ assert File.exists?("#{@emoji_path}/test_pack_nonshared2/pack.json")
+ assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png")
+
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/test_pack_nonshared2")
+ |> json_response(200) == "ok"
+
+ refute File.exists?("#{@emoji_path}/test_pack_nonshared2")
+ end
+
+ test "nonshareable instance", %{admin_conn: admin_conn} do
+ mock(fn
+ %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: []}})
+ end)
+
+ assert admin_conn
+ |> post(
+ "/api/pleroma/emoji/packs/download",
+ %{
+ url: "https://old-instance",
+ name: "test_pack",
+ as: "test_pack2"
+ }
+ )
+ |> json_response(500) == %{
+ "error" => "The requested instance does not support sharing emoji packs"
+ }
+ end
+
+ test "checksum fail", %{admin_conn: admin_conn} do
+ mock(fn
+ %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: ["shareable_emoji_packs"]}})
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/pack_bad_sha"
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: Pleroma.Emoji.Pack.load_pack("pack_bad_sha") |> Jason.encode!()
+ }
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/pack_bad_sha/archive"
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip")
+ }
+ end)
+
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/download", %{
+ url: "https://example.com",
+ name: "pack_bad_sha",
+ as: "pack_bad_sha2"
+ })
+ |> json_response(:internal_server_error) == %{
+ "error" => "SHA256 for the pack doesn't match the one sent by the server"
+ }
+ end
+
+ test "other error", %{admin_conn: admin_conn} do
+ mock(fn
+ %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
+ json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
+
+ %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
+ json(%{metadata: %{features: ["shareable_emoji_packs"]}})
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/test_pack"
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: Pleroma.Emoji.Pack.load_pack("test_pack") |> Jason.encode!()
+ }
+ end)
+
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/download", %{
+ url: "https://example.com",
+ name: "test_pack",
+ as: "test_pack2"
+ })
+ |> json_response(:internal_server_error) == %{
+ "error" =>
+ "The pack was not set as shared and there is no fallback src to download from"
+ }
+ end
+ end
+
+ describe "PATCH /api/pleroma/emoji/packs/:name" do
setup do
- pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
+ pack_file = "#{@emoji_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
on_exit(fn ->
File.write!(pack_file, original_content)
end)
- admin = insert(:user, is_admin: true)
- %{conn: conn} = oauth_access(["admin:write"], user: admin)
-
{:ok,
- admin: admin,
- conn: conn,
pack_file: pack_file,
new_data: %{
"license" => "Test license changed",
@@ -224,15 +310,8 @@ test "downloading shared & unshared packs from another instance, deleting them"
end
test "for a pack without a fallback source", ctx do
- conn = ctx[:conn]
-
- assert conn
- |> post(
- emoji_api_path(conn, :update_metadata, "test_pack"),
- %{
- "new_data" => ctx[:new_data]
- }
- )
+ assert ctx[:admin_conn]
+ |> patch("/api/pleroma/emoji/packs/test_pack", %{"metadata" => ctx[:new_data]})
|> json_response(200) == ctx[:new_data]
assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data]
@@ -244,7 +323,7 @@ test "for a pack with a fallback source", ctx do
method: :get,
url: "https://nonshared-pack"
} ->
- text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
+ text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip"))
end)
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
@@ -256,15 +335,8 @@ test "for a pack with a fallback source", ctx do
"74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF"
)
- conn = ctx[:conn]
-
- assert conn
- |> post(
- emoji_api_path(conn, :update_metadata, "test_pack"),
- %{
- "new_data" => new_data
- }
- )
+ assert ctx[:admin_conn]
+ |> patch("/api/pleroma/emoji/packs/test_pack", %{metadata: new_data})
|> json_response(200) == new_data_with_sha
assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha
@@ -282,181 +354,377 @@ test "when the fallback source doesn't have all the files", ctx do
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
- conn = ctx[:conn]
-
- assert (conn
- |> post(
- emoji_api_path(conn, :update_metadata, "test_pack"),
- %{
- "new_data" => new_data
- }
- )
- |> json_response(:bad_request))["error"] =~ "does not have all"
+ assert ctx[:admin_conn]
+ |> patch("/api/pleroma/emoji/packs/test_pack", %{metadata: new_data})
+ |> json_response(:bad_request) == %{
+ "error" => "The fallback archive does not have all files specified in pack.json"
+ }
end
end
- test "updating pack files" do
- pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
- original_content = File.read!(pack_file)
+ describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/:name/files" do
+ setup do
+ pack_file = "#{@emoji_path}/test_pack/pack.json"
+ original_content = File.read!(pack_file)
- on_exit(fn ->
- File.write!(pack_file, original_content)
+ on_exit(fn ->
+ File.write!(pack_file, original_content)
+ end)
- File.rm_rf!("#{@emoji_dir_path}/test_pack/blank_url.png")
- File.rm_rf!("#{@emoji_dir_path}/test_pack/dir")
- File.rm_rf!("#{@emoji_dir_path}/test_pack/dir_2")
- end)
+ :ok
+ end
- admin = insert(:user, is_admin: true)
- %{conn: conn} = oauth_access(["admin:write"], user: admin)
+ test "create shortcode exists", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank",
+ filename: "dir/blank.png",
+ file: %Plug.Upload{
+ filename: "blank.png",
+ path: "#{@emoji_path}/test_pack/blank.png"
+ }
+ })
+ |> json_response(:conflict) == %{
+ "error" => "An emoji with the \"blank\" shortcode already exists"
+ }
+ end
- same_name = %{
- "action" => "add",
- "shortcode" => "blank",
- "filename" => "dir/blank.png",
- "file" => %Plug.Upload{
- filename: "blank.png",
- path: "#{@emoji_dir_path}/test_pack/blank.png"
- }
- }
+ test "don't rewrite old emoji", %{admin_conn: admin_conn} do
+ on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end)
- different_name = %{same_name | "shortcode" => "blank_2"}
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank2",
+ filename: "dir/blank.png",
+ file: %Plug.Upload{
+ filename: "blank.png",
+ path: "#{@emoji_path}/test_pack/blank.png"
+ }
+ })
+ |> json_response(200) == %{"blank" => "blank.png", "blank2" => "dir/blank.png"}
- assert (conn
- |> post(emoji_api_path(conn, :update_file, "test_pack"), same_name)
- |> json_response(:conflict))["error"] =~ "already exists"
+ assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png")
- assert conn
- |> post(emoji_api_path(conn, :update_file, "test_pack"), different_name)
- |> json_response(200) == %{"blank" => "blank.png", "blank_2" => "dir/blank.png"}
+ assert admin_conn
+ |> patch("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank",
+ new_shortcode: "blank2",
+ new_filename: "dir_2/blank_3.png"
+ })
+ |> json_response(:conflict) == %{
+ "error" =>
+ "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option"
+ }
+ end
- assert File.exists?("#{@emoji_dir_path}/test_pack/dir/blank.png")
+ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do
+ on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end)
- assert conn
- |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
- "action" => "update",
- "shortcode" => "blank_2",
- "new_shortcode" => "blank_3",
- "new_filename" => "dir_2/blank_3.png"
- })
- |> json_response(200) == %{"blank" => "blank.png", "blank_3" => "dir_2/blank_3.png"}
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank2",
+ filename: "dir/blank.png",
+ file: %Plug.Upload{
+ filename: "blank.png",
+ path: "#{@emoji_path}/test_pack/blank.png"
+ }
+ })
+ |> json_response(200) == %{"blank" => "blank.png", "blank2" => "dir/blank.png"}
- refute File.exists?("#{@emoji_dir_path}/test_pack/dir/")
- assert File.exists?("#{@emoji_dir_path}/test_pack/dir_2/blank_3.png")
+ assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png")
- assert conn
- |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
- "action" => "remove",
- "shortcode" => "blank_3"
- })
- |> json_response(200) == %{"blank" => "blank.png"}
+ assert admin_conn
+ |> patch("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank2",
+ new_shortcode: "blank3",
+ new_filename: "dir_2/blank_3.png",
+ force: true
+ })
+ |> json_response(200) == %{
+ "blank" => "blank.png",
+ "blank3" => "dir_2/blank_3.png"
+ }
- refute File.exists?("#{@emoji_dir_path}/test_pack/dir_2/")
+ assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png")
+ end
- mock(fn
- %{
- method: :get,
- url: "https://test-blank/blank_url.png"
- } ->
- text(File.read!("#{@emoji_dir_path}/test_pack/blank.png"))
- end)
+ test "with empty filename", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank2",
+ filename: "",
+ file: %Plug.Upload{
+ filename: "blank.png",
+ path: "#{@emoji_path}/test_pack/blank.png"
+ }
+ })
+ |> json_response(:bad_request) == %{
+ "error" => "pack name, shortcode or filename cannot be empty"
+ }
+ end
- # The name should be inferred from the URL ending
- from_url = %{
- "action" => "add",
- "shortcode" => "blank_url",
- "file" => "https://test-blank/blank_url.png"
- }
+ test "add file with not loaded pack", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/not_loaded/files", %{
+ shortcode: "blank2",
+ filename: "dir/blank.png",
+ file: %Plug.Upload{
+ filename: "blank.png",
+ path: "#{@emoji_path}/test_pack/blank.png"
+ }
+ })
+ |> json_response(:bad_request) == %{
+ "error" => "pack \"not_loaded\" is not found"
+ }
+ end
- assert conn
- |> post(emoji_api_path(conn, :update_file, "test_pack"), from_url)
- |> json_response(200) == %{
- "blank" => "blank.png",
- "blank_url" => "blank_url.png"
- }
+ test "remove file with not loaded pack", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/not_loaded/files", %{shortcode: "blank3"})
+ |> json_response(:bad_request) == %{"error" => "pack \"not_loaded\" is not found"}
+ end
- assert File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
+ test "remove file with empty shortcode", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/not_loaded/files", %{shortcode: ""})
+ |> json_response(:bad_request) == %{
+ "error" => "pack name or shortcode cannot be empty"
+ }
+ end
- assert conn
- |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
- "action" => "remove",
- "shortcode" => "blank_url"
- })
- |> json_response(200) == %{"blank" => "blank.png"}
+ test "update file with not loaded pack", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> patch("/api/pleroma/emoji/packs/not_loaded/files", %{
+ shortcode: "blank4",
+ new_shortcode: "blank3",
+ new_filename: "dir_2/blank_3.png"
+ })
+ |> json_response(:bad_request) == %{"error" => "pack \"not_loaded\" is not found"}
+ end
- refute File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
+ test "new with shortcode as file with update", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank4",
+ filename: "dir/blank.png",
+ file: %Plug.Upload{
+ filename: "blank.png",
+ path: "#{@emoji_path}/test_pack/blank.png"
+ }
+ })
+ |> json_response(200) == %{"blank" => "blank.png", "blank4" => "dir/blank.png"}
+
+ assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png")
+
+ assert admin_conn
+ |> patch("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank4",
+ new_shortcode: "blank3",
+ new_filename: "dir_2/blank_3.png"
+ })
+ |> json_response(200) == %{"blank3" => "dir_2/blank_3.png", "blank" => "blank.png"}
+
+ refute File.exists?("#{@emoji_path}/test_pack/dir/")
+ assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png")
+
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/test_pack/files", %{shortcode: "blank3"})
+ |> json_response(200) == %{"blank" => "blank.png"}
+
+ refute File.exists?("#{@emoji_path}/test_pack/dir_2/")
+
+ on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end)
+ end
+
+ test "new with shortcode from url", %{admin_conn: admin_conn} do
+ mock(fn
+ %{
+ method: :get,
+ url: "https://test-blank/blank_url.png"
+ } ->
+ text(File.read!("#{@emoji_path}/test_pack/blank.png"))
+ end)
+
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank_url",
+ file: "https://test-blank/blank_url.png"
+ })
+ |> json_response(200) == %{
+ "blank_url" => "blank_url.png",
+ "blank" => "blank.png"
+ }
+
+ assert File.exists?("#{@emoji_path}/test_pack/blank_url.png")
+
+ on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end)
+ end
+
+ test "new without shortcode", %{admin_conn: admin_conn} do
+ on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end)
+
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_pack/files", %{
+ file: %Plug.Upload{
+ filename: "shortcode.png",
+ path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png"
+ }
+ })
+ |> json_response(200) == %{"shortcode" => "shortcode.png", "blank" => "blank.png"}
+ end
+
+ test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/test_pack/files", %{shortcode: "blank2"})
+ |> json_response(:bad_request) == %{"error" => "Emoji \"blank2\" does not exist"}
+ end
+
+ test "update non existing emoji", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> patch("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank2",
+ new_shortcode: "blank3",
+ new_filename: "dir_2/blank_3.png"
+ })
+ |> json_response(:bad_request) == %{"error" => "Emoji \"blank2\" does not exist"}
+ end
+
+ test "update with empty shortcode", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> patch("/api/pleroma/emoji/packs/test_pack/files", %{
+ shortcode: "blank",
+ new_filename: "dir_2/blank_3.png"
+ })
+ |> json_response(:bad_request) == %{
+ "error" => "new_shortcode or new_filename cannot be empty"
+ }
+ end
end
- test "creating and deleting a pack" do
- on_exit(fn ->
- File.rm_rf!("#{@emoji_dir_path}/test_created")
- end)
+ describe "POST/DELETE /api/pleroma/emoji/packs/:name" do
+ test "creating and deleting a pack", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_created")
+ |> json_response(200) == "ok"
- admin = insert(:user, is_admin: true)
- %{conn: conn} = oauth_access(["admin:write"], user: admin)
+ assert File.exists?("#{@emoji_path}/test_created/pack.json")
- assert conn
- |> put_req_header("content-type", "application/json")
- |> put(
- emoji_api_path(
- conn,
- :create,
- "test_created"
- )
- )
- |> json_response(200) == "ok"
+ assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{
+ "pack" => %{},
+ "files" => %{}
+ }
- assert File.exists?("#{@emoji_dir_path}/test_created/pack.json")
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/test_created")
+ |> json_response(200) == "ok"
- assert Jason.decode!(File.read!("#{@emoji_dir_path}/test_created/pack.json")) == %{
- "pack" => %{},
- "files" => %{}
- }
+ refute File.exists?("#{@emoji_path}/test_created/pack.json")
+ end
- assert conn
- |> delete(emoji_api_path(conn, :delete, "test_created"))
- |> json_response(200) == "ok"
+ test "if pack exists", %{admin_conn: admin_conn} do
+ path = Path.join(@emoji_path, "test_created")
+ File.mkdir(path)
+ pack_file = Jason.encode!(%{files: %{}, pack: %{}})
+ File.write!(Path.join(path, "pack.json"), pack_file)
- refute File.exists?("#{@emoji_dir_path}/test_created/pack.json")
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/test_created")
+ |> json_response(:conflict) == %{
+ "error" => "A pack named \"test_created\" already exists"
+ }
+
+ on_exit(fn -> File.rm_rf(path) end)
+ end
+
+ test "with empty name", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> post("/api/pleroma/emoji/packs/ ")
+ |> json_response(:bad_request) == %{"error" => "pack name cannot be empty"}
+ end
end
- test "filesystem import" do
+ test "deleting nonexisting pack", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/non_existing")
+ |> json_response(:not_found) == %{"error" => "Pack non_existing does not exist"}
+ end
+
+ test "deleting with empty name", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> delete("/api/pleroma/emoji/packs/ ")
+ |> json_response(:bad_request) == %{"error" => "pack name cannot be empty"}
+ end
+
+ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do
on_exit(fn ->
- File.rm!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt")
- File.rm!("#{@emoji_dir_path}/test_pack_for_import/pack.json")
+ File.rm!("#{@emoji_path}/test_pack_for_import/emoji.txt")
+ File.rm!("#{@emoji_path}/test_pack_for_import/pack.json")
end)
- conn = build_conn()
- resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+ resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200)
refute Map.has_key?(resp, "test_pack_for_import")
- admin = insert(:user, is_admin: true)
- %{conn: conn} = oauth_access(["admin:write"], user: admin)
-
- assert conn
- |> post(emoji_api_path(conn, :import_from_fs))
+ assert admin_conn
+ |> get("/api/pleroma/emoji/packs/import")
|> json_response(200) == ["test_pack_for_import"]
- resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+ resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200)
assert resp["test_pack_for_import"]["files"] == %{"blank" => "blank.png"}
- File.rm!("#{@emoji_dir_path}/test_pack_for_import/pack.json")
- refute File.exists?("#{@emoji_dir_path}/test_pack_for_import/pack.json")
+ File.rm!("#{@emoji_path}/test_pack_for_import/pack.json")
+ refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json")
- emoji_txt_content = "blank, blank.png, Fun\n\nblank2, blank.png"
+ emoji_txt_content = """
+ blank, blank.png, Fun
+ blank2, blank.png
+ foo, /emoji/test_pack_for_import/blank.png
+ bar
+ """
- File.write!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt", emoji_txt_content)
+ File.write!("#{@emoji_path}/test_pack_for_import/emoji.txt", emoji_txt_content)
- assert conn
- |> post(emoji_api_path(conn, :import_from_fs))
+ assert admin_conn
+ |> get("/api/pleroma/emoji/packs/import")
|> json_response(200) == ["test_pack_for_import"]
- resp = build_conn() |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+ resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200)
assert resp["test_pack_for_import"]["files"] == %{
"blank" => "blank.png",
- "blank2" => "blank.png"
+ "blank2" => "blank.png",
+ "foo" => "blank.png"
}
end
+
+ describe "GET /api/pleroma/emoji/packs/:name" do
+ test "shows pack.json", %{conn: conn} do
+ assert %{
+ "files" => %{"blank" => "blank.png"},
+ "pack" => %{
+ "can-download" => true,
+ "description" => "Test description",
+ "download-sha256" => _,
+ "homepage" => "https://pleroma.social",
+ "license" => "Test license",
+ "share-files" => true
+ }
+ } =
+ conn
+ |> get("/api/pleroma/emoji/packs/test_pack")
+ |> json_response(200)
+ end
+
+ test "non existing pack", %{conn: conn} do
+ assert conn
+ |> get("/api/pleroma/emoji/packs/non_existing")
+ |> json_response(:not_found) == %{"error" => "Pack non_existing does not exist"}
+ end
+
+ test "error name", %{conn: conn} do
+ assert conn
+ |> get("/api/pleroma/emoji/packs/ ")
+ |> json_response(:bad_request) == %{"error" => "pack name cannot be empty"}
+ end
+ end
end
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
index 61a1689b9..43f1b154d 100644
--- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
@@ -3,12 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
+ use Oban.Testing, repo: Pleroma.Repo
use Pleroma.Web.ConnCase
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@@ -41,7 +43,9 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
- {:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+ {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+
+ ObanHelpers.perform_all()
result =
conn
@@ -52,7 +56,9 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
assert %{"id" => id} = json_response(result, 200)
assert to_string(activity.id) == id
- object = Object.normalize(activity)
+ ObanHelpers.perform_all()
+
+ object = Object.get_by_ap_id(activity.data["object"])
assert object.data["reaction_count"] == 0
end
@@ -71,8 +77,8 @@ test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
assert result == []
- {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
- {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")
User.perform(:delete, doomed_user)
@@ -109,8 +115,8 @@ test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
assert result == []
- {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
- {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
result =
conn
diff --git a/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs b/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs
new file mode 100644
index 000000000..d23d08a00
--- /dev/null
+++ b/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs
@@ -0,0 +1,260 @@
+defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do
+ use Pleroma.Web.ConnCase
+
+ import Pleroma.Factory
+ alias Pleroma.MFA.Settings
+ alias Pleroma.MFA.TOTP
+
+ describe "GET /api/pleroma/accounts/mfa/settings" do
+ test "returns user mfa settings for new user", %{conn: conn} do
+ token = insert(:oauth_token, scopes: ["read", "follow"])
+ token2 = insert(:oauth_token, scopes: ["write"])
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> get("/api/pleroma/accounts/mfa")
+ |> json_response(:ok) == %{
+ "settings" => %{"enabled" => false, "totp" => false}
+ }
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token2.token}")
+ |> get("/api/pleroma/accounts/mfa")
+ |> json_response(403) == %{
+ "error" => "Insufficient permissions: read:security."
+ }
+ end
+
+ test "returns user mfa settings with enabled totp", %{conn: conn} do
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %Settings{
+ enabled: true,
+ totp: %Settings.TOTP{secret: "XXX", delivery_type: "app", confirmed: true}
+ }
+ )
+
+ token = insert(:oauth_token, scopes: ["read", "follow"], user: user)
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> get("/api/pleroma/accounts/mfa")
+ |> json_response(:ok) == %{
+ "settings" => %{"enabled" => true, "totp" => true}
+ }
+ end
+ end
+
+ describe "GET /api/pleroma/accounts/mfa/backup_codes" do
+ test "returns backup codes", %{conn: conn} do
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %Settings{
+ backup_codes: ["1", "2", "3"],
+ totp: %Settings.TOTP{secret: "secret"}
+ }
+ )
+
+ token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
+ token2 = insert(:oauth_token, scopes: ["read"])
+
+ response =
+ conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> get("/api/pleroma/accounts/mfa/backup_codes")
+ |> json_response(:ok)
+
+ assert [<<_::bytes-size(6)>>, <<_::bytes-size(6)>>] = response["codes"]
+ user = refresh_record(user)
+ mfa_settings = user.multi_factor_authentication_settings
+ assert mfa_settings.totp.secret == "secret"
+ refute mfa_settings.backup_codes == ["1", "2", "3"]
+ refute mfa_settings.backup_codes == []
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token2.token}")
+ |> get("/api/pleroma/accounts/mfa/backup_codes")
+ |> json_response(403) == %{
+ "error" => "Insufficient permissions: write:security."
+ }
+ end
+ end
+
+ describe "GET /api/pleroma/accounts/mfa/setup/totp" do
+ test "return errors when method is invalid", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
+
+ response =
+ conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> get("/api/pleroma/accounts/mfa/setup/torf")
+ |> json_response(400)
+
+ assert response == %{"error" => "undefined method"}
+ end
+
+ test "returns key and provisioning_uri", %{conn: conn} do
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %Settings{backup_codes: ["1", "2", "3"]}
+ )
+
+ token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
+ token2 = insert(:oauth_token, scopes: ["read"])
+
+ response =
+ conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> get("/api/pleroma/accounts/mfa/setup/totp")
+ |> json_response(:ok)
+
+ user = refresh_record(user)
+ mfa_settings = user.multi_factor_authentication_settings
+ secret = mfa_settings.totp.secret
+ refute mfa_settings.enabled
+ assert mfa_settings.backup_codes == ["1", "2", "3"]
+
+ assert response == %{
+ "key" => secret,
+ "provisioning_uri" => TOTP.provisioning_uri(secret, "#{user.email}")
+ }
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token2.token}")
+ |> get("/api/pleroma/accounts/mfa/setup/totp")
+ |> json_response(403) == %{
+ "error" => "Insufficient permissions: write:security."
+ }
+ end
+ end
+
+ describe "GET /api/pleroma/accounts/mfa/confirm/totp" do
+ test "returns success result", %{conn: conn} do
+ secret = TOTP.generate_secret()
+ code = TOTP.generate_token(secret)
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %Settings{
+ backup_codes: ["1", "2", "3"],
+ totp: %Settings.TOTP{secret: secret}
+ }
+ )
+
+ token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
+ token2 = insert(:oauth_token, scopes: ["read"])
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code})
+ |> json_response(:ok)
+
+ settings = refresh_record(user).multi_factor_authentication_settings
+ assert settings.enabled
+ assert settings.totp.secret == secret
+ assert settings.totp.confirmed
+ assert settings.backup_codes == ["1", "2", "3"]
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token2.token}")
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code})
+ |> json_response(403) == %{
+ "error" => "Insufficient permissions: write:security."
+ }
+ end
+
+ test "returns error if password incorrect", %{conn: conn} do
+ secret = TOTP.generate_secret()
+ code = TOTP.generate_token(secret)
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %Settings{
+ backup_codes: ["1", "2", "3"],
+ totp: %Settings.TOTP{secret: secret}
+ }
+ )
+
+ token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
+
+ response =
+ conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "xxx", code: code})
+ |> json_response(422)
+
+ settings = refresh_record(user).multi_factor_authentication_settings
+ refute settings.enabled
+ refute settings.totp.confirmed
+ assert settings.backup_codes == ["1", "2", "3"]
+ assert response == %{"error" => "Invalid password."}
+ end
+
+ test "returns error if code incorrect", %{conn: conn} do
+ secret = TOTP.generate_secret()
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %Settings{
+ backup_codes: ["1", "2", "3"],
+ totp: %Settings.TOTP{secret: secret}
+ }
+ )
+
+ token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
+ token2 = insert(:oauth_token, scopes: ["read"])
+
+ response =
+ conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"})
+ |> json_response(422)
+
+ settings = refresh_record(user).multi_factor_authentication_settings
+ refute settings.enabled
+ refute settings.totp.confirmed
+ assert settings.backup_codes == ["1", "2", "3"]
+ assert response == %{"error" => "invalid_token"}
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token2.token}")
+ |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"})
+ |> json_response(403) == %{
+ "error" => "Insufficient permissions: write:security."
+ }
+ end
+ end
+
+ describe "DELETE /api/pleroma/accounts/mfa/totp" do
+ test "returns success result", %{conn: conn} do
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %Settings{
+ backup_codes: ["1", "2", "3"],
+ totp: %Settings.TOTP{secret: "secret"}
+ }
+ )
+
+ token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
+ token2 = insert(:oauth_token, scopes: ["read"])
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"})
+ |> json_response(:ok)
+
+ settings = refresh_record(user).multi_factor_authentication_settings
+ refute settings.enabled
+ assert settings.totp.secret == nil
+ refute settings.totp.confirmed
+
+ assert conn
+ |> put_req_header("authorization", "Bearer #{token2.token}")
+ |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"})
+ |> json_response(403) == %{
+ "error" => "Insufficient permissions: write:security."
+ }
+ end
+ end
+end
diff --git a/test/web/plugs/plug_test.exs b/test/web/plugs/plug_test.exs
new file mode 100644
index 000000000..943e484e7
--- /dev/null
+++ b/test/web/plugs/plug_test.exs
@@ -0,0 +1,91 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PlugTest do
+ @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`"
+
+ alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug
+ alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug
+ alias Pleroma.Plugs.PlugHelper
+
+ import Mock
+
+ use Pleroma.Web.ConnCase
+
+ describe "when plug is skipped, " do
+ setup_with_mocks(
+ [
+ {ExpectPublicOrAuthenticatedCheckPlug, [:passthrough], []}
+ ],
+ %{conn: conn}
+ ) do
+ conn = ExpectPublicOrAuthenticatedCheckPlug.skip_plug(conn)
+ %{conn: conn}
+ end
+
+ test "it neither adds plug to called plugs list nor calls `perform/2`, " <>
+ "regardless of :if_func / :unless_func options",
+ %{conn: conn} do
+ for opts <- [%{}, %{if_func: fn _ -> true end}, %{unless_func: fn _ -> false end}] do
+ ret_conn = ExpectPublicOrAuthenticatedCheckPlug.call(conn, opts)
+
+ refute called(ExpectPublicOrAuthenticatedCheckPlug.perform(:_, :_))
+ refute PlugHelper.plug_called?(ret_conn, ExpectPublicOrAuthenticatedCheckPlug)
+ end
+ end
+ end
+
+ describe "when plug is NOT skipped, " do
+ setup_with_mocks([{ExpectAuthenticatedCheckPlug, [:passthrough], []}]) do
+ :ok
+ end
+
+ test "with no pre-run checks, adds plug to called plugs list and calls `perform/2`", %{
+ conn: conn
+ } do
+ ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{})
+
+ assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_))
+ assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug)
+ end
+
+ test "when :if_func option is given, calls the plug only if provided function evals tru-ish",
+ %{conn: conn} do
+ ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> false end})
+
+ refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_))
+ refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug)
+
+ ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> true end})
+
+ assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_))
+ assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug)
+ end
+
+ test "if :unless_func option is given, calls the plug only if provided function evals falsy",
+ %{conn: conn} do
+ ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> true end})
+
+ refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_))
+ refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug)
+
+ ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> false end})
+
+ assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_))
+ assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug)
+ end
+
+ test "allows a plug to be called multiple times (even if it's in called plugs list)", %{
+ conn: conn
+ } do
+ conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value1})
+ assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value1}))
+
+ assert PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug)
+
+ conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value2})
+ assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value2}))
+ end
+ end
+end
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index b2664bf28..b855d72ba 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -193,7 +193,7 @@ test "renders title for create activity with direct visibility" do
end
describe "build_content/3" do
- test "returns info content for direct message with enabled privacy option" do
+ test "hides details for notifications when privacy option enabled" do
user = insert(:user, nickname: "Bob")
user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true})
@@ -209,12 +209,37 @@ test "returns info content for direct message with enabled privacy option" do
object = Object.normalize(activity)
assert Impl.build_content(notif, actor, object) == %{
- body: "@Bob",
- title: "New Direct Message"
+ body: "New Direct Message"
+ }
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "visibility" => "public",
+ "status" => "
"public",
+ "status" =>
+ "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
+ })
+
+ notif = insert(:notification, user: user2, activity: activity)
+
+ actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
+ object = Object.normalize(activity)
+
+ assert Impl.build_content(notif, actor, object) == %{
+ body:
+ "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...",
+ title: "New Mention"
+ }
+
+ {:ok, activity} = CommonAPI.favorite(user, activity.id)
+
+ notif = insert(:notification, user: user2, activity: activity)
+
+ actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
+ object = Object.normalize(activity)
+
+ assert Impl.build_content(notif, actor, object) == %{
+ body: "@Bob has favorited your post",
+ title: "New Favorite"
+ }
end
end
end
diff --git a/test/web/streamer/ping_test.exs b/test/web/streamer/ping_test.exs
deleted file mode 100644
index 5df6c1cc3..000000000
--- a/test/web/streamer/ping_test.exs
+++ /dev/null
@@ -1,36 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.PingTest do
- use Pleroma.DataCase
-
- import Pleroma.Factory
- alias Pleroma.Web.Streamer
-
- setup do
- start_supervised({Streamer.supervisor(), [ping_interval: 30]})
-
- :ok
- end
-
- describe "sockets" do
- setup do
- user = insert(:user)
- {:ok, %{user: user}}
- end
-
- test "it sends pings", %{user: user} do
- task =
- Task.async(fn ->
- assert_receive {:text, received_event}, 40
- assert_receive {:text, received_event}, 40
- assert_receive {:text, received_event}, 40
- end)
-
- Streamer.add_socket("public", %{transport_pid: task.pid, assigns: %{user: user}})
-
- Task.await(task)
- end
- end
-end
diff --git a/test/web/streamer/state_test.exs b/test/web/streamer/state_test.exs
deleted file mode 100644
index a755e75c0..000000000
--- a/test/web/streamer/state_test.exs
+++ /dev/null
@@ -1,54 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.StateTest do
- use Pleroma.DataCase
-
- import Pleroma.Factory
- alias Pleroma.Web.Streamer
- alias Pleroma.Web.Streamer.StreamerSocket
-
- @moduletag needs_streamer: true
-
- describe "sockets" do
- setup do
- user = insert(:user)
- user2 = insert(:user)
- {:ok, %{user: user, user2: user2}}
- end
-
- test "it can add a socket", %{user: user} do
- Streamer.add_socket("public", %{transport_pid: 1, assigns: %{user: user}})
-
- assert(%{"public" => [%StreamerSocket{transport_pid: 1}]} = Streamer.get_sockets())
- end
-
- test "it can add multiple sockets per user", %{user: user} do
- Streamer.add_socket("public", %{transport_pid: 1, assigns: %{user: user}})
- Streamer.add_socket("public", %{transport_pid: 2, assigns: %{user: user}})
-
- assert(
- %{
- "public" => [
- %StreamerSocket{transport_pid: 2},
- %StreamerSocket{transport_pid: 1}
- ]
- } = Streamer.get_sockets()
- )
- end
-
- test "it will not add a duplicate socket", %{user: user} do
- Streamer.add_socket("activity", %{transport_pid: 1, assigns: %{user: user}})
- Streamer.add_socket("activity", %{transport_pid: 1, assigns: %{user: user}})
-
- assert(
- %{
- "activity" => [
- %StreamerSocket{transport_pid: 1}
- ]
- } = Streamer.get_sockets()
- )
- end
- end
-end
diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs
index 8b8d8af6c..ee530f4e9 100644
--- a/test/web/streamer/streamer_test.exs
+++ b/test/web/streamer/streamer_test.exs
@@ -12,13 +12,9 @@ defmodule Pleroma.Web.StreamerTest do
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
- alias Pleroma.Web.Streamer.StreamerSocket
- alias Pleroma.Web.Streamer.Worker
@moduletag needs_streamer: true, capture_log: true
- @streamer_timeout 150
- @streamer_start_wait 10
setup do: clear_config([:instance, :skip_thread_containment])
describe "user streams" do
@@ -29,69 +25,35 @@ defmodule Pleroma.Web.StreamerTest do
end
test "it streams the user's post in the 'user' stream", %{user: user} do
- task =
- Task.async(fn ->
- assert_receive {:text, _}, @streamer_timeout
- end)
-
- Streamer.add_socket(
- "user",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
-
+ Streamer.add_socket("user", user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
-
- Streamer.stream("user", activity)
- Task.await(task)
+ assert_receive {:render_with_user, _, _, ^activity}
+ refute Streamer.filtered_by_user?(user, activity)
end
test "it streams boosts of the user in the 'user' stream", %{user: user} do
- task =
- Task.async(fn ->
- assert_receive {:text, _}, @streamer_timeout
- end)
-
- Streamer.add_socket(
- "user",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
+ Streamer.add_socket("user", user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
{:ok, announce, _} = CommonAPI.repeat(activity.id, user)
- Streamer.stream("user", announce)
- Task.await(task)
+ assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce}
+ refute Streamer.filtered_by_user?(user, announce)
end
test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do
- task =
- Task.async(fn ->
- assert_receive {:text, _}, @streamer_timeout
- end)
-
- Streamer.add_socket(
- "user",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
-
+ Streamer.add_socket("user", user)
Streamer.stream("user", notify)
- Task.await(task)
+ assert_receive {:render_with_user, _, _, ^notify}
+ refute Streamer.filtered_by_user?(user, notify)
end
test "it sends notify to in the 'user:notification' stream", %{user: user, notify: notify} do
- task =
- Task.async(fn ->
- assert_receive {:text, _}, @streamer_timeout
- end)
-
- Streamer.add_socket(
- "user:notification",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
-
+ Streamer.add_socket("user:notification", user)
Streamer.stream("user:notification", notify)
- Task.await(task)
+ assert_receive {:render_with_user, _, _, ^notify}
+ refute Streamer.filtered_by_user?(user, notify)
end
test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{
@@ -100,18 +62,12 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl
blocked = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked)
- task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
-
- Streamer.add_socket(
- "user:notification",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
+ Streamer.add_socket("user:notification", user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ":("})
- {:ok, notif} = CommonAPI.favorite(blocked, activity.id)
+ {:ok, _} = CommonAPI.favorite(blocked, activity.id)
- Streamer.stream("user:notification", notif)
- Task.await(task)
+ refute_receive _
end
test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{
@@ -119,45 +75,50 @@ test "it doesn't send notify to the 'user:notification' stream when a thread is
} do
user2 = insert(:user)
- task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
-
- Streamer.add_socket(
- "user:notification",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
-
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
- {:ok, activity} = CommonAPI.add_mute(user, activity)
- {:ok, notif} = CommonAPI.favorite(user2, activity.id)
+ {:ok, _} = CommonAPI.add_mute(user, activity)
- Streamer.stream("user:notification", notif)
- Task.await(task)
+ Streamer.add_socket("user:notification", user)
+
+ {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id)
+
+ refute_receive _
+ assert Streamer.filtered_by_user?(user, favorite_activity)
end
- test "it doesn't send notify to the 'user:notification' stream' when a domain is blocked", %{
+ test "it sends favorite to 'user:notification' stream'", %{
user: user
} do
user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"})
- task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
+ Streamer.add_socket("user:notification", user)
+ {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id)
- Streamer.add_socket(
- "user:notification",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
+ assert_receive {:render_with_user, _, "notification.json", notif}
+ assert notif.activity.id == favorite_activity.id
+ refute Streamer.filtered_by_user?(user, notif)
+ end
+
+ test "it doesn't send the 'user:notification' stream' when a domain is blocked", %{
+ user: user
+ } do
+ user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"})
{:ok, user} = User.block_domain(user, "hecking-lewd-place.com")
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
- {:ok, notif} = CommonAPI.favorite(user2, activity.id)
+ Streamer.add_socket("user:notification", user)
+ {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id)
- Streamer.stream("user:notification", notif)
- Task.await(task)
+ refute_receive _
+ assert Streamer.filtered_by_user?(user, favorite_activity)
end
test "it sends follow activities to the 'user:notification' stream", %{
user: user
} do
user_url = user.ap_id
+ user2 = insert(:user)
body =
File.read!("test/fixtures/users_mock/localhost.json")
@@ -169,79 +130,57 @@ test "it sends follow activities to the 'user:notification' stream", %{
%Tesla.Env{status: 200, body: body}
end)
- user2 = insert(:user)
- task = Task.async(fn -> assert_receive {:text, _}, @streamer_timeout end)
+ Streamer.add_socket("user:notification", user)
+ {:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user)
- Process.sleep(@streamer_start_wait)
-
- Streamer.add_socket(
- "user:notification",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
-
- {:ok, _follower, _followed, _activity} = CommonAPI.follow(user2, user)
-
- # We don't directly pipe the notification to the streamer as it's already
- # generated as a side effect of CommonAPI.follow().
- Task.await(task)
+ assert_receive {:render_with_user, _, "notification.json", notif}
+ assert notif.activity.id == follow_activity.id
+ refute Streamer.filtered_by_user?(user, notif)
end
end
- test "it sends to public" do
+ test "it sends to public authenticated" do
user = insert(:user)
other_user = insert(:user)
- task =
- Task.async(fn ->
- assert_receive {:text, _}, @streamer_timeout
- end)
+ Streamer.add_socket("public", other_user)
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: user
- }
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "Test"})
+ assert_receive {:render_with_user, _, _, ^activity}
+ refute Streamer.filtered_by_user?(user, activity)
+ end
+ test "works for deletions" do
+ user = insert(:user)
+ other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "Test"})
- topics = %{
- "public" => [fake_socket]
- }
+ Streamer.add_socket("public", user)
- Worker.push_to_socket(topics, "public", activity)
+ {:ok, _} = CommonAPI.delete(activity.id, other_user)
+ activity_id = activity.id
+ assert_receive {:text, event}
+ assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event)
+ end
- Task.await(task)
+ test "it sends to public unauthenticated" do
+ user = insert(:user)
- task =
- Task.async(fn ->
- expected_event =
- %{
- "event" => "delete",
- "payload" => activity.id
- }
- |> Jason.encode!()
+ Streamer.add_socket("public", nil)
- assert_receive {:text, received_event}, @streamer_timeout
- assert received_event == expected_event
- end)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "Test"})
+ activity_id = activity.id
+ assert_receive {:text, event}
+ assert %{"event" => "update", "payload" => payload} = Jason.decode!(event)
+ assert %{"id" => ^activity_id} = Jason.decode!(payload)
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: user
- }
-
- {:ok, activity} = CommonAPI.delete(activity.id, other_user)
-
- topics = %{
- "public" => [fake_socket]
- }
-
- Worker.push_to_socket(topics, "public", activity)
-
- Task.await(task)
+ {:ok, _} = CommonAPI.delete(activity.id, user)
+ assert_receive {:text, event}
+ assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event)
end
describe "thread_containment" do
- test "it doesn't send to user if recipients invalid and thread containment is enabled" do
+ test "it filters to user if recipients invalid and thread containment is enabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user)
user = insert(:user)
@@ -256,12 +195,10 @@ test "it doesn't send to user if recipients invalid and thread containment is en
)
)
- task = Task.async(fn -> refute_receive {:text, _}, 1_000 end)
- fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
- topics = %{"public" => [fake_socket]}
- Worker.push_to_socket(topics, "public", activity)
-
- Task.await(task)
+ Streamer.add_socket("public", user)
+ Streamer.stream("public", activity)
+ assert_receive {:render_with_user, _, _, ^activity}
+ assert Streamer.filtered_by_user?(user, activity)
end
test "it sends message if recipients invalid and thread containment is disabled" do
@@ -279,12 +216,11 @@ test "it sends message if recipients invalid and thread containment is disabled"
)
)
- task = Task.async(fn -> assert_receive {:text, _}, 1_000 end)
- fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
- topics = %{"public" => [fake_socket]}
- Worker.push_to_socket(topics, "public", activity)
+ Streamer.add_socket("public", user)
+ Streamer.stream("public", activity)
- Task.await(task)
+ assert_receive {:render_with_user, _, _, ^activity}
+ refute Streamer.filtered_by_user?(user, activity)
end
test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do
@@ -302,255 +238,168 @@ test "it sends message if recipients invalid and thread containment is enabled b
)
)
- task = Task.async(fn -> assert_receive {:text, _}, 1_000 end)
- fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
- topics = %{"public" => [fake_socket]}
- Worker.push_to_socket(topics, "public", activity)
+ Streamer.add_socket("public", user)
+ Streamer.stream("public", activity)
- Task.await(task)
+ assert_receive {:render_with_user, _, _, ^activity}
+ refute Streamer.filtered_by_user?(user, activity)
end
end
describe "blocks" do
- test "it doesn't send messages involving blocked users" do
+ test "it filters messages involving blocked users" do
user = insert(:user)
blocked_user = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked_user)
+ Streamer.add_socket("public", user)
{:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"})
-
- task =
- Task.async(fn ->
- refute_receive {:text, _}, 1_000
- end)
-
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: user
- }
-
- topics = %{
- "public" => [fake_socket]
- }
-
- Worker.push_to_socket(topics, "public", activity)
-
- Task.await(task)
+ assert_receive {:render_with_user, _, _, ^activity}
+ assert Streamer.filtered_by_user?(user, activity)
end
- test "it doesn't send messages transitively involving blocked users" do
+ test "it filters messages transitively involving blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
- task =
- Task.async(fn ->
- refute_receive {:text, _}, 1_000
- end)
-
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: blocker
- }
-
- topics = %{
- "public" => [fake_socket]
- }
+ Streamer.add_socket("public", blocker)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
- Worker.push_to_socket(topics, "public", activity_one)
+ assert_receive {:render_with_user, _, _, ^activity_one}
+ assert Streamer.filtered_by_user?(blocker, activity_one)
{:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
- Worker.push_to_socket(topics, "public", activity_two)
+ assert_receive {:render_with_user, _, _, ^activity_two}
+ assert Streamer.filtered_by_user?(blocker, activity_two)
{:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
- Worker.push_to_socket(topics, "public", activity_three)
-
- Task.await(task)
+ assert_receive {:render_with_user, _, _, ^activity_three}
+ assert Streamer.filtered_by_user?(blocker, activity_three)
end
end
- test "it doesn't send unwanted DMs to list" do
- user_a = insert(:user)
- user_b = insert(:user)
- user_c = insert(:user)
+ describe "lists" do
+ test "it doesn't send unwanted DMs to list" do
+ user_a = insert(:user)
+ user_b = insert(:user)
+ user_c = insert(:user)
- {:ok, user_a} = User.follow(user_a, user_b)
+ {:ok, user_a} = User.follow(user_a, user_b)
- {:ok, list} = List.create("Test", user_a)
- {:ok, list} = List.follow(list, user_b)
+ {:ok, list} = List.create("Test", user_a)
+ {:ok, list} = List.follow(list, user_b)
- {:ok, activity} =
- CommonAPI.post(user_b, %{
- "status" => "@#{user_c.nickname} Test",
- "visibility" => "direct"
- })
+ Streamer.add_socket("list:#{list.id}", user_a)
- task =
- Task.async(fn ->
- refute_receive {:text, _}, 1_000
- end)
+ {:ok, _activity} =
+ CommonAPI.post(user_b, %{
+ "status" => "@#{user_c.nickname} Test",
+ "visibility" => "direct"
+ })
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: user_a
- }
+ refute_receive _
+ end
- topics = %{
- "list:#{list.id}" => [fake_socket]
- }
+ test "it doesn't send unwanted private posts to list" do
+ user_a = insert(:user)
+ user_b = insert(:user)
- Worker.handle_call({:stream, "list", activity}, self(), topics)
+ {:ok, list} = List.create("Test", user_a)
+ {:ok, list} = List.follow(list, user_b)
- Task.await(task)
+ Streamer.add_socket("list:#{list.id}", user_a)
+
+ {:ok, _activity} =
+ CommonAPI.post(user_b, %{
+ "status" => "Test",
+ "visibility" => "private"
+ })
+
+ refute_receive _
+ end
+
+ test "it sends wanted private posts to list" do
+ user_a = insert(:user)
+ user_b = insert(:user)
+
+ {:ok, user_a} = User.follow(user_a, user_b)
+
+ {:ok, list} = List.create("Test", user_a)
+ {:ok, list} = List.follow(list, user_b)
+
+ Streamer.add_socket("list:#{list.id}", user_a)
+
+ {:ok, activity} =
+ CommonAPI.post(user_b, %{
+ "status" => "Test",
+ "visibility" => "private"
+ })
+
+ assert_receive {:render_with_user, _, _, ^activity}
+ refute Streamer.filtered_by_user?(user_a, activity)
+ end
end
- test "it doesn't send unwanted private posts to list" do
- user_a = insert(:user)
- user_b = insert(:user)
+ describe "muted reblogs" do
+ test "it filters muted reblogs" do
+ user1 = insert(:user)
+ user2 = insert(:user)
+ user3 = insert(:user)
+ CommonAPI.follow(user1, user2)
+ CommonAPI.hide_reblogs(user1, user2)
- {:ok, list} = List.create("Test", user_a)
- {:ok, list} = List.follow(list, user_b)
+ {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
- {:ok, activity} =
- CommonAPI.post(user_b, %{
- "status" => "Test",
- "visibility" => "private"
- })
+ Streamer.add_socket("user", user1)
+ {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2)
+ assert_receive {:render_with_user, _, _, ^announce_activity}
+ assert Streamer.filtered_by_user?(user1, announce_activity)
+ end
- task =
- Task.async(fn ->
- refute_receive {:text, _}, 1_000
- end)
+ test "it filters reblog notification for reblog-muted actors" do
+ user1 = insert(:user)
+ user2 = insert(:user)
+ CommonAPI.follow(user1, user2)
+ CommonAPI.hide_reblogs(user1, user2)
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: user_a
- }
+ {:ok, create_activity} = CommonAPI.post(user1, %{"status" => "I'm kawen"})
+ Streamer.add_socket("user", user1)
+ {:ok, _favorite_activity, _} = CommonAPI.repeat(create_activity.id, user2)
- topics = %{
- "list:#{list.id}" => [fake_socket]
- }
+ assert_receive {:render_with_user, _, "notification.json", notif}
+ assert Streamer.filtered_by_user?(user1, notif)
+ end
- Worker.handle_call({:stream, "list", activity}, self(), topics)
+ test "it send non-reblog notification for reblog-muted actors" do
+ user1 = insert(:user)
+ user2 = insert(:user)
+ CommonAPI.follow(user1, user2)
+ CommonAPI.hide_reblogs(user1, user2)
- Task.await(task)
+ {:ok, create_activity} = CommonAPI.post(user1, %{"status" => "I'm kawen"})
+ Streamer.add_socket("user", user1)
+ {:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id)
+
+ assert_receive {:render_with_user, _, "notification.json", notif}
+ refute Streamer.filtered_by_user?(user1, notif)
+ end
end
- test "it sends wanted private posts to list" do
- user_a = insert(:user)
- user_b = insert(:user)
-
- {:ok, user_a} = User.follow(user_a, user_b)
-
- {:ok, list} = List.create("Test", user_a)
- {:ok, list} = List.follow(list, user_b)
-
- {:ok, activity} =
- CommonAPI.post(user_b, %{
- "status" => "Test",
- "visibility" => "private"
- })
-
- task =
- Task.async(fn ->
- assert_receive {:text, _}, 1_000
- end)
-
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: user_a
- }
-
- Streamer.add_socket(
- "list:#{list.id}",
- fake_socket
- )
-
- Worker.handle_call({:stream, "list", activity}, self(), %{})
-
- Task.await(task)
- end
-
- test "it doesn't send muted reblogs" do
- user1 = insert(:user)
- user2 = insert(:user)
- user3 = insert(:user)
- CommonAPI.hide_reblogs(user1, user2)
-
- {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
- {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2)
-
- task =
- Task.async(fn ->
- refute_receive {:text, _}, 1_000
- end)
-
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: user1
- }
-
- topics = %{
- "public" => [fake_socket]
- }
-
- Worker.push_to_socket(topics, "public", announce_activity)
-
- Task.await(task)
- end
-
- test "it does send non-reblog notification for reblog-muted actors" do
- user1 = insert(:user)
- user2 = insert(:user)
- user3 = insert(:user)
- CommonAPI.hide_reblogs(user1, user2)
-
- {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
- {:ok, favorite_activity} = CommonAPI.favorite(user2, create_activity.id)
-
- task =
- Task.async(fn ->
- assert_receive {:text, _}, 1_000
- end)
-
- fake_socket = %StreamerSocket{
- transport_pid: task.pid,
- user: user1
- }
-
- topics = %{
- "public" => [fake_socket]
- }
-
- Worker.push_to_socket(topics, "public", favorite_activity)
-
- Task.await(task)
- end
-
- test "it doesn't send posts from muted threads" do
+ test "it filters posts from muted threads" do
user = insert(:user)
user2 = insert(:user)
+ Streamer.add_socket("user", user2)
{:ok, user2, user, _activity} = CommonAPI.follow(user2, user)
-
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
-
- {:ok, activity} = CommonAPI.add_mute(user2, activity)
-
- task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
-
- Streamer.add_socket(
- "user",
- %{transport_pid: task.pid, assigns: %{user: user2}}
- )
-
- Streamer.stream("user", activity)
- Task.await(task)
+ {:ok, _} = CommonAPI.add_mute(user2, activity)
+ assert_receive {:render_with_user, _, _, ^activity}
+ assert Streamer.filtered_by_user?(user2, activity)
end
describe "direct streams" do
@@ -562,22 +411,7 @@ test "it sends conversation update to the 'direct' stream", %{} do
user = insert(:user)
another_user = insert(:user)
- task =
- Task.async(fn ->
- assert_receive {:text, received_event}, @streamer_timeout
-
- assert %{"event" => "conversation", "payload" => received_payload} =
- Jason.decode!(received_event)
-
- assert %{"last_status" => last_status} = Jason.decode!(received_payload)
- [participation] = Participation.for_user(user)
- assert last_status["pleroma"]["direct_conversation_id"] == participation.id
- end)
-
- Streamer.add_socket(
- "direct",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
+ Streamer.add_socket("direct", user)
{:ok, _create_activity} =
CommonAPI.post(another_user, %{
@@ -585,42 +419,47 @@ test "it sends conversation update to the 'direct' stream", %{} do
"visibility" => "direct"
})
- Task.await(task)
+ assert_receive {:text, received_event}
+
+ assert %{"event" => "conversation", "payload" => received_payload} =
+ Jason.decode!(received_event)
+
+ assert %{"last_status" => last_status} = Jason.decode!(received_payload)
+ [participation] = Participation.for_user(user)
+ assert last_status["pleroma"]["direct_conversation_id"] == participation.id
end
test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted" do
user = insert(:user)
another_user = insert(:user)
+ Streamer.add_socket("direct", user)
+
{:ok, create_activity} =
CommonAPI.post(another_user, %{
"status" => "hi @#{user.nickname}",
"visibility" => "direct"
})
- task =
- Task.async(fn ->
- assert_receive {:text, received_event}, @streamer_timeout
- assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
+ create_activity_id = create_activity.id
+ assert_receive {:render_with_user, _, _, ^create_activity}
+ assert_receive {:text, received_conversation1}
+ assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
- refute_receive {:text, _}, @streamer_timeout
- end)
+ {:ok, _} = CommonAPI.delete(create_activity_id, another_user)
- Process.sleep(@streamer_start_wait)
+ assert_receive {:text, received_event}
- Streamer.add_socket(
- "direct",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
+ assert %{"event" => "delete", "payload" => ^create_activity_id} =
+ Jason.decode!(received_event)
- {:ok, _} = CommonAPI.delete(create_activity.id, another_user)
-
- Task.await(task)
+ refute_receive _
end
test "it sends conversation update to the 'direct' stream when a message is deleted" do
user = insert(:user)
another_user = insert(:user)
+ Streamer.add_socket("direct", user)
{:ok, create_activity} =
CommonAPI.post(another_user, %{
@@ -630,35 +469,30 @@ test "it sends conversation update to the 'direct' stream when a message is dele
{:ok, create_activity2} =
CommonAPI.post(another_user, %{
- "status" => "hi @#{user.nickname}",
+ "status" => "hi @#{user.nickname} 2",
"in_reply_to_status_id" => create_activity.id,
"visibility" => "direct"
})
- task =
- Task.async(fn ->
- assert_receive {:text, received_event}, @streamer_timeout
- assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
-
- assert_receive {:text, received_event}, @streamer_timeout
-
- assert %{"event" => "conversation", "payload" => received_payload} =
- Jason.decode!(received_event)
-
- assert %{"last_status" => last_status} = Jason.decode!(received_payload)
- assert last_status["id"] == to_string(create_activity.id)
- end)
-
- Process.sleep(@streamer_start_wait)
-
- Streamer.add_socket(
- "direct",
- %{transport_pid: task.pid, assigns: %{user: user}}
- )
+ assert_receive {:render_with_user, _, _, ^create_activity}
+ assert_receive {:render_with_user, _, _, ^create_activity2}
+ assert_receive {:text, received_conversation1}
+ assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
+ assert_receive {:text, received_conversation1}
+ assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
{:ok, _} = CommonAPI.delete(create_activity2.id, another_user)
- Task.await(task)
+ assert_receive {:text, received_event}
+ assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
+
+ assert_receive {:text, received_event}
+
+ assert %{"event" => "conversation", "payload" => received_payload} =
+ Jason.decode!(received_event)
+
+ assert %{"last_status" => last_status} = Jason.decode!(received_payload)
+ assert last_status["id"] == to_string(create_activity.id)
end
end
end
diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs
index 5ff8694a8..f7e54c26a 100644
--- a/test/web/twitter_api/remote_follow_controller_test.exs
+++ b/test/web/twitter_api/remote_follow_controller_test.exs
@@ -6,11 +6,14 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Config
+ alias Pleroma.MFA
+ alias Pleroma.MFA.TOTP
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import ExUnit.CaptureLog
import Pleroma.Factory
+ import Ecto.Query
setup do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -160,6 +163,119 @@ test "returns success result when user already in followers", %{conn: conn} do
end
end
+ describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do
+ test "render the MFA login form", %{conn: conn} do
+ otp_secret = TOTP.generate_secret()
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ user2 = insert(:user)
+
+ response =
+ conn
+ |> post(remote_follow_path(conn, :do_follow), %{
+ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
+ })
+ |> response(200)
+
+ mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id))
+
+ assert response =~ "Two-factor authentication"
+ assert response =~ "Authentication code"
+ assert response =~ mfa_token.token
+ refute user2.follower_address in User.following(user)
+ end
+
+ test "returns error when password is incorrect", %{conn: conn} do
+ otp_secret = TOTP.generate_secret()
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ user2 = insert(:user)
+
+ response =
+ conn
+ |> post(remote_follow_path(conn, :do_follow), %{
+ "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id}
+ })
+ |> response(200)
+
+ assert response =~ "Wrong username or password"
+ refute user2.follower_address in User.following(user)
+ end
+
+ test "follows", %{conn: conn} do
+ otp_secret = TOTP.generate_secret()
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ {:ok, %{token: token}} = MFA.Token.create_token(user)
+
+ user2 = insert(:user)
+ otp_token = TOTP.generate_token(otp_secret)
+
+ conn =
+ conn
+ |> post(
+ remote_follow_path(conn, :do_follow),
+ %{
+ "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id}
+ }
+ )
+
+ assert redirected_to(conn) == "/users/#{user2.id}"
+ assert user2.follower_address in User.following(user)
+ end
+
+ test "returns error when auth code is incorrect", %{conn: conn} do
+ otp_secret = TOTP.generate_secret()
+
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
+ }
+ )
+
+ {:ok, %{token: token}} = MFA.Token.create_token(user)
+
+ user2 = insert(:user)
+ otp_token = TOTP.generate_token(TOTP.generate_secret())
+
+ response =
+ conn
+ |> post(
+ remote_follow_path(conn, :do_follow),
+ %{
+ "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id}
+ }
+ )
+ |> response(200)
+
+ assert response =~ "Wrong authentication code"
+ refute user2.follower_address in User.following(user)
+ end
+ end
+
describe "POST /ostatus_subscribe - follow/2 without assigned user " do
test "follows", %{conn: conn} do
user = insert(:user)
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 7926a0757..368533292 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
test "it registers a new user and returns the user." do
data = %{
- :nickname => "lain",
+ :username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:password => "bear",
@@ -35,7 +35,7 @@ test "it registers a new user and returns the user." do
test "it registers a new user with empty string in bio and returns the user." do
data = %{
- :nickname => "lain",
+ :username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "",
@@ -60,7 +60,7 @@ test "it sends confirmation email if :account_activation_required is specified i
end
data = %{
- :nickname => "lain",
+ :username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "",
@@ -87,7 +87,7 @@ test "it sends confirmation email if :account_activation_required is specified i
test "it registers a new user and parses mentions in the bio" do
data1 = %{
- :nickname => "john",
+ :username => "john",
:email => "john@gmail.com",
:fullname => "John Doe",
:bio => "test",
@@ -98,7 +98,7 @@ test "it registers a new user and parses mentions in the bio" do
{:ok, user1} = TwitterAPI.register_user(data1)
data2 = %{
- :nickname => "lain",
+ :username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "@john test",
@@ -123,7 +123,7 @@ test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite()
data = %{
- :nickname => "vinny",
+ :username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@@ -145,7 +145,7 @@ test "returns user on success" do
test "returns error on invalid token" do
data = %{
- :nickname => "GrimReaper",
+ :username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
@@ -165,7 +165,7 @@ test "returns error on expired token" do
UserInviteToken.update_invite!(invite, used: true)
data = %{
- :nickname => "GrimReaper",
+ :username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
@@ -186,7 +186,7 @@ test "returns error on expired token" do
setup do
data = %{
- :nickname => "vinny",
+ :username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@@ -250,7 +250,7 @@ test "returns user on success, after him registration fails" do
UserInviteToken.update_invite!(invite, uses: 99)
data = %{
- :nickname => "vinny",
+ :username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@@ -269,7 +269,7 @@ test "returns user on success, after him registration fails" do
AccountView.render("show.json", %{user: fetched_user})
data = %{
- :nickname => "GrimReaper",
+ :username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
@@ -292,7 +292,7 @@ test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
data = %{
- :nickname => "vinny",
+ :username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@@ -317,7 +317,7 @@ test "error after max uses" do
UserInviteToken.update_invite!(invite, uses: 99)
data = %{
- :nickname => "vinny",
+ :username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
@@ -335,7 +335,7 @@ test "error after max uses" do
AccountView.render("show.json", %{user: fetched_user})
data = %{
- :nickname => "GrimReaper",
+ :username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
@@ -355,7 +355,7 @@ test "returns error on overdue date" do
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
data = %{
- :nickname => "GrimReaper",
+ :username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
@@ -377,7 +377,7 @@ test "returns error on with overdue date and after max" do
UserInviteToken.update_invite!(invite, uses: 100)
data = %{
- :nickname => "GrimReaper",
+ :username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
@@ -395,16 +395,15 @@ test "returns error on with overdue date and after max" do
test "it returns the error on registration problems" do
data = %{
- :nickname => "lain",
+ :username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
- :bio => "close the world.",
- :password => "bear"
+ :bio => "close the world."
}
- {:error, error_object} = TwitterAPI.register_user(data)
+ {:error, error} = TwitterAPI.register_user(data)
- assert is_binary(error_object[:error])
+ assert is_binary(error)
refute User.get_cached_by_nickname("lain")
end
diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs
index 4b4282727..f4884e0a2 100644
--- a/test/web/web_finger/web_finger_test.exs
+++ b/test/web/web_finger/web_finger_test.exs
@@ -67,7 +67,7 @@ test "it work for AP-only user" do
assert data["magic_key"] == nil
assert data["salmon"] == nil
- assert data["topic"] == "https://mstdn.jp/users/kPherox.atom"
+ assert data["topic"] == nil
assert data["subject"] == "acct:kPherox@mstdn.jp"
assert data["ap_id"] == "https://mstdn.jp/users/kPherox"
assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"