Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/delete-validator
This commit is contained in:
commit
6fb96f64c1
|
@ -15,15 +15,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
|
||||||
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
|
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
|
||||||
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
|
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
|
||||||
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
||||||
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
|
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
|
||||||
- Mix task to create trusted OAuth App.
|
- Mix task to create trusted OAuth App.
|
||||||
- Notifications: Added `follow_request` notification type (configurable, see `[:notifications, :enable_follow_request_notifications]` setting).
|
- Notifications: Added `follow_request` notification type.
|
||||||
- Added `:reject_deletes` group to SimplePolicy
|
- Added `:reject_deletes` group to SimplePolicy
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
|
- Mastodon API: Extended `/api/v1/instance`.
|
||||||
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
||||||
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
|
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
|
||||||
- Mastodon API: Add support for filtering replies in public and home timelines
|
- Mastodon API: Add support for filtering replies in public and home timelines
|
||||||
|
@ -37,6 +39,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Filtering of push notifications on activities from blocked domains
|
- Filtering of push notifications on activities from blocked domains
|
||||||
|
|
||||||
## [unreleased-patch]
|
## [unreleased-patch]
|
||||||
|
### Security
|
||||||
|
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
|
||||||
|
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Logger configuration through AdminFE
|
- Logger configuration through AdminFE
|
||||||
- HTTP Basic Authentication permissions issue
|
- HTTP Basic Authentication permissions issue
|
||||||
|
|
|
@ -562,8 +562,6 @@
|
||||||
inactivity_threshold: 7
|
inactivity_threshold: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
config :pleroma, :notifications, enable_follow_request_notifications: false
|
|
||||||
|
|
||||||
config :pleroma, :oauth2,
|
config :pleroma, :oauth2,
|
||||||
token_expires_in: 600,
|
token_expires_in: 600,
|
||||||
issue_new_refresh_token: true,
|
issue_new_refresh_token: true,
|
||||||
|
|
|
@ -2273,20 +2273,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
group: :pleroma,
|
|
||||||
key: :notifications,
|
|
||||||
type: :group,
|
|
||||||
description: "Notification settings",
|
|
||||||
children: [
|
|
||||||
%{
|
|
||||||
key: :enable_follow_request_notifications,
|
|
||||||
type: :boolean,
|
|
||||||
description:
|
|
||||||
"Enables notifications on new follow requests (causes issues with older PleromaFE versions)."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: Pleroma.Emails.UserEmail,
|
key: Pleroma.Emails.UserEmail,
|
||||||
|
|
|
@ -202,4 +202,19 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
||||||
- `bio`: optional
|
- `bio`: optional
|
||||||
- `captcha_solution`: optional, contains provider-specific captcha solution,
|
- `captcha_solution`: optional, contains provider-specific captcha solution,
|
||||||
- `captcha_token`: optional, contains provider-specific captcha token
|
- `captcha_token`: optional, contains provider-specific captcha token
|
||||||
|
- `captcha_answer_data`: optional, contains provider-specific captcha data
|
||||||
- `token`: invite token required when the registrations aren't public.
|
- `token`: invite token required when the registrations aren't public.
|
||||||
|
|
||||||
|
## Instance
|
||||||
|
|
||||||
|
`GET /api/v1/instance` has additional fields
|
||||||
|
|
||||||
|
- `max_toot_chars`: The maximum characters per post
|
||||||
|
- `poll_limits`: The limits of polls
|
||||||
|
- `upload_limit`: The maximum upload file size
|
||||||
|
- `avatar_upload_limit`: The same for avatars
|
||||||
|
- `background_upload_limit`: The same for backgrounds
|
||||||
|
- `banner_upload_limit`: The same for banners
|
||||||
|
- `pleroma.metadata.features`: A list of supported features
|
||||||
|
- `pleroma.metadata.federation`: The federation restrictions of this instance
|
||||||
|
- `vapid_public_key`: The public key needed for push messages
|
||||||
|
|
|
@ -49,11 +49,11 @@ Feel free to contact us to be added to this list!
|
||||||
- Platforms: Android
|
- Platforms: Android
|
||||||
- Features: Streaming Ready
|
- Features: Streaming Ready
|
||||||
|
|
||||||
### Roma
|
### Fedi
|
||||||
- Homepage: <https://www.pleroma.com/#mobileApps>
|
- Homepage: <https://www.fediapp.com/>
|
||||||
- Source Code: [iOS](https://github.com/roma-apps/roma-ios), [Android](https://github.com/roma-apps/roma-android)
|
- Source Code: Proprietary, but free
|
||||||
- Platforms: iOS, Android
|
- Platforms: iOS, Android
|
||||||
- Features: No Streaming
|
- Features: Pleroma-specific features like Reactions
|
||||||
|
|
||||||
### Tusky
|
### Tusky
|
||||||
- Homepage: <https://tuskyapp.github.io/>
|
- Homepage: <https://tuskyapp.github.io/>
|
||||||
|
|
|
@ -73,7 +73,6 @@ def start(_type, _args) do
|
||||||
Pleroma.Repo,
|
Pleroma.Repo,
|
||||||
Config.TransferTask,
|
Config.TransferTask,
|
||||||
Pleroma.Emoji,
|
Pleroma.Emoji,
|
||||||
Pleroma.Captcha,
|
|
||||||
Pleroma.Plugs.RateLimiter.Supervisor
|
Pleroma.Plugs.RateLimiter.Supervisor
|
||||||
] ++
|
] ++
|
||||||
cachex_children() ++
|
cachex_children() ++
|
||||||
|
|
|
@ -3,53 +3,22 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Captcha do
|
defmodule Pleroma.Captcha do
|
||||||
import Pleroma.Web.Gettext
|
|
||||||
|
|
||||||
alias Calendar.DateTime
|
alias Calendar.DateTime
|
||||||
alias Plug.Crypto.KeyGenerator
|
alias Plug.Crypto.KeyGenerator
|
||||||
alias Plug.Crypto.MessageEncryptor
|
alias Plug.Crypto.MessageEncryptor
|
||||||
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def start_link(_) do
|
|
||||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def init(_) do
|
|
||||||
{:ok, nil}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Ask the configured captcha service for a new captcha
|
Ask the configured captcha service for a new captcha
|
||||||
"""
|
"""
|
||||||
def new do
|
def new do
|
||||||
GenServer.call(__MODULE__, :new)
|
if not enabled?() do
|
||||||
end
|
%{type: :none}
|
||||||
|
|
||||||
@doc """
|
|
||||||
Ask the configured captcha service to validate the captcha
|
|
||||||
"""
|
|
||||||
def validate(token, captcha, answer_data) do
|
|
||||||
GenServer.call(__MODULE__, {:validate, token, captcha, answer_data})
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def handle_call(:new, _from, state) do
|
|
||||||
enabled = Pleroma.Config.get([__MODULE__, :enabled])
|
|
||||||
|
|
||||||
if !enabled do
|
|
||||||
{:reply, %{type: :none}, state}
|
|
||||||
else
|
else
|
||||||
new_captcha = method().new()
|
new_captcha = method().new()
|
||||||
|
|
||||||
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
|
|
||||||
|
|
||||||
# This make salt a little different for two keys
|
# This make salt a little different for two keys
|
||||||
token = new_captcha[:token]
|
{secret, sign_secret} = secret_pair(new_captcha[:token])
|
||||||
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
|
|
||||||
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
|
|
||||||
# Basically copy what Phoenix.Token does here, add the time to
|
# Basically copy what Phoenix.Token does here, add the time to
|
||||||
# the actual data and make it a binary to then encrypt it
|
# the actual data and make it a binary to then encrypt it
|
||||||
encrypted_captcha_answer =
|
encrypted_captcha_answer =
|
||||||
|
@ -60,55 +29,73 @@ def handle_call(:new, _from, state) do
|
||||||
|> :erlang.term_to_binary()
|
|> :erlang.term_to_binary()
|
||||||
|> MessageEncryptor.encrypt(secret, sign_secret)
|
|> MessageEncryptor.encrypt(secret, sign_secret)
|
||||||
|
|
||||||
{
|
|
||||||
:reply,
|
|
||||||
# Replace the answer with the encrypted answer
|
# Replace the answer with the encrypted answer
|
||||||
%{new_captcha | answer_data: encrypted_captcha_answer},
|
%{new_captcha | answer_data: encrypted_captcha_answer}
|
||||||
state
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc """
|
||||||
def handle_call({:validate, token, captcha, answer_data}, _from, state) do
|
Ask the configured captcha service to validate the captcha
|
||||||
|
"""
|
||||||
|
def validate(token, captcha, answer_data) do
|
||||||
|
with {:ok, %{at: at, answer_data: answer_md5}} <- validate_answer_data(token, answer_data),
|
||||||
|
:ok <- validate_expiration(at),
|
||||||
|
:ok <- validate_usage(token),
|
||||||
|
:ok <- method().validate(token, captcha, answer_md5),
|
||||||
|
{:ok, _} <- mark_captcha_as_used(token) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], false)
|
||||||
|
|
||||||
|
defp seconds_valid, do: Pleroma.Config.get!([__MODULE__, :seconds_valid])
|
||||||
|
|
||||||
|
defp secret_pair(token) do
|
||||||
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
|
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
|
||||||
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
|
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
|
||||||
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
|
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
|
||||||
|
|
||||||
# If the time found is less than (current_time-seconds_valid) then the time has already passed
|
{secret, sign_secret}
|
||||||
# Later we check that the time found is more than the presumed invalidatation time, that means
|
end
|
||||||
# that the data is still valid and the captcha can be checked
|
|
||||||
seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])
|
defp validate_answer_data(token, answer_data) do
|
||||||
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid)
|
{secret, sign_secret} = secret_pair(token)
|
||||||
|
|
||||||
result =
|
|
||||||
with false <- is_nil(answer_data),
|
with false <- is_nil(answer_data),
|
||||||
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
|
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
|
||||||
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
|
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
|
||||||
try do
|
{:ok, %{at: at, answer_data: answer_md5}}
|
||||||
if DateTime.before?(at, valid_if_after),
|
|
||||||
do: throw({:error, dgettext("errors", "CAPTCHA expired")})
|
|
||||||
|
|
||||||
if not is_nil(Cachex.get!(:used_captcha_cache, token)),
|
|
||||||
do: throw({:error, dgettext("errors", "CAPTCHA already used")})
|
|
||||||
|
|
||||||
res = method().validate(token, captcha, answer_md5)
|
|
||||||
# Throw if an error occurs
|
|
||||||
if res != :ok, do: throw(res)
|
|
||||||
|
|
||||||
# Mark this captcha as used
|
|
||||||
{:ok, _} =
|
|
||||||
Cachex.put(:used_captcha_cache, token, true, ttl: :timer.seconds(seconds_valid))
|
|
||||||
|
|
||||||
:ok
|
|
||||||
catch
|
|
||||||
:throw, e -> e
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
_ -> {:error, dgettext("errors", "Invalid answer data")}
|
_ -> {:error, :invalid_answer_data}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
{:reply, result, state}
|
defp validate_expiration(created_at) do
|
||||||
|
# If the time found is less than (current_time-seconds_valid) then the time has already passed
|
||||||
|
# Later we check that the time found is more than the presumed invalidatation time, that means
|
||||||
|
# that the data is still valid and the captcha can be checked
|
||||||
|
|
||||||
|
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid())
|
||||||
|
|
||||||
|
if DateTime.before?(created_at, valid_if_after) do
|
||||||
|
{:error, :expired}
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_usage(token) do
|
||||||
|
if is_nil(Cachex.get!(:used_captcha_cache, token)) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :already_used}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp mark_captcha_as_used(token) do
|
||||||
|
ttl = seconds_valid() |> :timer.seconds()
|
||||||
|
Cachex.put(:used_captcha_cache, token, true, ttl: ttl)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
|
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Captcha.Kocaptcha do
|
defmodule Pleroma.Captcha.Kocaptcha do
|
||||||
import Pleroma.Web.Gettext
|
|
||||||
alias Pleroma.Captcha.Service
|
alias Pleroma.Captcha.Service
|
||||||
@behaviour Service
|
@behaviour Service
|
||||||
|
|
||||||
|
@ -13,7 +12,7 @@ def new do
|
||||||
|
|
||||||
case Tesla.get(endpoint <> "/new") do
|
case Tesla.get(endpoint <> "/new") do
|
||||||
{:error, _} ->
|
{:error, _} ->
|
||||||
%{error: dgettext("errors", "Kocaptcha service unavailable")}
|
%{error: :kocaptcha_service_unavailable}
|
||||||
|
|
||||||
{:ok, res} ->
|
{:ok, res} ->
|
||||||
json_resp = Jason.decode!(res.body)
|
json_resp = Jason.decode!(res.body)
|
||||||
|
@ -33,6 +32,6 @@ def validate(_token, captcha, answer_data) do
|
||||||
if not is_nil(captcha) and
|
if not is_nil(captcha) and
|
||||||
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
|
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
|
||||||
do: :ok,
|
do: :ok,
|
||||||
else: {:error, dgettext("errors", "Invalid CAPTCHA")}
|
else: {:error, :invalid}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Captcha.Native do
|
defmodule Pleroma.Captcha.Native do
|
||||||
import Pleroma.Web.Gettext
|
|
||||||
alias Pleroma.Captcha.Service
|
alias Pleroma.Captcha.Service
|
||||||
@behaviour Service
|
@behaviour Service
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@ defmodule Pleroma.Captcha.Native do
|
||||||
def new do
|
def new do
|
||||||
case Captcha.get() do
|
case Captcha.get() do
|
||||||
:error ->
|
:error ->
|
||||||
%{error: dgettext("errors", "Captcha error")}
|
%{error: :captcha_error}
|
||||||
|
|
||||||
{:ok, answer_data, img_binary} ->
|
{:ok, answer_data, img_binary} ->
|
||||||
%{
|
%{
|
||||||
|
@ -25,7 +24,7 @@ def new do
|
||||||
|
|
||||||
@impl Service
|
@impl Service
|
||||||
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
|
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
|
||||||
def validate(_token, _captcha, _answer), do: {:error, dgettext("errors", "Invalid CAPTCHA")}
|
def validate(_token, _captcha, _answer), do: {:error, :invalid}
|
||||||
|
|
||||||
defp token do
|
defp token do
|
||||||
10
|
10
|
||||||
|
|
|
@ -20,4 +20,9 @@ defmodule Pleroma.Constants do
|
||||||
"deleted_activity_id"
|
"deleted_activity_id"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const(static_only_files,
|
||||||
|
do:
|
||||||
|
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -293,17 +293,8 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
|
|
||||||
if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
|
|
||||||
Activity.follow_accepted?(activity) do
|
|
||||||
do_create_notifications(activity)
|
|
||||||
else
|
|
||||||
{:ok, []}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||||
when type in ["Like", "Announce", "Move", "EmojiReact"] do
|
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
|
||||||
do_create_notifications(activity)
|
do_create_notifications(activity)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Plugs.InstanceStatic do
|
defmodule Pleroma.Plugs.InstanceStatic do
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
This is a shim to call `Plug.Static` but with runtime `from` configuration.
|
This is a shim to call `Plug.Static` but with runtime `from` configuration.
|
||||||
|
|
||||||
|
@ -21,9 +23,6 @@ def file_path(path) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@only ~w(index.html robots.txt static emoji packs sounds images instance favicon.png sw.js
|
|
||||||
sw-pleroma.js)
|
|
||||||
|
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
opts
|
opts
|
||||||
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
|
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
|
||||||
|
@ -31,7 +30,7 @@ def init(opts) do
|
||||||
|> Plug.Static.init()
|
|> Plug.Static.init()
|
||||||
end
|
end
|
||||||
|
|
||||||
for only <- @only do
|
for only <- Pleroma.Constants.static_only_files() do
|
||||||
at = Plug.Router.Utils.split("/")
|
at = Plug.Router.Utils.split("/")
|
||||||
|
|
||||||
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
|
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
|
||||||
|
|
|
@ -13,8 +13,9 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||||
def init(options), do: options
|
def init(options), do: options
|
||||||
|
|
||||||
defp key_id_from_conn(conn) do
|
defp key_id_from_conn(conn) do
|
||||||
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn) do
|
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
Signature.key_id_to_actor_id(key_id)
|
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
|
||||||
|
ap_id
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
nil
|
nil
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Signature do
|
||||||
alias Pleroma.Keys
|
alias Pleroma.Keys
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
|
|
||||||
def key_id_to_actor_id(key_id) do
|
def key_id_to_actor_id(key_id) do
|
||||||
uri =
|
uri =
|
||||||
|
@ -21,12 +22,23 @@ def key_id_to_actor_id(key_id) do
|
||||||
uri
|
uri
|
||||||
end
|
end
|
||||||
|
|
||||||
URI.to_string(uri)
|
maybe_ap_id = URI.to_string(uri)
|
||||||
|
|
||||||
|
case Types.ObjectID.cast(maybe_ap_id) do
|
||||||
|
{:ok, ap_id} ->
|
||||||
|
{:ok, ap_id}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
|
||||||
|
%{"ap_id" => ap_id} -> {:ok, ap_id}
|
||||||
|
_ -> {:error, maybe_ap_id}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_public_key(conn) do
|
def fetch_public_key(conn) do
|
||||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
actor_id <- key_id_to_actor_id(kid),
|
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||||
{:ok, public_key}
|
{:ok, public_key}
|
||||||
else
|
else
|
||||||
|
@ -37,7 +49,7 @@ def fetch_public_key(conn) do
|
||||||
|
|
||||||
def refetch_public_key(conn) do
|
def refetch_public_key(conn) do
|
||||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
actor_id <- key_id_to_actor_id(kid),
|
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||||
{:ok, public_key}
|
{:ok, public_key}
|
||||||
|
|
|
@ -1445,9 +1445,16 @@ def perform(:delete, %User{} = user) do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
delete_user_activities(user)
|
delete_user_activities(user)
|
||||||
|
|
||||||
|
if user.local do
|
||||||
|
user
|
||||||
|
|> change(%{deactivated: true, email: nil})
|
||||||
|
|> update_and_set_cache()
|
||||||
|
else
|
||||||
invalidate_cache(user)
|
invalidate_cache(user)
|
||||||
Repo.delete(user)
|
Repo.delete(user)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def perform(:deactivate_async, user, status), do: deactivate(user, status)
|
def perform(:deactivate_async, user, status), do: deactivate(user, status)
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
[unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
|
[unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Note: :following and :followers must be served even without authentication (as via :api)
|
||||||
plug(
|
plug(
|
||||||
EnsureAuthenticatedPlug
|
EnsureAuthenticatedPlug
|
||||||
when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers]
|
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
A module to handle coding from internal to wire ActivityPub and back.
|
A module to handle coding from internal to wire ActivityPub and back.
|
||||||
"""
|
"""
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.EarmarkRenderer
|
||||||
alias Pleroma.FollowingRelationship
|
alias Pleroma.FollowingRelationship
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Containment
|
alias Pleroma.Object.Containment
|
||||||
|
@ -43,6 +44,7 @@ def fix_object(object, options \\ []) do
|
||||||
|> fix_addressing
|
|> fix_addressing
|
||||||
|> fix_summary
|
|> fix_summary
|
||||||
|> fix_type(options)
|
|> fix_type(options)
|
||||||
|
|> fix_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def fix_summary(%{"summary" => nil} = object) do
|
def fix_summary(%{"summary" => nil} = object) do
|
||||||
|
@ -357,6 +359,18 @@ def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
|
||||||
|
|
||||||
def fix_type(object, _), do: object
|
def fix_type(object, _), do: object
|
||||||
|
|
||||||
|
defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
|
||||||
|
when is_binary(content) do
|
||||||
|
html_content =
|
||||||
|
content
|
||||||
|
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
|
||||||
|
|> Pleroma.HTML.filter_tags()
|
||||||
|
|
||||||
|
Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix_content(object), do: object
|
||||||
|
|
||||||
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
|
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
|
||||||
with true <- id =~ "follows",
|
with true <- id =~ "follows",
|
||||||
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
|
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
|
||||||
|
@ -1183,18 +1197,24 @@ def add_attributed_to(object) do
|
||||||
|
|
||||||
def prepare_attachments(object) do
|
def prepare_attachments(object) do
|
||||||
attachments =
|
attachments =
|
||||||
(object["attachment"] || [])
|
object
|
||||||
|
|> Map.get("attachment", [])
|
||||||
|> Enum.map(fn data ->
|
|> Enum.map(fn data ->
|
||||||
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
|
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
|
||||||
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
|
|
||||||
|
%{
|
||||||
|
"url" => href,
|
||||||
|
"mediaType" => media_type,
|
||||||
|
"name" => data["name"],
|
||||||
|
"type" => "Document"
|
||||||
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Map.put(object, "attachment", attachments)
|
Map.put(object, "attachment", attachments)
|
||||||
end
|
end
|
||||||
|
|
||||||
def strip_internal_fields(object) do
|
def strip_internal_fields(object) do
|
||||||
object
|
Map.drop(object, Pleroma.Constants.object_internal_fields())
|
||||||
|> Map.drop(Pleroma.Constants.object_internal_fields())
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp strip_internal_tags(%{"tag" => tags} = object) do
|
defp strip_internal_tags(%{"tag" => tags} = object) do
|
||||||
|
|
|
@ -41,9 +41,17 @@ def pagination_params do
|
||||||
Operation.parameter(
|
Operation.parameter(
|
||||||
:limit,
|
:limit,
|
||||||
:query,
|
:query,
|
||||||
%Schema{type: :integer, default: 20, maximum: 40},
|
%Schema{type: :integer, default: 20},
|
||||||
"Limit"
|
"Maximum number of items to return. Will be ignored if it's more than 40"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def empty_object_response do
|
||||||
|
Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty_array_response do
|
||||||
|
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -344,7 +344,7 @@ def endorsements_operation do
|
||||||
description: "Not implemented",
|
description: "Not implemented",
|
||||||
security: [%{"oAuth" => ["read:accounts"]}],
|
security: [%{"oAuth" => ["read:accounts"]}],
|
||||||
responses: %{
|
responses: %{
|
||||||
200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
|
200 => empty_array_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -356,7 +356,7 @@ def identity_proofs_operation do
|
||||||
operationId: "AccountController.identity_proofs",
|
operationId: "AccountController.identity_proofs",
|
||||||
description: "Not implemented",
|
description: "Not implemented",
|
||||||
responses: %{
|
responses: %{
|
||||||
200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
|
200 => empty_array_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
||||||
alias OpenApiSpex.Operation
|
alias OpenApiSpex.Operation
|
||||||
alias OpenApiSpex.Schema
|
alias OpenApiSpex.Schema
|
||||||
alias Pleroma.Web.ApiSpec.Helpers
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
def open_api_operation(action) do
|
def open_api_operation(action) do
|
||||||
operation = String.to_existing_atom("#{action}_operation")
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
@ -46,9 +46,7 @@ def create_operation do
|
||||||
operationId: "DomainBlockController.create",
|
operationId: "DomainBlockController.create",
|
||||||
requestBody: domain_block_request(),
|
requestBody: domain_block_request(),
|
||||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||||
responses: %{
|
responses: %{200 => empty_object_response()}
|
||||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,7 +65,7 @@ def delete_operation do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp domain_block_request do
|
defp domain_block_request do
|
||||||
Helpers.request_body(
|
request_body(
|
||||||
"Parameters",
|
"Parameters",
|
||||||
%Schema{
|
%Schema{
|
||||||
type: :object,
|
type: :object,
|
||||||
|
|
211
lib/pleroma/web/api_spec/operations/notification_operation.ex
Normal file
211
lib/pleroma/web/api_spec/operations/notification_operation.ex
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Get all notifications",
|
||||||
|
description:
|
||||||
|
"Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values.",
|
||||||
|
operationId: "NotificationController.index",
|
||||||
|
security: [%{"oAuth" => ["read:notifications"]}],
|
||||||
|
parameters:
|
||||||
|
[
|
||||||
|
Operation.parameter(
|
||||||
|
:exclude_types,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: notification_type()},
|
||||||
|
"Array of types to exclude"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:account_id,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :string},
|
||||||
|
"Return only notifications received from this account"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:exclude_visibilities,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: VisibilityScope},
|
||||||
|
"Exclude the notifications for activities with the given visibilities"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:include_types,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: notification_type()},
|
||||||
|
"Include the notifications for activities with the given types"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:with_muted,
|
||||||
|
:query,
|
||||||
|
BooleanLike,
|
||||||
|
"Include the notifications from muted users"
|
||||||
|
)
|
||||||
|
] ++ pagination_params(),
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Array of notifications", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: notification()
|
||||||
|
}),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Get a single notification",
|
||||||
|
description: "View information about a notification with a given ID.",
|
||||||
|
operationId: "NotificationController.show",
|
||||||
|
security: [%{"oAuth" => ["read:notifications"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Notification", "application/json", notification())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Dismiss all notifications",
|
||||||
|
description: "Clear all notifications from the server.",
|
||||||
|
operationId: "NotificationController.clear",
|
||||||
|
security: [%{"oAuth" => ["write:notifications"]}],
|
||||||
|
responses: %{200 => empty_object_response()}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def dismiss_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Dismiss a single notification",
|
||||||
|
description: "Clear a single notification from the server.",
|
||||||
|
operationId: "NotificationController.dismiss",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["write:notifications"]}],
|
||||||
|
responses: %{200 => empty_object_response()}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def dismiss_via_body_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Dismiss a single notification",
|
||||||
|
deprecated: true,
|
||||||
|
description: "Clear a single notification from the server.",
|
||||||
|
operationId: "NotificationController.dismiss_via_body",
|
||||||
|
requestBody:
|
||||||
|
request_body(
|
||||||
|
"Parameters",
|
||||||
|
%Schema{type: :object, properties: %{id: %Schema{type: :string}}},
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
security: [%{"oAuth" => ["write:notifications"]}],
|
||||||
|
responses: %{200 => empty_object_response()}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_multiple_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Dismiss multiple notifications",
|
||||||
|
operationId: "NotificationController.destroy_multiple",
|
||||||
|
security: [%{"oAuth" => ["write:notifications"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:ids,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: %Schema{type: :string}},
|
||||||
|
"Array of notification IDs to dismiss",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{200 => empty_object_response()}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification do
|
||||||
|
%Schema{
|
||||||
|
title: "Notification",
|
||||||
|
description: "Response schema for a notification",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
type: notification_type(),
|
||||||
|
created_at: %Schema{type: :string, format: :"date-time"},
|
||||||
|
account: %Schema{
|
||||||
|
allOf: [Account],
|
||||||
|
description: "The account that performed the action that generated the notification."
|
||||||
|
},
|
||||||
|
status: %Schema{
|
||||||
|
allOf: [Status],
|
||||||
|
description:
|
||||||
|
"Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.",
|
||||||
|
nullable: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"id" => "34975861",
|
||||||
|
"type" => "mention",
|
||||||
|
"created_at" => "2019-11-23T07:49:02.064Z",
|
||||||
|
"account" => Account.schema().example,
|
||||||
|
"status" => Status.schema().example
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_type do
|
||||||
|
%Schema{
|
||||||
|
type: :string,
|
||||||
|
enum: [
|
||||||
|
"follow",
|
||||||
|
"favourite",
|
||||||
|
"reblog",
|
||||||
|
"mention",
|
||||||
|
"poll",
|
||||||
|
"pleroma:emoji_reaction",
|
||||||
|
"move",
|
||||||
|
"follow_request"
|
||||||
|
],
|
||||||
|
description: """
|
||||||
|
The type of event that resulted in the notification.
|
||||||
|
|
||||||
|
- `follow` - Someone followed you
|
||||||
|
- `mention` - Someone mentioned you in their status
|
||||||
|
- `reblog` - Someone boosted one of your statuses
|
||||||
|
- `favourite` - Someone favourited one of your statuses
|
||||||
|
- `poll` - A poll you have voted in or created has ended
|
||||||
|
- `move` - Someone moved their account
|
||||||
|
- `pleroma:emoji_reaction` - Someone reacted with emoji to your status
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(:id, :path, :string, "Notification ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
78
lib/pleroma/web/api_spec/operations/report_operation.ex
Normal file
78
lib/pleroma/web/api_spec/operations/report_operation.ex
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.ReportOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["reports"],
|
||||||
|
summary: "File a report",
|
||||||
|
description: "Report problematic users to your moderators",
|
||||||
|
operationId: "ReportController.create",
|
||||||
|
security: [%{"oAuth" => ["follow", "write:reports"]}],
|
||||||
|
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Report", "application/json", create_response()),
|
||||||
|
400 => Operation.response("Report", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_request do
|
||||||
|
%Schema{
|
||||||
|
title: "ReportCreateRequest",
|
||||||
|
description: "POST body for creating a report",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
account_id: %Schema{type: :string, description: "ID of the account to report"},
|
||||||
|
status_ids: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description: "Array of Statuses to attach to the report, for context"
|
||||||
|
},
|
||||||
|
comment: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Reason for the report"
|
||||||
|
},
|
||||||
|
forward: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
"If the account is remote, should the report be forwarded to the remote admin?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: [:account_id],
|
||||||
|
example: %{
|
||||||
|
"account_id" => "123",
|
||||||
|
"status_ids" => ["1337"],
|
||||||
|
"comment" => "bad status!",
|
||||||
|
"forward" => "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_response do
|
||||||
|
%Schema{
|
||||||
|
title: "ReportResponse",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string, description: "Report ID"},
|
||||||
|
action_taken: %Schema{type: :boolean, description: "Is action taken?"}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"id" => "123",
|
||||||
|
"action_taken" => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -43,8 +43,8 @@ def unfollow(follower, unfollowed) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def accept_follow_request(follower, followed) do
|
def accept_follow_request(follower, followed) do
|
||||||
with {:ok, follower} <- User.follow(follower, followed),
|
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
||||||
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
{:ok, follower} <- User.follow(follower, followed),
|
||||||
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
|
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
|
||||||
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
|
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
|
||||||
{:ok, _activity} <-
|
{:ok, _activity} <-
|
||||||
|
@ -382,9 +382,9 @@ def thread_muted?(user, activity) do
|
||||||
ThreadMute.exists?(user.id, activity.data["context"])
|
ThreadMute.exists?(user.id, activity.data["context"])
|
||||||
end
|
end
|
||||||
|
|
||||||
def report(user, %{"account_id" => account_id} = data) do
|
def report(user, data) do
|
||||||
with {:ok, account} <- get_reported_account(account_id),
|
with {:ok, account} <- get_reported_account(data.account_id),
|
||||||
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
|
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
|
||||||
{:ok, statuses} <- get_report_statuses(account, data) do
|
{:ok, statuses} <- get_report_statuses(account, data) do
|
||||||
ActivityPub.flag(%{
|
ActivityPub.flag(%{
|
||||||
context: Utils.generate_context_id(),
|
context: Utils.generate_context_id(),
|
||||||
|
@ -392,13 +392,11 @@ def report(user, %{"account_id" => account_id} = data) do
|
||||||
account: account,
|
account: account,
|
||||||
statuses: statuses,
|
statuses: statuses,
|
||||||
content: content_html,
|
content: content_html,
|
||||||
forward: data["forward"] || false
|
forward: Map.get(data, :forward, false)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
|
|
||||||
|
|
||||||
defp get_reported_account(account_id) do
|
defp get_reported_account(account_id) do
|
||||||
case User.get_cached_by_id(account_id) do
|
case User.get_cached_by_id(account_id) do
|
||||||
%User{} = account -> {:ok, account}
|
%User{} = account -> {:ok, account}
|
||||||
|
|
|
@ -504,7 +504,8 @@ def make_report_content_html(comment) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
|
def get_report_statuses(%User{ap_id: actor}, %{status_ids: status_ids})
|
||||||
|
when is_list(status_ids) do
|
||||||
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
|
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
defmodule Pleroma.Web.Endpoint do
|
defmodule Pleroma.Web.Endpoint do
|
||||||
use Phoenix.Endpoint, otp_app: :pleroma
|
use Phoenix.Endpoint, otp_app: :pleroma
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
socket("/socket", Pleroma.Web.UserSocket)
|
socket("/socket", Pleroma.Web.UserSocket)
|
||||||
|
|
||||||
plug(Pleroma.Plugs.SetLocalePlug)
|
plug(Pleroma.Plugs.SetLocalePlug)
|
||||||
|
@ -34,8 +36,7 @@ defmodule Pleroma.Web.Endpoint do
|
||||||
Plug.Static,
|
Plug.Static,
|
||||||
at: "/",
|
at: "/",
|
||||||
from: :pleroma,
|
from: :pleroma,
|
||||||
only:
|
only: Pleroma.Constants.static_only_files(),
|
||||||
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc),
|
|
||||||
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||||
gzip: true,
|
gzip: true,
|
||||||
cache_control_for_etags: @static_cache_control,
|
cache_control_for_etags: @static_cache_control,
|
||||||
|
|
|
@ -94,24 +94,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||||
|
|
||||||
@doc "POST /api/v1/accounts"
|
@doc "POST /api/v1/accounts"
|
||||||
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
||||||
params =
|
|
||||||
params
|
|
||||||
|> Map.take([
|
|
||||||
:email,
|
|
||||||
:bio,
|
|
||||||
:captcha_solution,
|
|
||||||
:captcha_token,
|
|
||||||
:captcha_answer_data,
|
|
||||||
:token,
|
|
||||||
:password,
|
|
||||||
:fullname
|
|
||||||
])
|
|
||||||
|> Map.put(:nickname, params.username)
|
|
||||||
|> Map.put(:fullname, Map.get(params, :fullname, params.username))
|
|
||||||
|> Map.put(:confirm, params.password)
|
|
||||||
|> Map.put(:trusted_app, app.trusted)
|
|
||||||
|
|
||||||
with :ok <- validate_email_param(params),
|
with :ok <- validate_email_param(params),
|
||||||
|
:ok <- TwitterAPI.validate_captcha(app, params),
|
||||||
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
|
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
|
||||||
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
|
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
|
@ -121,7 +105,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
||||||
created_at: Token.Utils.format_created_at(token)
|
created_at: Token.Utils.format_created_at(token)
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
{:error, errors} -> json_response(conn, :bad_request, errors)
|
{:error, error} -> json_response(conn, :bad_request, %{error: error})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -133,11 +117,11 @@ def create(conn, _) do
|
||||||
render_error(conn, :forbidden, "Invalid credentials")
|
render_error(conn, :forbidden, "Invalid credentials")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
|
defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
|
||||||
|
|
||||||
defp validate_email_param(_) do
|
defp validate_email_param(_) do
|
||||||
case Pleroma.Config.get([:instance, :account_activation_required]) do
|
case Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||||
true -> {:error, %{"error" => "Missing parameters"}}
|
true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
|
||||||
_ -> :ok
|
_ -> :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
|
|
||||||
@oauth_read_actions [:show, :index]
|
@oauth_read_actions [:show, :index]
|
||||||
|
|
||||||
|
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
%{scopes: ["read:notifications"]} when action in @oauth_read_actions
|
%{scopes: ["read:notifications"]} when action in @oauth_read_actions
|
||||||
|
@ -20,14 +22,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
|
|
||||||
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
|
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
|
||||||
|
|
||||||
# GET /api/v1/notifications
|
# GET /api/v1/notifications
|
||||||
def index(conn, %{"account_id" => account_id} = params) do
|
def index(conn, %{account_id: account_id} = params) do
|
||||||
case Pleroma.User.get_cached_by_id(account_id) do
|
case Pleroma.User.get_cached_by_id(account_id) do
|
||||||
%{ap_id: account_ap_id} ->
|
%{ap_id: account_ap_id} ->
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|> Map.delete("account_id")
|
|> Map.delete(:account_id)
|
||||||
|> Map.put("account_ap_id", account_ap_id)
|
|> Map.put(:account_ap_id, account_ap_id)
|
||||||
|
|
||||||
index(conn, params)
|
index(conn, params)
|
||||||
|
|
||||||
|
@ -39,6 +43,7 @@ def index(conn, %{"account_id" => account_id} = params) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def index(%{assigns: %{user: user}} = conn, params) do
|
def index(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
|
||||||
notifications = MastodonAPI.get_notifications(user, params)
|
notifications = MastodonAPI.get_notifications(user, params)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
@ -51,7 +56,7 @@ def index(%{assigns: %{user: user}} = conn, params) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /api/v1/notifications/:id
|
# GET /api/v1/notifications/:id
|
||||||
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||||
with {:ok, notification} <- Notification.get(user, id) do
|
with {:ok, notification} <- Notification.get(user, id) do
|
||||||
render(conn, "show.json", notification: notification, for: user)
|
render(conn, "show.json", notification: notification, for: user)
|
||||||
else
|
else
|
||||||
|
@ -69,8 +74,8 @@ def clear(%{assigns: %{user: user}} = conn, _params) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /api/v1/notifications/:id/dismiss
|
# POST /api/v1/notifications/:id/dismiss
|
||||||
# POST /api/v1/notifications/dismiss (deprecated)
|
|
||||||
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
|
def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
|
||||||
with {:ok, _notif} <- Notification.dismiss(user, id) do
|
with {:ok, _notif} <- Notification.dismiss(user, id) do
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
else
|
else
|
||||||
|
@ -81,8 +86,13 @@ def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/notifications/dismiss (deprecated)
|
||||||
|
def dismiss_via_body(%{body_params: params} = conn, _) do
|
||||||
|
dismiss(conn, params)
|
||||||
|
end
|
||||||
|
|
||||||
# DELETE /api/v1/notifications/destroy_multiple
|
# DELETE /api/v1/notifications/destroy_multiple
|
||||||
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
|
def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
|
||||||
Notification.destroy_multiple(user, ids)
|
Notification.destroy_multiple(user, ids)
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,10 +9,13 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
|
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
|
||||||
|
|
||||||
@doc "POST /api/v1/reports"
|
@doc "POST /api/v1/reports"
|
||||||
def create(%{assigns: %{user: user}} = conn, params) do
|
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
|
||||||
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
|
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
|
||||||
render(conn, "show.json", activity: activity)
|
render(conn, "show.json", activity: activity)
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,10 +5,13 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
||||||
use Pleroma.Web, :view
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Web.ActivityPub.MRF
|
||||||
|
|
||||||
@mastodon_api_level "2.7.2"
|
@mastodon_api_level "2.7.2"
|
||||||
|
|
||||||
def render("show.json", _) do
|
def render("show.json", _) do
|
||||||
instance = Pleroma.Config.get(:instance)
|
instance = Config.get(:instance)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
uri: Pleroma.Web.base_url(),
|
uri: Pleroma.Web.base_url(),
|
||||||
|
@ -29,7 +32,58 @@ def render("show.json", _) do
|
||||||
upload_limit: Keyword.get(instance, :upload_limit),
|
upload_limit: Keyword.get(instance, :upload_limit),
|
||||||
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
|
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
|
||||||
background_upload_limit: Keyword.get(instance, :background_upload_limit),
|
background_upload_limit: Keyword.get(instance, :background_upload_limit),
|
||||||
banner_upload_limit: Keyword.get(instance, :banner_upload_limit)
|
banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
|
||||||
|
pleroma: %{
|
||||||
|
metadata: %{
|
||||||
|
features: features(),
|
||||||
|
federation: federation()
|
||||||
|
},
|
||||||
|
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def features do
|
||||||
|
[
|
||||||
|
"pleroma_api",
|
||||||
|
"mastodon_api",
|
||||||
|
"mastodon_api_streaming",
|
||||||
|
"polls",
|
||||||
|
"pleroma_explicit_addressing",
|
||||||
|
"shareable_emoji_packs",
|
||||||
|
"multifetch",
|
||||||
|
"pleroma:api/v1/notifications:include_types_filter",
|
||||||
|
if Config.get([:media_proxy, :enabled]) do
|
||||||
|
"media_proxy"
|
||||||
|
end,
|
||||||
|
if Config.get([:gopher, :enabled]) do
|
||||||
|
"gopher"
|
||||||
|
end,
|
||||||
|
if Config.get([:chat, :enabled]) do
|
||||||
|
"chat"
|
||||||
|
end,
|
||||||
|
if Config.get([:instance, :allow_relay]) do
|
||||||
|
"relay"
|
||||||
|
end,
|
||||||
|
if Config.get([:instance, :safe_dm_mentions]) do
|
||||||
|
"safe_dm_mentions"
|
||||||
|
end,
|
||||||
|
"pleroma_emoji_reactions"
|
||||||
|
]
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def federation do
|
||||||
|
quarantined = Config.get([:instance, :quarantined_instances], [])
|
||||||
|
|
||||||
|
if Config.get([:instance, :mrf_transparency]) do
|
||||||
|
{:ok, data} = MRF.describe()
|
||||||
|
|
||||||
|
data
|
||||||
|
|> Map.merge(%{quarantined_instances: quarantined})
|
||||||
|
else
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
|> Map.put(:enabled, Config.get([:instance, :federating]))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,8 +9,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
||||||
alias Pleroma.Stats
|
alias Pleroma.Stats
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web
|
alias Pleroma.Web
|
||||||
alias Pleroma.Web.ActivityPub.MRF
|
|
||||||
alias Pleroma.Web.Federator.Publisher
|
alias Pleroma.Web.Federator.Publisher
|
||||||
|
alias Pleroma.Web.MastodonAPI.InstanceView
|
||||||
|
|
||||||
def schemas(conn, _params) do
|
def schemas(conn, _params) do
|
||||||
response = %{
|
response = %{
|
||||||
|
@ -34,51 +34,12 @@ def schemas(conn, _params) do
|
||||||
def raw_nodeinfo do
|
def raw_nodeinfo do
|
||||||
stats = Stats.get_stats()
|
stats = Stats.get_stats()
|
||||||
|
|
||||||
quarantined = Config.get([:instance, :quarantined_instances], [])
|
|
||||||
|
|
||||||
staff_accounts =
|
staff_accounts =
|
||||||
User.all_superusers()
|
User.all_superusers()
|
||||||
|> Enum.map(fn u -> u.ap_id end)
|
|> Enum.map(fn u -> u.ap_id end)
|
||||||
|
|
||||||
federation_response =
|
features = InstanceView.features()
|
||||||
if Config.get([:instance, :mrf_transparency]) do
|
federation = InstanceView.federation()
|
||||||
{:ok, data} = MRF.describe()
|
|
||||||
|
|
||||||
data
|
|
||||||
|> Map.merge(%{quarantined_instances: quarantined})
|
|
||||||
else
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
|> Map.put(:enabled, Config.get([:instance, :federating]))
|
|
||||||
|
|
||||||
features =
|
|
||||||
[
|
|
||||||
"pleroma_api",
|
|
||||||
"mastodon_api",
|
|
||||||
"mastodon_api_streaming",
|
|
||||||
"polls",
|
|
||||||
"pleroma_explicit_addressing",
|
|
||||||
"shareable_emoji_packs",
|
|
||||||
"multifetch",
|
|
||||||
"pleroma:api/v1/notifications:include_types_filter",
|
|
||||||
if Config.get([:media_proxy, :enabled]) do
|
|
||||||
"media_proxy"
|
|
||||||
end,
|
|
||||||
if Config.get([:gopher, :enabled]) do
|
|
||||||
"gopher"
|
|
||||||
end,
|
|
||||||
if Config.get([:chat, :enabled]) do
|
|
||||||
"chat"
|
|
||||||
end,
|
|
||||||
if Config.get([:instance, :allow_relay]) do
|
|
||||||
"relay"
|
|
||||||
end,
|
|
||||||
if Config.get([:instance, :safe_dm_mentions]) do
|
|
||||||
"safe_dm_mentions"
|
|
||||||
end,
|
|
||||||
"pleroma_emoji_reactions"
|
|
||||||
]
|
|
||||||
|> Enum.filter(& &1)
|
|
||||||
|
|
||||||
%{
|
%{
|
||||||
version: "2.0",
|
version: "2.0",
|
||||||
|
@ -106,7 +67,7 @@ def raw_nodeinfo do
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
staffAccounts: staff_accounts,
|
staffAccounts: staff_accounts,
|
||||||
federation: federation_response,
|
federation: federation,
|
||||||
pollLimits: Config.get([:instance, :poll_limits]),
|
pollLimits: Config.get([:instance, :poll_limits]),
|
||||||
postFormats: Config.get([:instance, :allowed_post_formats]),
|
postFormats: Config.get([:instance, :allowed_post_formats]),
|
||||||
uploadLimits: %{
|
uploadLimits: %{
|
||||||
|
|
|
@ -61,7 +61,10 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}
|
||||||
else
|
else
|
||||||
users =
|
users =
|
||||||
Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
|
Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(fn
|
||||||
|
%{deactivated: false} -> true
|
||||||
|
_ -> false
|
||||||
|
end)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
name: emoji,
|
name: emoji,
|
||||||
|
|
|
@ -396,7 +396,7 @@ defmodule Pleroma.Web.Router do
|
||||||
post("/notifications/clear", NotificationController, :clear)
|
post("/notifications/clear", NotificationController, :clear)
|
||||||
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
|
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
|
||||||
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
|
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
|
||||||
post("/notifications/dismiss", NotificationController, :dismiss)
|
post("/notifications/dismiss", NotificationController, :dismiss_via_body)
|
||||||
|
|
||||||
post("/polls/:id/votes", PollController, :vote)
|
post("/polls/:id/votes", PollController, :vote)
|
||||||
|
|
||||||
|
@ -585,6 +585,7 @@ defmodule Pleroma.Web.Router do
|
||||||
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
|
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
|
||||||
post("/api/ap/upload_media", ActivityPubController, :upload_media)
|
post("/api/ap/upload_media", ActivityPubController, :upload_media)
|
||||||
|
|
||||||
|
# The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
|
||||||
get("/users/:nickname/followers", ActivityPubController, :followers)
|
get("/users/:nickname/followers", ActivityPubController, :followers)
|
||||||
get("/users/:nickname/following", ActivityPubController, :following)
|
get("/users/:nickname/following", ActivityPubController, :following)
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
|
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
|
||||||
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
||||||
<%= Phoenix.HTML.raw(assigns[:meta] || "") %>
|
<%= Phoenix.HTML.raw(assigns[:meta] || "") %>
|
||||||
<link rel="stylesheet" href="/static/static-fe.css">
|
<link rel="stylesheet" href="/static-fe/static-fe.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
|
@ -3,55 +3,28 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
||||||
|
import Pleroma.Web.Gettext
|
||||||
|
|
||||||
alias Pleroma.Emails.Mailer
|
alias Pleroma.Emails.Mailer
|
||||||
alias Pleroma.Emails.UserEmail
|
alias Pleroma.Emails.UserEmail
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.UserInviteToken
|
alias Pleroma.UserInviteToken
|
||||||
|
|
||||||
require Pleroma.Constants
|
|
||||||
|
|
||||||
def register_user(params, opts \\ []) do
|
def register_user(params, opts \\ []) do
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|> Map.take([
|
|> Map.take([:email, :token, :password])
|
||||||
:nickname,
|
|> Map.put(:bio, params |> Map.get(:bio, "") |> User.parse_bio())
|
||||||
:password,
|
|> Map.put(:nickname, params[:username])
|
||||||
:captcha_solution,
|
|> Map.put(:name, Map.get(params, :fullname, params[:username]))
|
||||||
:captcha_token,
|
|> Map.put(:password_confirmation, params[:password])
|
||||||
: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])
|
|
||||||
|
|
||||||
case validate_captcha(params) do
|
|
||||||
:ok ->
|
|
||||||
if Pleroma.Config.get([:instance, :registrations_open]) do
|
if Pleroma.Config.get([:instance, :registrations_open]) do
|
||||||
create_user(params, opts)
|
create_user(params, opts)
|
||||||
else
|
else
|
||||||
create_user_with_invite(params, opts)
|
create_user_with_invite(params, opts)
|
||||||
end
|
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
|
|
||||||
else
|
|
||||||
Pleroma.Captcha.validate(
|
|
||||||
params.captcha_token,
|
|
||||||
params.captcha_solution,
|
|
||||||
params.captcha_answer_data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_user_with_invite(params, opts) do
|
defp create_user_with_invite(params, opts) do
|
||||||
|
@ -75,16 +48,17 @@ defp create_user(params, opts) do
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
errors =
|
errors =
|
||||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
changeset
|
||||||
|
|> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
|
|
||||||
{:error, %{error: errors}}
|
{:error, errors}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def password_reset(nickname_or_email) do
|
def password_reset(nickname_or_email) do
|
||||||
with true <- is_binary(nickname_or_email),
|
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),
|
User.get_by_nickname_or_email(nickname_or_email),
|
||||||
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
|
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
|
||||||
user
|
user
|
||||||
|
@ -106,4 +80,58 @@ def password_reset(nickname_or_email) do
|
||||||
{:error, "unknown user"}
|
{:error, "unknown user"}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -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
|
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/chunk-0778.d9e7180a.css
Normal file
1
priv/static/adminfe/chunk-0778.d9e7180a.css
Normal file
|
@ -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%}}
|
|
@ -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}}
|
|
|
@ -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%}}
|
|
1
priv/static/adminfe/chunk-22d2.813009b9.css
Normal file
1
priv/static/adminfe/chunk-22d2.813009b9.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/chunk-4011.c4799067.css
Normal file
1
priv/static/adminfe/chunk-4011.c4799067.css
Normal file
File diff suppressed because one or more lines are too long
|
@ -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%}}
|
|
1
priv/static/adminfe/chunk-6b68.0cc00484.css
Normal file
1
priv/static/adminfe/chunk-6b68.0cc00484.css
Normal file
|
@ -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}
|
1
priv/static/adminfe/chunk-7637.941c4edb.css
Normal file
1
priv/static/adminfe/chunk-7637.941c4edb.css
Normal file
|
@ -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%}}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/chunk-970d.f59cca8c.css
Normal file
1
priv/static/adminfe/chunk-970d.f59cca8c.css
Normal file
File diff suppressed because one or more lines are too long
|
@ -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%}}
|
|
1
priv/static/adminfe/chunk-d38a.cabdc22e.css
Normal file
1
priv/static/adminfe/chunk-d38a.cabdc22e.css
Normal file
|
@ -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%}}
|
1
priv/static/adminfe/chunk-e458.f88bafea.css
Normal file
1
priv/static/adminfe/chunk-e458.f88bafea.css
Normal file
|
@ -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}}
|
|
@ -1 +1 @@
|
||||||
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.1abbc9b8.css rel=stylesheet><link href=chunk-libs.686b5876.css rel=stylesheet><link href=app.85534e14.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.cb26bbd1.js></script><script type=text/javascript src=static/js/chunk-elementUI.fba0efec.js></script><script type=text/javascript src=static/js/chunk-libs.b8c453ab.js></script><script type=text/javascript src=static/js/app.d898cc2b.js></script></body></html>
|
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.1abbc9b8.css rel=stylesheet><link href=chunk-libs.686b5876.css rel=stylesheet><link href=app.796ca6d4.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.1b4f6ce0.js></script><script type=text/javascript src=static/js/chunk-elementUI.fba0efec.js></script><script type=text/javascript src=static/js/chunk-libs.b8c453ab.js></script><script type=text/javascript src=static/js/app.203f69f8.js></script></body></html>
|
2
priv/static/adminfe/static/js/app.203f69f8.js
Normal file
2
priv/static/adminfe/static/js/app.203f69f8.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/app.203f69f8.js.map
Normal file
1
priv/static/adminfe/static/js/app.203f69f8.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-0778.b17650df.js
Normal file
2
priv/static/adminfe/static/js/chunk-0778.b17650df.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-0778.b17650df.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-0778.b17650df.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js
Normal file
2
priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-3384.458ffaf1.js
Normal file
2
priv/static/adminfe/static/js/chunk-3384.458ffaf1.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-3384.458ffaf1.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-3384.458ffaf1.js.map
Normal file
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-4011.67fb1692.js
Normal file
2
priv/static/adminfe/static/js/chunk-4011.67fb1692.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-4011.67fb1692.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-4011.67fb1692.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js
Normal file
2
priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,2 @@
|
||||||
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-4ffb"],{BF41:function(t,a,i){},"UUO+":function(t,a,i){"use strict";i.r(a);var s=i("zGwZ"),e=i.n(s),r={name:"Page401",data:function(){return{errGif:e.a+"?"+ +new Date,ewizardClap:"https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646",dialogVisible:!1}},methods:{back:function(){this.$route.query.noGoBack?this.$router.push({path:"/dashboard"}):this.$router.go(-1)}}},n=(i("UrVv"),i("KHd+")),l=Object(n.a)(r,function(){var t=this,a=t.$createElement,i=t._self._c||a;return i("div",{staticClass:"errPage-container"},[i("el-button",{staticClass:"pan-back-btn",attrs:{icon:"arrow-left"},on:{click:t.back}},[t._v("返回")]),t._v(" "),i("el-row",[i("el-col",{attrs:{span:12}},[i("h1",{staticClass:"text-jumbo text-ginormous"},[t._v("Oops!")]),t._v("\n gif来源"),i("a",{attrs:{href:"https://zh.airbnb.com/",target:"_blank"}},[t._v("airbnb")]),t._v(" 页面\n "),i("h2",[t._v("你没有权限去该页面")]),t._v(" "),i("h6",[t._v("如有不满请联系你领导")]),t._v(" "),i("ul",{staticClass:"list-unstyled"},[i("li",[t._v("或者你可以去:")]),t._v(" "),i("li",{staticClass:"link-type"},[i("router-link",{attrs:{to:"/dashboard"}},[t._v("回首页")])],1),t._v(" "),i("li",{staticClass:"link-type"},[i("a",{attrs:{href:"https://www.taobao.com/"}},[t._v("随便看看")])]),t._v(" "),i("li",[i("a",{attrs:{href:"#"},on:{click:function(a){a.preventDefault(),t.dialogVisible=!0}}},[t._v("点我看图")])])])]),t._v(" "),i("el-col",{attrs:{span:12}},[i("img",{attrs:{src:t.errGif,width:"313",height:"428",alt:"Girl has dropped her ice cream."}})])],1),t._v(" "),i("el-dialog",{attrs:{visible:t.dialogVisible,title:"随便看"},on:{"update:visible":function(a){t.dialogVisible=a}}},[i("img",{staticClass:"pan-img",attrs:{src:t.ewizardClap}})])],1)},[],!1,null,"ab9be52c",null);l.options.__file="401.vue";a.default=l.exports},UrVv:function(t,a,i){"use strict";var s=i("BF41");i.n(s).a},zGwZ:function(t,a,i){t.exports=i.p+"static/img/401.089007e.gif"}}]);
|
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-6e81"],{BF41:function(t,a,i){},"UUO+":function(t,a,i){"use strict";i.r(a);var e=i("zGwZ"),s=i.n(e),r={name:"Page401",data:function(){return{errGif:s.a+"?"+ +new Date,ewizardClap:"https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646",dialogVisible:!1}},methods:{back:function(){this.$route.query.noGoBack?this.$router.push({path:"/dashboard"}):this.$router.go(-1)}}},n=(i("UrVv"),i("KHd+")),l=Object(n.a)(r,function(){var t=this,a=t.$createElement,i=t._self._c||a;return i("div",{staticClass:"errPage-container"},[i("el-button",{staticClass:"pan-back-btn",attrs:{icon:"arrow-left"},on:{click:t.back}},[t._v("返回")]),t._v(" "),i("el-row",[i("el-col",{attrs:{span:12}},[i("h1",{staticClass:"text-jumbo text-ginormous"},[t._v("Oops!")]),t._v("\n gif来源"),i("a",{attrs:{href:"https://zh.airbnb.com/",target:"_blank"}},[t._v("airbnb")]),t._v(" 页面\n "),i("h2",[t._v("你没有权限去该页面")]),t._v(" "),i("h6",[t._v("如有不满请联系你领导")]),t._v(" "),i("ul",{staticClass:"list-unstyled"},[i("li",[t._v("或者你可以去:")]),t._v(" "),i("li",{staticClass:"link-type"},[i("router-link",{attrs:{to:"/dashboard"}},[t._v("回首页")])],1),t._v(" "),i("li",{staticClass:"link-type"},[i("a",{attrs:{href:"https://www.taobao.com/"}},[t._v("随便看看")])]),t._v(" "),i("li",[i("a",{attrs:{href:"#"},on:{click:function(a){a.preventDefault(),t.dialogVisible=!0}}},[t._v("点我看图")])])])]),t._v(" "),i("el-col",{attrs:{span:12}},[i("img",{attrs:{src:t.errGif,width:"313",height:"428",alt:"Girl has dropped her ice cream."}})])],1),t._v(" "),i("el-dialog",{attrs:{visible:t.dialogVisible,title:"随便看"},on:{"update:visible":function(a){t.dialogVisible=a}}},[i("img",{staticClass:"pan-img",attrs:{src:t.ewizardClap}})])],1)},[],!1,null,"ab9be52c",null);l.options.__file="401.vue";a.default=l.exports},UrVv:function(t,a,i){"use strict";var e=i("BF41");i.n(e).a},zGwZ:function(t,a,i){t.exports=i.p+"static/img/401.089007e.gif"}}]);
|
||||||
//# sourceMappingURL=chunk-4ffb.0e8f3772.js.map
|
//# sourceMappingURL=chunk-6e81.3733ace2.js.map
|
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js
Normal file
2
priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-970d.2457e066.js
Normal file
2
priv/static/adminfe/static/js/chunk-970d.2457e066.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-970d.2457e066.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-970d.2457e066.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-d38a.a851004a.js
Normal file
2
priv/static/adminfe/static/js/chunk-d38a.a851004a.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-d38a.a851004a.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-d38a.a851004a.js.map
Normal file
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/chunk-e458.4e5aad44.js
Normal file
2
priv/static/adminfe/static/js/chunk-e458.4e5aad44.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/chunk-e458.4e5aad44.js.map
Normal file
1
priv/static/adminfe/static/js/chunk-e458.4e5aad44.js.map
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue