stable
commit
7ac0cffb34
@ -0,0 +1,156 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA do
|
||||
@moduledoc """
|
||||
The MFA context.
|
||||
"""
|
||||
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.User
|
||||
|
||||
alias Pleroma.MFA.BackupCodes
|
||||
alias Pleroma.MFA.Changeset
|
||||
alias Pleroma.MFA.Settings
|
||||
alias Pleroma.MFA.TOTP
|
||||
|
||||
@doc """
|
||||
Returns MFA methods the user has enabled.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Pleroma.MFA.supported_method(User)
|
||||
"totp, u2f"
|
||||
"""
|
||||
@spec supported_methods(User.t()) :: String.t()
|
||||
def supported_methods(user) do
|
||||
settings = fetch_settings(user)
|
||||
|
||||
Settings.mfa_methods()
|
||||
|> Enum.reduce([], fn m, acc ->
|
||||
if method_enabled?(m, settings) do
|
||||
acc ++ [m]
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|> Enum.join(",")
|
||||
end
|
||||
|
||||
@doc "Checks that user enabled MFA"
|
||||
def require?(user) do
|
||||
fetch_settings(user).enabled
|
||||
end
|
||||
|
||||
@doc """
|
||||
Display MFA settings of user
|
||||
"""
|
||||
def mfa_settings(user) do
|
||||
settings = fetch_settings(user)
|
||||
|
||||
Settings.mfa_methods()
|
||||
|> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
|
||||
|> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def fetch_settings(%User{} = user) do
|
||||
user.multi_factor_authentication_settings || %Settings{}
|
||||
end
|
||||
|
||||
@doc "clears backup codes"
|
||||
def invalidate_backup_code(%User{} = user, hash_code) do
|
||||
%{backup_codes: codes} = fetch_settings(user)
|
||||
|
||||
user
|
||||
|> Changeset.cast_backup_codes(codes -- [hash_code])
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc "generates backup codes"
|
||||
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
|
||||
def generate_backup_codes(%User{} = user) do
|
||||
with codes <- BackupCodes.generate(),
|
||||
hashed_codes <- Enum.map(codes, &Pbkdf2.hashpwsalt/1),
|
||||
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
|
||||
{:ok, _} <- User.update_and_set_cache(changeset) do
|
||||
{:ok, codes}
|
||||
else
|
||||
{:error, msg} ->
|
||||
%{error: msg}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates secret key and set delivery_type to 'app' for TOTP method.
|
||||
"""
|
||||
@spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def setup_totp(user) do
|
||||
user
|
||||
|> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Confirms the TOTP method for user.
|
||||
|
||||
`attrs`:
|
||||
`password` - current user password
|
||||
`code` - TOTP token
|
||||
"""
|
||||
@spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
def confirm_totp(%User{} = user, attrs) do
|
||||
with settings <- user.multi_factor_authentication_settings.totp,
|
||||
{:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
|
||||
user
|
||||
|> Changeset.confirm_totp()
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disables the TOTP method for user.
|
||||
|
||||
`attrs`:
|
||||
`password` - current user password
|
||||
"""
|
||||
@spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def disable_totp(%User{} = user) do
|
||||
user
|
||||
|> Changeset.disable_totp()
|
||||
|> Changeset.disable()
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Force disables all MFA methods for user.
|
||||
"""
|
||||
@spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def disable(%User{} = user) do
|
||||
user
|
||||
|> Changeset.disable_totp()
|
||||
|> Changeset.disable(true)
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the user has MFA method enabled.
|
||||
"""
|
||||
def method_enabled?(method, settings) do
|
||||
with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the user has enabled at least one MFA method.
|
||||
"""
|
||||
def enabled?(settings) do
|
||||
Settings.mfa_methods()
|
||||
|> Enum.map(fn m -> method_enabled?(m, settings) end)
|
||||
|> Enum.any?()
|
||||
end
|
||||
end
|
@ -0,0 +1,31 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.BackupCodes do
|
||||
@moduledoc """
|
||||
This module contains functions for generating backup codes.
|
||||
"""
|
||||
alias Pleroma.Config
|
||||
|
||||
@config_ns [:instance, :multi_factor_authentication, :backup_codes]
|
||||
|
||||
@doc """
|
||||
Generates backup codes.
|
||||
"""
|
||||
@spec generate(Keyword.t()) :: list(String.t())
|
||||
def generate(opts \\ []) do
|
||||
number_of_codes = Keyword.get(opts, :number_of_codes, default_backup_codes_number())
|
||||
code_length = Keyword.get(opts, :length, default_backup_codes_code_length())
|
||||
|
||||
Enum.map(1..number_of_codes, fn _ ->
|
||||
:crypto.strong_rand_bytes(div(code_length, 2))
|
||||
|> Base.encode16(case: :lower)
|
||||
end)
|
||||
end
|
||||
|
||||
defp default_backup_codes_number, do: Config.get(@config_ns ++ [:number], 5)
|
||||
|
||||
defp default_backup_codes_code_length,
|
||||
do: Config.get(@config_ns ++ [:length], 16)
|
||||
end
|
@ -0,0 +1,64 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Changeset do
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.Settings
|
||||
alias Pleroma.User
|
||||
|
||||
def disable(%Ecto.Changeset{} = changeset, force \\ false) do
|
||||
settings =
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> MFA.fetch_settings()
|
||||
|
||||
if force || not MFA.enabled?(settings) do
|
||||
put_change(changeset, %Settings{settings | enabled: false})
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||
user
|
||||
|> put_change(%Settings{settings | totp: %Settings.TOTP{}})
|
||||
end
|
||||
|
||||
def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||
totp_settings = %Settings.TOTP{settings.totp | confirmed: true}
|
||||
|
||||
user
|
||||
|> put_change(%Settings{settings | totp: totp_settings, enabled: true})
|
||||
end
|
||||
|
||||
def setup_totp(%User{} = user, attrs) do
|
||||
mfa_settings = MFA.fetch_settings(user)
|
||||
|
||||
totp_settings =
|
||||
%Settings.TOTP{}
|
||||
|> Ecto.Changeset.cast(attrs, [:secret, :delivery_type])
|
||||
|
||||
user
|
||||
|> put_change(%Settings{mfa_settings | totp: Ecto.Changeset.apply_changes(totp_settings)})
|
||||
end
|
||||
|
||||
def cast_backup_codes(%User{} = user, codes) do
|
||||
user
|
||||
|> put_change(%Settings{
|
||||
user.multi_factor_authentication_settings
|
||||
| backup_codes: codes
|
||||
})
|
||||
end
|
||||
|
||||
defp put_change(%User{} = user, settings) do
|
||||
user
|
||||
|> Ecto.Changeset.change()
|
||||
|> put_change(settings)
|
||||
end
|
||||
|
||||
defp put_change(%Ecto.Changeset{} = changeset, settings) do
|
||||
changeset
|
||||
|> Ecto.Changeset.put_change(:multi_factor_authentication_settings, settings)
|
||||
end
|
||||
end
|
@ -0,0 +1,24 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Settings do
|
||||
use Ecto.Schema
|
||||
|
||||
@primary_key false
|
||||
|
||||
@mfa_methods [:totp]
|
||||
embedded_schema do
|
||||
field(:enabled, :boolean, default: false)
|
||||
field(:backup_codes, {:array, :string}, default: [])
|
||||
|
||||
embeds_one :totp, TOTP, on_replace: :delete, primary_key: false do
|
||||
field(:secret, :string)
|
||||
# app | sms
|
||||
field(:delivery_type, :string, default: "app")
|
||||
field(:confirmed, :boolean, default: false)
|
||||
end
|
||||
end
|
||||
|
||||
def mfa_methods, do: @mfa_methods
|
||||
end
|
@ -0,0 +1,106 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Token do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.Token, as: OAuthToken
|
||||
|
||||
@expires 300
|
||||
|
||||
schema "mfa_tokens" do
|
||||
field(:token, :string)
|
||||
field(:valid_until, :naive_datetime_usec)
|
||||
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:authorization, Authorization)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def get_by_token(token) do
|
||||
from(
|
||||
t in __MODULE__,
|
||||
where: t.token == ^token,
|
||||
preload: [:user, :authorization]
|
||||
)
|
||||
|> Repo.find_resource()
|
||||
end
|
||||
|
||||
def validate(token) do
|
||||
with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
|
||||
{:expired, false} <- {:expired, is_expired?(token)} do
|
||||
{:ok, token}
|
||||
else
|
||||
{:expired, _} -> {:error, :expired_token}
|
||||
{:fetch_token, _} -> {:error, :not_found}
|
||||
error -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def create_token(%User{} = user) do
|
||||
%__MODULE__{}
|
||||
|> change
|
||||
|> assign_user(user)
|
||||
|> put_token
|
||||
|> put_valid_until
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def create_token(user, authorization) do
|
||||
%__MODULE__{}
|
||||
|> change
|
||||
|> assign_user(user)
|
||||
|> assign_authorization(authorization)
|
||||
|> put_token
|
||||
|> put_valid_until
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
defp assign_user(changeset, user) do
|
||||
changeset
|
||||
|> put_assoc(:user, user)
|
||||
|> validate_required([:user])
|
||||
end
|
||||
|
||||
defp assign_authorization(changeset, authorization) do
|
||||
changeset
|
||||
|> put_assoc(:authorization, authorization)
|
||||
|> validate_required([:authorization])
|
||||
end
|
||||
|
||||
defp put_token(changeset) do
|
||||
changeset
|
||||
|> change(%{token: OAuthToken.Utils.generate_token()})
|
||||
|> validate_required([:token])
|
||||
|> unique_constraint(:token)
|
||||
end
|
||||
|
||||
defp put_valid_until(changeset) do
|
||||
expires_in = NaiveDateTime.add(NaiveDateTime.utc_now(), @expires)
|
||||
|
||||
changeset
|
||||
|> change(%{valid_until: expires_in})
|
||||
|> validate_required([:valid_until])
|
||||
end
|
||||
|
||||
def is_expired?(%__MODULE__{valid_until: valid_until}) do
|
||||
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
|
||||
end
|
||||
|
||||
def is_expired?(_), do: false
|
||||
|
||||
def delete_expired_tokens do
|
||||
from(
|
||||
q in __MODULE__,
|
||||
where: fragment("?", q.valid_until) < ^Timex.now()
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
end
|
@ -0,0 +1,86 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.TOTP do
|
||||
@moduledoc """
|
||||
This module represents functions to create secrets for
|
||||
TOTP Application as well as validate them with a time based token.
|
||||
"""
|
||||
alias Pleroma.Config
|
||||
|
||||
@config_ns [:instance, :multi_factor_authentication, :totp]
|
||||
|
||||
@doc """
|
||||
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
"""
|
||||
def provisioning_uri(secret, label, opts \\ []) do
|
||||
query =
|
||||
%{
|
||||
secret: secret,
|
||||
issuer: Keyword.get(opts, :issuer, default_issuer()),
|
||||
digits: Keyword.get(opts, :digits, default_digits()),
|
||||
period: Keyword.get(opts, :period, default_period())
|
||||
}
|
||||
|> Enum.filter(fn {_, v} -> not is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
|> URI.encode_query()
|
||||
|
||||
%URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
|
||||
|> URI.to_string()
|
||||
end
|
||||
|
||||
defp default_period, do: Config.get(@config_ns ++ [:period])
|
||||
defp default_digits, do: Config.get(@config_ns ++ [:digits])
|
||||
|
||||
defp default_issuer,
|
||||
do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))
|
||||
|
||||
@doc "Creates a random Base 32 encoded string"
|
||||
def generate_secret do
|
||||
Base.encode32(:crypto.strong_rand_bytes(10))
|
||||
end
|
||||
|
||||
@doc "Generates a valid token based on a secret"
|
||||
def generate_token(secret) do
|
||||
:pot.totp(secret)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates a given token based on a secret.
|
||||
|
||||
optional parameters:
|
||||
`token_length` default `6`
|
||||
`interval_length` default `30`
|
||||
`window` default 0
|
||||
|
||||
Returns {:ok, :pass} if the token is valid and
|
||||
{:error, :invalid_token} if it is not.
|
||||
"""
|
||||
@spec validate_token(String.t(), String.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def validate_token(secret, token)
|
||||
when is_binary(secret) and is_binary(token) do
|
||||
opts = [
|
||||
token_length: default_digits(),
|
||||
interval_length: default_period()
|
||||
]
|
||||
|
||||
validate_token(secret, token, opts)
|
||||
end
|
||||
|
||||
def validate_token(_, _), do: {:error, :invalid_secret_and_token}
|
||||
|
||||
@doc "See `validate_token/2`"
|
||||
@spec validate_token(String.t(), String.t(), Keyword.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def validate_token(secret, token, options)
|
||||
when is_binary(secret) and is_binary(token) do
|
||||
case :pot.valid_totp(token, secret, options) do
|
||||
true -> {:ok, :pass}
|
||||
false -> {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
|
||||
end
|
@ -0,0 +1,99 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:to, Types.Recipients, default: [])
|
||||
field(:cc, Types.Recipients, default: [])
|
||||
field(:deleted_activity_id, Types.ObjectID)
|
||||
field(:object, Types.ObjectID)
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def add_deleted_activity_id(cng) do
|
||||
object =
|
||||
cng
|
||||
|> get_field(:object)
|
||||
|
||||
with %Activity{id: id} <- Activity.get_create_by_object_ap_id(object) do
|
||||
cng
|
||||
|> put_change(:deleted_activity_id, id)
|
||||
else
|
||||
_ -> cng
|
||||
end
|
||||
end
|
||||
|
||||
@deletable_types ~w{
|
||||
Answer
|
||||
Article
|
||||
Audio
|
||||
Event
|
||||
Note
|
||||
Page
|
||||
Question
|
||||
Video
|
||||
}
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_inclusion(:type, ["Delete"])
|
||||
|> validate_actor_presence()
|
||||
|> validate_deletion_rights()
|
||||
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|
||||
|> add_deleted_activity_id()
|
||||
end
|
||||
|
||||
def do_not_federate?(cng) do
|
||||
!same_domain?(cng)
|
||||
end
|
||||
|
||||
defp same_domain?(cng) do
|
||||
actor_uri =
|
||||
cng
|
||||
|> get_field(:actor)
|
||||
|> URI.parse()
|
||||
|
||||
object_uri =
|
||||
cng
|
||||
|> get_field(:object)
|
||||
|> URI.parse()
|
||||
|
||||
object_uri.host == actor_uri.host
|
||||
end
|
||||
|
||||
def validate_deletion_rights(cng) do
|
||||
actor = User.get_cached_by_ap_id(get_field(cng, :actor))
|
||||
|
||||
if User.superuser?(actor) || same_domain?(cng) do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|> add_error(:actor, "is not allowed to delete object")
|
||||
end
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> validate_data
|
||||
end
|
||||
end
|
@ -0,0 +1,34 @@
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
|
||||
use Ecto.Type
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
|
||||
|
||||
def type, do: {:array, ObjectID}
|
||||
|
||||
def cast(object) when is_binary(object) do
|
||||
cast([object])
|
||||
end
|
||||
|
||||
def cast(data) when is_list(data) do
|
||||
data
|
||||
|> Enum.reduce({:ok, []}, fn element, acc ->
|
||||
case {acc, ObjectID.cast(element)} do
|
||||
{:error, _} -> :error
|
||||
{_, :error} -> :error
|
||||
{{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def cast(_) do
|
||||
:error
|
||||
end
|
||||
|
||||
def dump(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def load(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
@ -0,0 +1,139 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
|
||||
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
||||
@moduledoc """
|
||||
This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
|
||||
(https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
|
||||
The main difference is ignoring unexpected query params instead of throwing
|
||||
an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
|
||||
to disable this behavior. Also, the default rendering error module
|
||||
is `Pleroma.Web.ApiSpec.RenderError`.
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
alias Plug.Conn
|
||||
|
||||
@impl Plug
|
||||
def init(opts) do
|
||||
opts
|
||||
|> Map.new()
|
||||
|> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
|
||||
end
|
||||
|
||||
@impl Plug
|
||||
def call(%{private: %{open_api_spex: private_data}} = conn, %{
|
||||
operation_id: operation_id,
|
||||
render_error: render_error
|
||||
}) do
|
||||
spec = private_data.spec
|
||||
operation = private_data.operation_lookup[operation_id]
|
||||
|
||||
content_type =
|
||||
case Conn.get_req_header(conn, "content-type") do
|
||||
[header_value | _] ->
|
||||
header_value
|
||||
|> String.split(";")
|
||||
|> List.first()
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
private_data = Map.put(private_data, :operation_id, operation_id)
|
||||
conn = Conn.put_private(conn, :open_api_spex, private_data)
|
||||
|
||||
case cast_and_validate(spec, operation, conn, content_type, strict?()) do
|
||||
{:ok, conn} ->
|
||||
conn
|
||||
|
||||
{:error, reason} ->
|
||||
opts = render_error.init(reason)
|
||||
|
||||
conn
|
||||
|> render_error.call(opts)
|
||||
|> Plug.Conn.halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(
|
||||
%{
|
||||
private: %{
|
||||
phoenix_controller: controller,
|
||||
phoenix_action: action,
|
||||
open_api_spex: private_data
|
||||
}
|
||||
} = conn,
|
||||
opts
|
||||
) do
|
||||
operation =
|
||||
case private_data.operation_lookup[{controller, action}] do
|
||||
nil ->
|
||||
operation_id = controller.open_api_operation(action).operationId
|
||||
operation = private_data.operation_lookup[operation_id]
|
||||
|
||||
operation_lookup =
|
||||
private_data.operation_lookup
|
||||
|> Map.put({controller, action}, operation)
|
||||
|
||||
OpenApiSpex.Plug.Cache.adapter().put(
|
||||
private_data.spec_module,
|
||||
{private_data.spec, operation_lookup}
|
||||
)
|
||||
|
||||
operation
|
||||
|
||||
operation ->
|
||||
operation
|
||||
end
|
||||
|
||||
if operation.operationId do
|
||||
call(conn, Map.put(opts, :operation_id, operation.operationId))
|
||||
else
|
||||
raise "operationId was not found in action API spec"
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
|
||||
|
||||
defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
|
||||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||
end
|
||||
|
||||
defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
|
||||
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
|
||||
{:ok, conn} ->
|
||||
{:ok, conn}
|
||||
|
||||
# Remove unexpected query params and cast/validate again
|
||||
{:error, errors} ->
|
||||
query_params =
|
||||
Enum.reduce(errors, conn.query_params, fn
|
||||
%{reason: :unexpected_field, name: name, path: [name]}, params ->
|
||||
Map.delete(params, name)
|
||||
|
||||
%{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
|
||||
path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
|
||||
update_in(params, path, &List.delete(&1, value))
|
||||
|
||||
_, params ->
|
||||
params
|
||||
end)
|
||||
|
||||
conn = %Conn{conn | query_params: query_params}
|
||||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||
end
|
||||
end
|
||||
|
||||
defp list_items_to_string(list) do
|
||||
Enum.map(list, fn
|
||||
i when is_atom(i) -> to_string(i)
|
||||
i -> i
|
||||
end)
|
||||
end
|
||||
|
||||
defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
|
||||
end
|
@ -0,0 +1,61 @@
|
||||
# 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.ConversationOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Conversation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
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: ["Conversations"],
|
||||
summary: "Show conversation",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
operationId: "ConversationController.index",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:recipients,
|
||||
:query,
|
||||
%Schema{type: :array, items: FlakeID},
|
||||
"Only return conversations with the given recipients (a list of user ids)"
|
||||
)
|
||||
| pagination_params()
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Conversation", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: Conversation,
|
||||
example: [Conversation.schema().example]
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def mark_as_read_operation do
|
||||
%Operation{
|
||||
tags: ["Conversations"],
|
||||
summary: "Mark as read",
|
||||
operationId: "ConversationController.mark_as_read",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
],
|
||||
security: [%{"oAuth" => ["write:conversations"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Conversation", "application/json", Conversation)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,227 @@
|
||||
# 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.FilterOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias 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: ["apps"],
|
||||
summary: "View all filters",
|
||||
operationId: "FilterController.index",
|
||||
security: [%{"oAuth" => ["read:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filters", "application/json", array_of_filters())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "Create a filter",
|
||||
operationId: "FilterController.create",
|
||||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
security: [%{"oAuth" => ["write:filters"]}],
|
||||
responses: %{200 => Operation.response("Filter", "application/json", filter())}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "View all filters",
|
||||
parameters: [id_param()],
|
||||
operationId: "FilterController.show",
|
||||
security: [%{"oAuth" => ["read:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filter", "application/json", filter())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "Update a filter",
|
||||
parameters: [id_param()],
|
||||
operationId: "FilterController.update",
|
||||
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
|
||||
security: [%{"oAuth" => ["write:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filter", "application/json", filter())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "Remove a filter",
|
||||
parameters: [id_param()],
|
||||
operationId: "FilterController.delete",
|
||||
security: [%{"oAuth" => ["write:filters"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Filter", "application/json", %Schema{
|
||||
type: :object,
|
||||
description: "Empty object"
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true)
|
||||
end
|
||||
|
||||
defp filter do
|
||||
%Schema{
|
||||
title: "Filter",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
phrase: %Schema{type: :string, description: "The text to be filtered"},
|
||||
context: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
|
||||
description: "The contexts in which the filter should be applied."
|
||||
},
|
||||
expires_at: %Schema{
|
||||
type: :string,
|
||||
format: :"date-time",
|
||||
description:
|
||||
"When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
|
||||
nullable: true
|
||||
},
|
||||
irreversible: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Should matching entities in home and notifications be dropped by the server?"
|
||||
},
|
||||
whole_word: %Schema{
|
||||
type: :boolean,
|
||||
description: "Should the filter consider word boundaries?"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "5580",
|
||||
"phrase" => "@twitter.com",
|
||||
"context" => [
|
||||
"home",
|
||||
"notifications",
|
||||
"public",
|
||||
"thread"
|
||||
],
|
||||
"whole_word" => false,
|
||||
"expires_at" => nil,
|
||||
"irreversible" => true
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_filters do
|
||||
%Schema{
|
||||
title: "ArrayOfFilters",
|
||||
description: "Array of Filters",
|
||||
type: :array,
|
||||
items: filter(),
|
||||
example: [
|
||||
%{
|
||||
"id" => "5580",
|
||||
"phrase" => "@twitter.com",
|
||||
"context" => [
|
||||
"home",
|
||||
"notifications",
|
||||
"public",
|
||||
"thread"
|
||||
],
|
||||
"whole_word" => false,
|
||||
"expires_at" => nil,
|
||||
"irreversible" => true
|
||||
},
|
||||
%{
|
||||
"id" => "6191",
|
||||
"phrase" => ":eurovision2019:",
|
||||
"context" => [
|
||||
"home"
|
||||
],
|
||||
"whole_word" => true,
|
||||
"expires_at" => "2019-05-21T13:47:31.333Z",
|
||||
"irreversible" => false
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "FilterCreateRequest",
|
||||
allOf: [
|
||||
update_request(),
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
irreversible: %Schema{
|
||||
type: :bolean,
|
||||
description:
|
||||
"Should the server irreversibly drop matching entities from home and notifications?",
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
example: %{
|
||||
"phrase" => "knights",
|
||||
"context" => ["home"]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
title: "FilterUpdateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
phrase: %Schema{type: :string, description: "The text to be filtered"},
|
||||
context: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
|
||||
description:
|
||||
"Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
|
||||
},
|
||||
irreversible: %Schema{
|
||||
type: :bolean,
|
||||
description:
|
||||
"Should the server irreversibly drop matching entities from home and notifications?"
|
||||
},
|
||||
whole_word: %Schema{
|
||||
type: :bolean,
|
||||
description: "Consider word boundaries?",
|
||||
default: true
|
||||
}
|
||||
# TODO: probably should implement filter expiration
|
||||
# expires_in: %Schema{
|
||||
# type: :string,
|
||||
# format: :"date-time",
|
||||
# description:
|
||||
# "ISO 8601 Datetime for when the filter expires. Otherwise,
|
||||
# null for a filter that doesn't expire."
|
||||
# }
|
||||
},
|
||||
required: [:phrase, :context],
|
||||
example: %{
|
||||
"phrase" => "knights",
|
||||
"context" => ["home"]
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,65 @@
|
||||
# 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.FollowRequestOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Follow Requests"],
|
||||
summary: "Pending Follows",
|
||||
security: [%{"oAuth" => ["read:follows", "follow"]}],
|
||||
operationId: "FollowRequestController.index",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Account", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: Account,
|
||||
example: [Account.schema().example]
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def authorize_operation do
|
||||
%Operation{
|
||||
tags: ["Follow Requests"],
|
||||
summary: "Accept Follow",
|
||||
operationId: "FollowRequestController.authorize",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def reject_operation do
|
||||
%Operation{
|
||||
tags: ["Follow Requests"],
|
||||
summary: "Reject Follow",
|
||||
operationId: "FollowRequestController.reject",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,169 @@
|
||||
# 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.InstanceOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Instance"],
|
||||
summary: "Fetch instance",
|
||||
description: "Information about the server",
|
||||
operationId: "InstanceController.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Instance", "application/json", instance())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def peers_operation do
|
||||
%Operation{
|
||||
tags: ["Instance"],
|
||||
summary: "List of known hosts",
|
||||
operationId: "InstanceController.peers",
|
||||
responses: %{
|
||||
200 => Operation.response("Array of domains", "application/json", array_of_domains())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp instance do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
uri: %Schema{type: :string, description: "The domain name of the instance"},
|
||||
title: %Schema{type: :string, description: "The title of the website"},
|
||||
description: %Schema{
|
||||
type: :string,
|
||||
description: "Admin-defined description of the Pleroma site"
|
||||
},
|
||||
version: %Schema{
|
||||
type: :string,
|
||||
description: "The version of Pleroma installed on the instance"
|
||||
},
|
||||
email: %Schema{
|
||||
type: :string,
|
||||
description: "An email that may be contacted for any inquiries",
|
||||
format: :email
|
||||
},
|
||||
urls: %Schema{
|
||||
type: :object,
|
||||
description: "URLs of interest for clients apps",
|
||||
properties: %{
|
||||
streaming_api: %Schema{
|
||||
type: :string,
|
||||
description: "Websockets address for push streaming"
|
||||
}
|
||||
}
|
||||
},
|
||||
stats: %Schema{
|
||||
type: :object,
|
||||
description: "Statistics about how much information the instance contains",
|
||||
properties: %{
|
||||
user_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Users registered on this instance"
|
||||
},
|
||||
status_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Statuses authored by users on instance"
|
||||
},
|
||||
domain_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Domains federated with this instance"
|
||||
}
|
||||
}
|
||||
},
|
||||
thumbnail: %Schema{
|
||||
type: :string,
|
||||
description: "Banner image for the website",
|
||||
nullable: true
|
||||
},
|
||||
languages: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Primary langauges of the website and its staff"
|
||||
},
|
||||
registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
|
||||
# Extra (not present in Mastodon):
|
||||
max_toot_chars: %Schema{
|
||||
type: :integer,
|
||||
description: ": Posts character limit (CW/Subject included in the counter)"
|
||||
},
|
||||
poll_limits: %Schema{
|
||||
type: :object,
|
||||
description: "A map with poll limits for local polls",
|
||||
properties: %{
|
||||
max_options: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum number of options."
|
||||
},
|
||||
max_option_chars: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum number of characters per option."
|
||||
},
|
||||
min_expiration: %Schema{
|
||||
type: :integer,
|
||||
description: "Minimum expiration time (in seconds)."
|
||||
},
|
||||
max_expiration: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum expiration time (in seconds)."
|
||||
}
|
||||
}
|
||||
},
|
||||
upload_limit: %Schema{
|
||||
type: :integer,
|
||||
description: "File size limit of uploads (except for avatar, background, banner)"
|
||||
},
|
||||
avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||
background_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||
banner_upload_limit: %Schema{type: :integer, description: "The title of the website"}
|
||||
},
|
||||
example: %{
|
||||
"avatar_upload_limit" => 2_000_000,
|
||||
"background_upload_limit" => 4_000_000,
|
||||
"banner_upload_limit" => 4_000_000,
|
||||
"description" => "A Pleroma instance, an alternative fediverse server",
|
||||
"email" => "lain@lain.com",
|
||||
"languages" => ["en"],
|
||||
"max_toot_chars" => 5000,
|
||||
"poll_limits" => %{
|
||||
"max_expiration" => 31_536_000,
|
||||
"max_option_chars" => 200,
|
||||
"max_options" => 20,
|
||||
"min_expiration" => 0
|
||||
},
|
||||
"registrations" => false,
|
||||
"stats" => %{
|
||||
"domain_count" => 2996,
|
||||
"status_count" => 15_802,
|
||||
"user_count" => 5
|
||||
},
|
||||
"thumbnail" => "https://lain.com/instance/thumbnail.jpeg",
|
||||
"title" => "lain.com",
|
||||
"upload_limit" => 16_000_000,
|
||||
"uri" => "https://lain.com",
|
||||
"urls" => %{
|
||||
"streaming_api" => "wss://lain.com"
|
||||
},
|
||||
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_domains do
|
||||
%Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
example: ["pleroma.site", "lain.com", "bikeshed.party"]
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,188 @@
|
||||
# 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.ListOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.List
|
||||
|
||||
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: ["Lists"],
|
||||
summary: "Show user's lists",
|
||||
description: "Fetch all lists that the user owns",
|
||||
security: [%{"oAuth" => ["read:lists"]}],
|
||||
operationId: "ListController.index",
|
||||
responses: %{
|
||||
200 => Operation.response("Array of List", "application/json", array_of_lists())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Create a list",
|
||||
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
|
||||
operationId: "ListController.create",
|
||||
requestBody: create_update_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("List", "application/json", List),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Show a single list",
|
||||
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
|
||||
operationId: "ListController.show",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["read:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("List", "application/json", List),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Update a list",
|
||||
description: "Change the title of a list",
|
||||
operationId: "ListController.update",
|
||||
parameters: [id_param()],
|
||||
requestBody: create_update_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("List", "application/json", List),
|
||||
422 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Delete a list",
|
||||
operationId: "ListController.delete",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def list_accounts_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "View accounts in list",
|
||||
operationId: "ListController.list_accounts",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["read:lists"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Account", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: Account
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def add_to_list_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Add accounts to list",
|
||||
description: "Add accounts to the given list.",
|
||||
operationId: "ListController.add_to_list",
|
||||
parameters: [id_param()],
|
||||
requestBody: add_remove_accounts_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def remove_from_list_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Remove accounts from list",
|
||||
operationId: "ListController.remove_from_list",
|
||||
parameters: [id_param()],
|
||||
requestBody: add_remove_accounts_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_lists do
|
||||
%Schema{
|
||||
title: "ArrayOfLists",
|
||||
description: "Response schema for lists",
|
||||
type: :array,
|
||||
items: List,
|
||||
example: [
|
||||
%{"id" => "123", "title" => "my list"},
|
||||
%{"id" => "1337", "title" => "another list"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "List ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp create_update_request do
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
description: "POST body for creating or updating a List",
|
||||
type: :object,
|
||||
properties: %{
|
||||
title: %Schema{type: :string, description: "List title"}
|
||||
},
|
||||
required: [:title]
|
||||
},
|
||||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp add_remove_accounts_request do
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
description: "POST body for adding/removing accounts to/from a List",
|
||||
type: :object,
|
||||
properties: %{
|
||||
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
|
||||
},
|
||||
required: [:account_ids]
|
||||
},
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,140 @@
|
||||
# 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.MarkerOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias 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: ["Markers"],
|
||||
summary: "Get saved timeline position",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
operationId: "MarkerController.index",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:timeline,
|
||||
:query,
|
||||
%Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string, enum: ["home", "notifications"]}
|
||||
},
|
||||
"Array of markers to fetch. If not provided, an empty object will be returned."
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Marker", "application/json", response()),
|
||||
403 => Operation.response("Error", "application/json", api_error())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def upsert_operation do
|
||||
%Operation{
|
||||
tags: ["Markers"],
|
||||
summary: "Save position in timeline",
|
||||
operationId: "MarkerController.upsert",
|
||||
requestBody: Helpers.request_body("Parameters", upsert_request(), required: true),
|
||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Marker", "application/json", response()),
|
||||
403 => Operation.response("Error", "application/json", api_error())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp marker do
|
||||
%Schema{
|
||||
title: "Marker",
|
||||
description: "Schema for a marker",
|
||||
type: :object,
|
||||
properties: %{
|
||||
last_read_id: %Schema{type: :string},
|
||||
version: %Schema{type: :integer},
|
||||
updated_at: %Schema{type: :string},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
unread_count: %Schema{type: :integer}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"last_read_id" => "35098814",
|
||||
"version" => 361,
|
||||
"updated_at" => "2019-11-26T22:37:25.239Z",
|
||||
"pleroma" => %{"unread_count" => 5}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp response do
|
||||
%Schema{
|
||||
title: "MarkersResponse",
|
||||
description: "Response schema for markers",
|
||||
type: :object,
|
||||
properties: %{
|
||||
notifications: %Schema{allOf: [marker()], nullable: true},
|
||||
home: %Schema{allOf: [marker()], nullable: true}
|
||||
},
|
||||
items: %Schema{type: :string},
|
||||
example: %{
|
||||
"notifications" => %{
|
||||
"last_read_id" => "35098814",
|
||||
"version" => 361,
|
||||
"updated_at" => "2019-11-26T22:37:25.239Z",
|
||||
"pleroma" => %{"unread_count" => 0}
|
||||
},
|
||||
"home" => %{
|
||||
"last_read_id" => "103206604258487607",
|
||||
"version" => 468,
|
||||
"updated_at" => "2019-11-26T22:37:25.235Z",
|
||||
"pleroma" => %{"unread_count" => 10}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp upsert_request do
|
||||
%Schema{
|
||||
title: "MarkersUpsertRequest",
|
||||
description: "Request schema for marker upsert",
|
||||
type: :object,
|
||||
properties: %{
|
||||
notifications: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
last_read_id: %Schema{type: :string}
|
||||
}
|
||||
},
|
||||
home: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
last_read_id: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"home" => %{
|
||||
"last_read_id" => "103194548672408537",
|
||||
"version" => 462,
|
||||
"updated_at" => "2019-11-24T19:39:39.337Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp api_error do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{error: %Schema{type: :string}}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,76 @@
|
||||
# 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.PollOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Polls"],
|
||||
summary: "View a poll",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "PollController.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Poll", "application/json", Poll),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def vote_operation do
|
||||
%Operation{
|
||||
tags: ["Polls"],
|
||||
summary: "Vote on a poll",
|
||||
parameters: [id_param()],
|
||||
operationId: "PollController.vote",
|
||||
requestBody: vote_request(),
|
||||
security: [%{"oAuth" => ["write:statuses"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Poll", "application/json", Poll),
|
||||
422 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Poll ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp vote_request do
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
choices: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :integer},
|
||||
description: "Array of own votes containing index for each option (starting from 0)"
|
||||
}
|
||||
},
|
||||
required: [:choices]
|
||||
},
|
||||
required: true,
|
||||
example: %{
|
||||
"choices" => [0, 1, 2]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,96 @@
|
||||
# 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.ScheduledActivityOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
|
||||
|
||||
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: ["Scheduled Statuses"],
|
||||
summary: "View scheduled statuses",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: pagination_params(),
|
||||
operationId: "ScheduledActivity.index",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of ScheduledStatus", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: ScheduledStatus
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Scheduled Statuses"],
|
||||
summary: "View a single scheduled status",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "ScheduledActivity.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Scheduled Statuses"],
|
||||
summary: "Schedule a status",
|
||||
operationId: "ScheduledActivity.update",
|
||||
security: [%{"oAuth" => ["write:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
scheduled_at: %Schema{
|
||||
type: :string,
|
||||
format: :"date-time",
|
||||
description:
|
||||
"ISO 8601 Datetime at which the status will be published. Must be at least 5 minutes into the future."
|
||||
}
|
||||
}
|
||||
}),
|
||||
responses: %{
|
||||
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Scheduled Statuses"],
|
||||
summary: "Cancel a scheduled status",
|
||||
security: [%{"oAuth" => ["write:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "ScheduledActivity.delete",
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Poll ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,188 @@
|
||||
# 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.SubscriptionOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.PushSubscription
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Push Subscriptions"],
|
||||
summary: "Subscribe to push notifications",
|
||||
description:
|
||||
"Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
|
||||
operationId: "SubscriptionController.create",
|
||||
security: [%{"oAuth" => ["push"]}],
|
||||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Push Subscriptions"],
|
||||
summary: "Get current subscription",
|
||||
description: "View the PushSubscription currently associated with this access token.",
|
||||
operationId: "SubscriptionController.show",
|
||||
security: [%{"oAuth" => ["push"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||
403 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Push Subscriptions"],
|
||||
summary: "Change types of notifications",
|
||||
description:
|
||||
"Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
|
||||
operationId: "SubscriptionController.update",
|
||||
security: [%{"oAuth" => ["push"]}],
|
||||
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Push Subscriptions"],
|
||||
summary: "Remove current subscription",
|
||||
description: "Removes the current Web Push API subscription.",
|
||||
operationId: "SubscriptionController.delete",
|
||||
security: [%{"oAuth" => ["push"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
|
||||
403 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "SubscriptionCreateRequest",
|
||||
description: "POST body for creating a push subscription",
|
||||
type: :object,
|
||||
properties: %{
|
||||
subscription: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
endpoint: %Schema{
|
||||
type: :string,
|
||||
description: "Endpoint URL that is called when a notification event occurs."
|
||||
},
|
||||
keys: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
p256dh: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"User agent public key. Base64 encoded string of public key of ECDH key using `prime256v1` curve."
|
||||
},
|
||||
auth: %Schema{
|
||||
type: :string,
|
||||
description: "Auth secret. Base64 encoded string of 16 bytes of random data."
|
||||
}
|
||||
},
|
||||
required: [:p256dh, :auth]
|
||||
}
|
||||
},
|
||||
required: [:endpoint, :keys]
|
||||
},
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
alerts: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
|
||||
favourite: %Schema{
|
||||
type: :boolean,
|
||||
description: "Receive favourite notifications?"
|
||||
},
|
||||
reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"},
|
||||
mention: %Schema{type: :boolean, description: "Receive mention notifications?"},
|
||||
poll: %Schema{type: :boolean, description: "Receive poll notifications?"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [:subscription],
|
||||
example: %{
|
||||
"subscription" => %{
|
||||
"endpoint" => "https://example.com/example/1234",
|
||||
"keys" => %{
|
||||
"auth" => "8eDyX_uCN0XRhSbY5hs7Hg==",
|
||||
"p256dh" =>
|
||||
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA="
|
||||
}
|
||||
},
|
||||
"data" => %{
|
||||
"alerts" => %{
|
||||
"follow" => true,
|
||||
"mention" => true,
|
||||
"poll" => false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
title: "SubscriptionUpdateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
alerts: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
|
||||
favourite: %Schema{
|
||||
type: :boolean,
|
||||
description: "Receive favourite notifications?"
|
||||
},
|
||||
reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"},
|
||||
mention: %Schema{type: :boolean, description: "Receive mention notifications?"},
|
||||
poll: %Schema{type: :boolean, description: "Receive poll notifications?"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"data" => %{
|
||||
"alerts" => %{
|
||||
"follow" => true,
|
||||
"favourite" => true,
|
||||
"reblog" => true,
|
||||
"mention" => true,
|
||||
"poll" => true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,68 @@
|
||||
# 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.Schemas.Attachment do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Attachment",
|
||||
description: "Represents a file or media attachment that can be added to a status.",
|
||||
type: :object,
|
||||
requried: [:id, :url, :preview_url],
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "The location of the original full-size attachment"
|
||||
},
|
||||
remote_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description:
|
||||
"The location of the full-size original attachment on the remote website. String (URL), or null if the attachment is local",
|
||||
nullable: true
|
||||
},
|
||||
preview_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "The location of a scaled-down preview of the attachment"
|
||||
},
|
||||
text_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "A shorter URL for the attachment"
|
||||
},
|
||||
description: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description:
|
||||
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load"
|
||||
},
|
||||
type: %Schema{
|
||||
type: :string,
|
||||
enum: ["image", "video", "audio", "unknown"],
|
||||
description: "The type of the attachment"
|
||||
},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
mime_type: %Schema{type: :string, description: "mime type of the attachment"}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
id: "1638338801",
|
||||
type: "image",
|
||||
url: "someurl",
|
||||
remote_url: "someurl",
|
||||
preview_url: "someurl",
|
||||
text_url: "someurl",
|
||||
description: nil,
|
||||
pleroma: %{mime_type: "image/png"}
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
# 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.Schemas.Conversation do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Conversation",
|
||||
description: "Represents a conversation with \"direct message\" visibility.",
|
||||
type: :object,
|
||||
required: [:id, :accounts, :unread],
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
accounts: %Schema{
|
||||
type: :array,
|
||||
items: Account,
|
||||
description: "Participants in the conversation"
|
||||
},
|
||||
unread: %Schema{
|
||||
type: :boolean,
|
||||
description: "Is the conversation currently marked as unread?"
|
||||
},
|
||||
# last_status: Status
|
||||
last_status: %Schema{
|
||||
allOf: [Status],
|
||||
description: "The last status in the conversation, to be used for optional display"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "418450",
|
||||
"unread" => true,
|
||||
"accounts" => [Account.schema().example],
|
||||
"last_status" => Status.schema().example
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,23 @@
|
||||
# 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.Schemas.List do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "List",
|
||||
description: "Represents a list of users",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "The internal database ID of the list"},
|
||||
title: %Schema{type: :string, description: "The user-defined title of the list"}
|
||||
},
|
||||
example: %{
|
||||
"id" => "12249",
|
||||
"title" => "Friends"
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,66 @@
|
||||
# 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.Schemas.PushSubscription do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PushSubscription",
|
||||
description: "Response schema for a push subscription",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{
|
||||
anyOf: [%Schema{type: :string}, %Schema{type: :integer}],
|
||||
description: "The id of the push subscription in the database."
|
||||
},
|
||||
endpoint: %Schema{type: :string, description: "Where push alerts will be sent to."},
|
||||
server_key: %Schema{type: :string, description: "The streaming server's VAPID key."},
|
||||
alerts: %Schema{
|
||||
type: :object,
|
||||
description: "Which alerts should be delivered to the endpoint.",
|
||||
properties: %{
|
||||
follow: %Schema{
|
||||
type: :boolean,
|
||||
description: "Receive a push notification when someone has followed you?"
|
||||
},
|
||||
favourite: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Receive a push notification when a status you created has been favourited by someone else?"
|
||||
},
|
||||
reblog: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Receive a push notification when a status you created has been boosted by someone else?"
|
||||
},
|
||||
mention: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Receive a push notification when someone else has mentioned you in a status?"
|
||||
},
|
||||
poll: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Receive a push notification when a poll you voted in or created has ended? "
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "328_183",
|
||||
"endpoint" => "https://yourdomain.example/listener",
|
||||
"alerts" => %{
|
||||
"follow" => true,
|
||||
"favourite" => true,
|
||||
"reblog" => true,
|
||||
"mention" => true,
|
||||
"poll" => true
|
||||
},
|
||||
"server_key" =>
|
||||
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M="
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,54 @@
|
||||
# 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.Schemas.ScheduledStatus do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Attachment
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ScheduledStatus",
|
||||
description: "Represents a status that will be published at a future scheduled date.",
|
||||
type: :object,
|
||||
required: [:id, :scheduled_at, :params],
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
scheduled_at: %Schema{type: :string, format: :"date-time"},
|
||||
media_attachments: %Schema{type: :array, items: Attachment},
|
||||
params: %Schema{
|
||||
type: :object,
|
||||
required: [:text, :visibility],
|
||||
properties: %{
|
||||
text: %Schema{type: :string, nullable: true},
|
||||
media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
|
||||
sensitive: %Schema{type: :boolean, nullable: true},
|
||||
spoiler_text: %Schema{type: :string, nullable: true},
|
||||
visibility: %Schema{type: VisibilityScope, nullable: true},
|
||||
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||
poll: %Schema{type: Poll, nullable: true},
|
||||
in_reply_to_id: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
id: "3221",
|
||||
scheduled_at: "2019-12-05T12:33:01.000Z",
|
||||
params: %{
|
||||
text: "test content",
|
||||
media_ids: nil,
|
||||
sensitive: nil,
|
||||
spoiler_text: nil,
|
||||
visibility: nil,
|
||||
scheduled_at: nil,
|
||||
poll: nil,
|
||||
idempotency: nil,
|
||||
in_reply_to_id: nil
|
||||
},
|
||||
media_attachments: [Attachment.schema().example]
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,45 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.TOTPAuthenticator do
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.User
|
||||
|
||||
@doc "Verify code or check backup code."
|
||||
@spec verify(String.t(), User.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def verify(
|
||||
token,
|
||||
%User{
|
||||
multi_factor_authentication_settings:
|
||||
%{enabled: true, totp: %{secret: secret, confirmed: true}} = _
|
||||
} = _user
|
||||
)
|
||||
when is_binary(token) and byte_size(token) > 0 do
|
||||
TOTP.validate_token(secret, token)
|
||||
end
|
||||
|
||||
def verify(_, _), do: {:error, :invalid_token}
|
||||
|
||||
@spec verify_recovery_code(User.t(), String.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token}
|
||||
def verify_recovery_code(
|
||||
%User{multi_factor_authentication_settings: %{enabled: true, backup_codes: codes}} = user,
|
||||
code
|
||||
)
|
||||
when is_list(codes) and is_binary(code) do
|
||||
hash_code = Enum.find(codes, fn hash -> Pbkdf2.checkpw(code, hash) end)
|
||||
|
||||
if hash_code do
|
||||
MFA.invalidate_backup_code(user, hash_code)
|
||||
{:ok, :pass}
|
||||
else
|
||||
{:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
def verify_recovery_code(_, _), do: {:error, :invalid_token}
|
||||
end
|
@ -0,0 +1,97 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.MFAController do
|
||||
@moduledoc """
|
||||
The model represents api to use Multi Factor authentications.
|
||||
"""
|
||||
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Web.Auth.TOTPAuthenticator
|
||||
alias Pleroma.Web.OAuth.MFAView, as: View
|
||||
alias Pleroma.Web.OAuth.OAuthController
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
plug(:fetch_session when action in [:show, :verify])
|
||||
plug(:fetch_flash when action in [:show, :verify])
|
||||
|
||||
@doc """
|
||||
Display form to input mfa code or recovery code.
|
||||
"""
|
||||
def show(conn, %{"mfa_token" => mfa_token} = params) do
|
||||
template = Map.get(params, "challenge_type", "totp")
|
||||
|
||||
conn
|
||||
|> put_view(View)
|
||||
|> render("#{template}.html", %{
|
||||
mfa_token: mfa_token,
|
||||
redirect_uri: params["redirect_uri"],
|
||||
state: params["state"]
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verification code and continue authorization.
|
||||
"""
|
||||
def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do
|
||||
with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
|
||||
{:ok, _} <- validates_challenge(user, mfa_params) do
|
||||
conn
|
||||
|> OAuthController.after_create_authorization(auth, %{
|
||||
"authorization" => %{
|
||||
"redirect_uri" => mfa_params["redirect_uri"],
|
||||
"state" => mfa_params["state"]
|
||||
}
|
||||
})
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_flash(:error, "Two-factor authentication failed.")
|
||||
|> put_status(:unauthorized)
|
||||
|> show(mfa_params)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verification second step of MFA (or recovery) and returns access token.
|
||||
|
||||
## Endpoint
|
||||
POST /oauth/mfa/challenge
|
||||
|
||||
params:
|
||||
`client_id`
|
||||
`client_secret`
|
||||
`mfa_token` - access token to check second step of mfa
|
||||
`challenge_type` - 'totp' or 'recovery'
|
||||
`code`
|
||||
|
||||
"""
|
||||
def challenge(conn, %{"mfa_token" => mfa_token} = params) do
|
||||
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
||||
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
|
||||
{:ok, _} <- validates_challenge(user, params),
|
||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||
json(conn, Token.Response.build(user, token))
|
||||
else
|
||||
_error ->
|
||||
conn
|
||||
|> put_status(400)
|
||||
|> json(%{error: "Invalid code"})
|
||||
end
|
||||
end
|
||||
|
||||
# Verify TOTP Code
|
||||
defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do
|
||||
TOTPAuthenticator.verify(code, user)
|
||||
end
|
||||
|
||||
# Verify Recovery Code
|
||||
defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do
|
||||
TOTPAuthenticator.verify_recovery_code(user, code)
|
||||
end
|
||||
|
||||
defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type}
|
||||
end
|
@ -0,0 +1,8 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.MFAView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML.Form
|
||||
end
|
@ -0,0 +1,38 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.Token.CleanWorker do
|
||||
@moduledoc """
|
||||
The module represents functions to clean an expired OAuth and MFA tokens.
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
@ten_seconds 10_000
|
||||
@one_day 86_400_000
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Web.OAuth
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
|
||||
def start_link(_), do: GenServer.start_link(__MODULE__, %{})
|
||||
|
||||
def init(_) do
|
||||
Process.send_after(self(), :perform, @ten_seconds)
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(:perform, state) do
|
||||
BackgroundWorker.enqueue("clean_expired_tokens", %{})
|
||||
interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day)
|
||||
|
||||
Process.send_after(self(), :perform, interval)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def perform(:clean) do
|
||||
OAuth.Token.delete_expired_tokens()
|
||||
MFA.Token.delete_expired_tokens()
|
||||
end
|
||||
end
|
@ -0,0 +1,133 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do
|
||||
@moduledoc "The module represents actions to manage MFA"
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings])
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:security"]} when action in [:setup, :confirm, :disable, :backup_codes]
|
||||
)
|
||||
|
||||
@doc """
|
||||
Gets user multi factor authentication settings
|
||||
|
||||
## Endpoint
|
||||
GET /api/pleroma/accounts/mfa
|
||||
|
||||
"""
|
||||
def settings(%{assigns: %{user: user}} = conn, _params) do
|
||||
json(conn, %{settings: MFA.mfa_settings(user)})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prepare setup mfa method
|
||||
|
||||
## Endpoint
|
||||
GET /api/pleroma/accounts/mfa/setup/[:method]
|
||||
|
||||
"""
|
||||
def setup(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = _params) do
|
||||
with {:ok, user} <- MFA.setup_totp(user),
|
||||
%{secret: secret} = _ <- user.multi_factor_authentication_settings.totp do
|
||||
provisioning_uri = TOTP.provisioning_uri(secret, "#{user.email}")
|
||||
|
||||
json(conn, %{provisioning_uri: provisioning_uri, key: secret})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
|
||||
def setup(conn, _params) do
|
||||
json_response(conn, :bad_request, %{error: "undefined method"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Confirms setup and enable mfa method
|
||||
|
||||
## Endpoint
|
||||
POST /api/pleroma/accounts/mfa/confirm/:method
|
||||
|
||||
- params:
|
||||
`code` - confirmation code
|
||||
`password` - current password
|
||||
"""
|
||||
def confirm(
|
||||
%{assigns: %{user: user}} = conn,
|
||||
%{"method" => "totp", "password" => _, "code" => _} = params
|
||||
) do
|
||||
with {:ok, _user} <- Utils.confirm_current_password(user, params["password"]),
|
||||
{:ok, _user} <- MFA.confirm_totp(user, params) do
|
||||
json(conn, %{})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
|
||||
def confirm(conn, _) do
|
||||
json_response(conn, :bad_request, %{error: "undefined mfa method"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disable mfa method and disable mfa if need.
|
||||
"""
|
||||
def disable(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = params) do
|
||||
with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
|
||||
{:ok, _user} <- MFA.disable_totp(user) do
|
||||
json(conn, %{})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
|
||||
def disable(%{assigns: %{user: user}} = conn, %{"method" => "mfa"} = params) do
|
||||
with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
|
||||
{:ok, _user} <- MFA.disable(user) do
|
||||
json(conn, %{})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
|
||||
def disable(conn, _) do
|
||||
json_response(conn, :bad_request, %{error: "undefined mfa method"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates backup codes.
|
||||
|
||||
## Endpoint
|
||||
GET /api/pleroma/accounts/mfa/backup_codes
|
||||
|
||||
## Response
|
||||
### Success
|
||||
`{codes: [codes]}`
|
||||
|
||||
### Error
|
||||
`{error: [error_message]}`
|
||||
|
||||
"""
|
||||
def backup_codes(%{assigns: %{user: user}} = conn, _params) do
|
||||
with {:ok, codes} <- MFA.generate_backup_codes(user) do
|
||||
json(conn, %{codes: codes})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
end
|
@ -1,37 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Streamer.Ping do
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Web.Streamer.State
|
||||
alias Pleroma.Web.Streamer.StreamerSocket
|
||||
|
||||
@keepalive_interval :timer.seconds(30)
|
||||
|
||||
def start_link(opts) do
|
||||
ping_interval = Keyword.get(opts, :ping_interval, @keepalive_interval)
|
||||
GenServer.start_link(__MODULE__, %{ping_interval: ping_interval}, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(%{ping_interval: ping_interval} = args) do
|
||||
Process.send_after(self(), :ping, ping_interval)
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
def handle_info(:ping, %{ping_interval: ping_interval} = state) do
|
||||
State.get_sockets()
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> Enum.each(fn %StreamerSocket{transport_pid: transport_pid} ->
|
||||
Logger.debug("Sending keepalive ping")
|
||||
send(transport_pid, {:text, ""})
|
||||
end)
|
||||
|
||||
Process.send_after(self(), :ping, ping_interval)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
@ -1,82 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Streamer.State do
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Web.Streamer.StreamerSocket
|
||||
|
||||
@env Mix.env()
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, %{sockets: %{}}, name: __MODULE__)
|
||||
end
|
||||
|
||||
def add_socket(topic, socket) do
|
||||
GenServer.call(__MODULE__, {:add, topic, socket})
|
||||
end
|
||||
|
||||
def remove_socket(topic, socket) do
|
||||
do_remove_socket(@env, topic, socket)
|
||||
end
|
||||
|
||||
def get_sockets do
|
||||
%{sockets: stream_sockets} = GenServer.call(__MODULE__, :get_state)
|
||||
stream_sockets
|
||||
end
|
||||
|
||||
def init(init_arg) do
|
||||
{:ok, init_arg}
|
||||
end
|
||||
|
||||
def handle_call(:get_state, _from, state) do
|
||||
{:reply, state, state}
|
||||
end
|
||||
|
||||
def handle_call({:add, topic, socket}, _from, %{sockets: sockets} = state) do
|
||||
internal_topic = internal_topic(topic, socket)
|
||||
stream_socket = StreamerSocket.from_socket(socket)
|
||||
|
||||
sockets_for_topic =
|
||||
sockets
|
||||
|> Map.get(internal_topic, [])
|
||||
|> List.insert_at(0, stream_socket)
|
||||
|> Enum.uniq()
|
||||
|
||||
state = put_in(state, [:sockets, internal_topic], sockets_for_topic)
|
||||
Logger.debug("Got new conn for #{topic}")
|
||||
{:reply, state, state}
|
||||
end
|
||||
|
||||
def handle_call({:remove, topic, socket}, _from, %{sockets: sockets} = state) do
|
||||
internal_topic = internal_topic(topic, socket)
|
||||
stream_socket = StreamerSocket.from_socket(socket)
|
||||
|
||||
sockets_for_topic =
|
||||
sockets
|
||||
|> Map.get(internal_topic, [])
|
||||
|> List.delete(stream_socket)
|
||||
|
||||
state = Kernel.put_in(state, [:sockets, internal_topic], sockets_for_topic)
|
||||
{:reply, state, state}
|
||||
end
|
||||
|
||||
defp do_remove_socket(:test, _, _) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp do_remove_socket(_env, topic, socket) do
|
||||
GenServer.call(__MODULE__, {:remove, topic, socket})
|
||||
end
|
||||
|
||||
defp internal_topic(topic, socket)
|
||||
when topic in ~w[user user:notification direct] do
|
||||
"#{topic}:#{socket.assigns[:user].id}"
|
||||
end
|
||||
|
||||
defp internal_topic(topic, _) do
|
||||
topic
|
||||
end
|
||||
end
|
@ -1,35 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Streamer.StreamerSocket do
|
||||
defstruct transport_pid: nil, user: nil
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Streamer.StreamerSocket
|
||||
|
||||
def from_socket(%{
|
||||
transport_pid: transport_pid,
|
||||
assigns: %{user: nil}
|
||||
}) do
|
||||
%StreamerSocket{
|
||||
transport_pid: transport_pid
|
||||
}
|
||||
end
|
||||
|
||||
def from_socket(%{
|
||||
transport_pid: transport_pid,
|
||||
assigns: %{user: %User{} = user}
|
||||
}) do
|
||||
%StreamerSocket{
|
||||
transport_pid: transport_pid,
|
||||
user: user
|
||||
}
|
||||
end
|
||||
|
||||
def from_socket(%{transport_pid: transport_pid}) do
|
||||
%StreamerSocket{
|
||||
transport_pid: transport_pid
|
||||
}
|
||||
end
|
||||
end
|
@ -1,37 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Streamer.Supervisor do
|
||||
use Supervisor
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(args) do
|
||||
children = [
|
||||
{Pleroma.Web.Streamer.State, args},
|
||||
{Pleroma.Web.Streamer.Ping, args},
|
||||
:poolboy.child_spec(:streamer_worker, poolboy_config())
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
|
||||
Supervisor.init(children, opts)
|
||||
end
|
||||
|
||||
defp poolboy_config do
|
||||
opts =
|
||||
Pleroma.Config.get(:streamer,
|
||||
workers: 3,
|
||||
overflow_workers: 2
|
||||
)
|
||||
|
||||
[
|
||||
{:name, {:local, :streamer_worker}},
|
||||
{:worker_module, Pleroma.Web.Streamer.Worker},
|
||||
{:size, opts[:workers]},
|
||||
{:max_overflow, opts[:overflow_workers]}
|
||||
]
|
||||
end
|
||||
end
|
@ -1,208 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Streamer.Worker do
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Streamer.State
|
||||
alias Pleroma.Web.Streamer.StreamerSocket
|
||||
alias Pleroma.Web.StreamerView
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, %{}, [])
|
||||
end
|
||||
|
||||
def init(init_arg) do
|
||||
{:ok, init_arg}
|
||||
end
|
||||
|
||||
def stream(pid, topics, items) do
|
||||
GenServer.call(pid, {:stream, topics, items})
|
||||
end
|
||||
|
||||
def handle_call({:stream, topics, item}, _from, state) when is_list(topics) do
|
||||
Enum.each(topics, fn t ->
|
||||
do_stream(%{topic: t, item: item})
|
||||
end)
|
||||
|
||||
{:reply, state, state}
|
||||
end
|
||||
|
||||
def handle_call({:stream, topic, items}, _from, state) when is_list(items) do
|
||||
Enum.each(items, fn i ->
|
||||
do_stream(%{topic: topic, item: i})
|
||||
end)
|
||||
|
||||
{:reply, state, state}
|
||||
end
|
||||
|
||||
def handle_call({:stream, topic, item}, _from, state) do
|
||||
do_stream(%{topic: topic, item: item})
|
||||
|
||||
{:reply, state, state}
|
||||
end
|
||||
|
||||
defp do_stream(%{topic: "direct", item: item}) do
|
||||
recipient_topics =
|
||||
User.get_recipients_from_activity(item)
|
||||
|> Enum.map(fn %{id: id} -> "direct:#{id}" end)
|
||||
|
||||
Enum.each(recipient_topics, fn user_topic ->
|
||||
Logger.debug("Trying to push direct message to #{user_topic}\n\n")
|
||||
push_to_socket(State.get_sockets(), user_topic, item)
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_stream(%{topic: "participation", item: participation}) do
|
||||
user_topic = "direct:#{participation.user_id}"
|
||||
Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
|
||||
|
||||
push_to_socket(State.get_sockets(), user_topic, participation)
|
||||
end
|
||||
|
||||
defp do_stream(%{topic: "list", item: item}) do
|
||||
# filter the recipient list if the activity is not public, see #270.
|
||||
recipient_lists =
|
||||
case Visibility.is_public?(item) do
|
||||
true ->
|
||||
Pleroma.List.get_lists_from_activity(item)
|
||||
|
||||
_ ->
|
||||
Pleroma.List.get_lists_from_activity(item)
|
||||
|> Enum.filter(fn list ->
|
||||
owner = User.get_cached_by_id(list.user_id)
|
||||
|
||||
Visibility.visible_for_user?(item, owner)
|
||||
end)
|
||||
end
|
||||
|
||||
recipient_topics =
|
||||
recipient_lists
|
||||
|> Enum.map(fn %{id: id} -> "list:#{id}" end)
|
||||
|
||||
Enum.each(recipient_topics, fn list_topic ->
|
||||
Logger.debug("Trying to push message to #{list_topic}\n\n")
|
||||
push_to_socket(State.get_sockets(), list_topic, item)
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_stream(%{topic: topic, item: %Notification{} = item})
|
||||
when topic in ["user", "user:notification"] do
|
||||
State.get_sockets()
|
||||
|> Map.get("#{topic}:#{item.user_id}", [])
|
||||
|> Enum.each(fn %StreamerSocket{transport_pid: transport_pid, user: socket_user} ->
|
||||
with %User{} = user <- User.get_cached_by_ap_id(socket_user.ap_id),
|
||||
true <- should_send?(user, item) do
|
||||
send(transport_pid, {:text, StreamerView.render("notification.json", socket_user, item)})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_stream(%{topic: "user", item: item}) do
|
||||
Logger.debug("Trying to push to users")
|
||||
|
||||
recipient_topics =
|
||||
User.get_recipients_from_activity(item)
|
||||
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
|
||||
|
||||
Enum.each(recipient_topics, fn topic ->
|
||||
push_to_socket(State.get_sockets(), topic, item)
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_stream(%{topic: topic, item: item}) do
|
||||
Logger.debug("Trying to push to #{topic}")
|
||||
Logger.debug("Pushing item to #{topic}")
|
||||
push_to_socket(State.get_sockets(), topic, item)
|
||||
end
|
||||
|
||||
defp should_send?(%User{} = user, %Activity{} = item) do
|
||||
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
|
||||
User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
|
||||
|
||||
recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
|
||||
recipients = MapSet.new(item.recipients)
|
||||
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
|
||||
|
||||
with parent <- Object.normalize(item) || item,
|
||||
true <-
|
||||
Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
|
||||
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
|
||||
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
|
||||
true <- MapSet.disjoint?(recipients, recipient_blocks),
|
||||
%{host: item_host} <- URI.parse(item.actor),
|
||||
%{host: parent_host} <- URI.parse(parent.data["actor"]),
|
||||
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
|
||||
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
|
||||
true <- thread_containment(item, user),
|
||||
false <- CommonAPI.thread_muted?(user, item) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp should_send?(%User{} = user, %Notification{activity: activity}) do
|
||||
should_send?(user, activity)
|
||||
end
|
||||
|
||||
def push_to_socket(topics, topic, %Participation{} = participation) do
|
||||
Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} ->
|
||||
send(transport_pid, {:text, StreamerView.render("conversation.json", participation)})
|
||||
end)
|
||||
end
|
||||
|
||||
def push_to_socket(topics, topic, %Activity{
|
||||
data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}
|
||||
}) do
|
||||
Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} ->
|
||||
send(
|
||||
transport_pid,
|
||||
{:text, %{event: "delete", payload: to_string(deleted_activity_id)} |> Jason.encode!()}
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def push_to_socket(_topics, _topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
|
||||
|
||||
def push_to_socket(topics, topic, item) do
|
||||
Enum.each(topics[topic] || [], fn %StreamerSocket{
|
||||
transport_pid: transport_pid,
|
||||
user: socket_user
|
||||
} ->
|
||||
# Get the current user so we have up-to-date blocks etc.
|
||||
if socket_user do
|
||||
user = User.get_cached_by_ap_id(socket_user.ap_id)
|
||||
|
||||
if should_send?(user, item) do
|
||||
send(transport_pid, {:text, StreamerView.render("update.json", item, user)})
|
||||
end
|
||||
else
|
||||
send(transport_pid, {:text, StreamerView.render("update.json", item)})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec thread_containment(Activity.t(), User.t()) :: boolean()
|
||||
defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true
|
||||
|
||||
defp thread_containment(activity, user) do
|
||||
if Config.get([:instance, :skip_thread_containment]) do
|
||||
true
|
||||
else
|
||||
ActivityPub.contain_activity(activity, user)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,24 @@
|
||||
<%= if get_flash(@conn, :info) do %>
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<% end %>
|
||||
<%= if get_flash(@conn, :error) do %>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<% end %>
|
||||
|
||||
<h2>Two-factor recovery</h2>
|
||||
|
||||
<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
||||
<div class="input">
|
||||
<%= label f, :code, "Recovery code" %>
|
||||
<%= text_input f, :code %>
|
||||
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
||||
<%= hidden_input f, :state, value: @state %>
|
||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||
<%= hidden_input f, :challenge_type, value: "recovery" %>
|
||||
</div>
|
||||
|
||||
<%= submit "Verify" %>
|
||||
<% end %>
|
||||
<a href="<%= mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
|
||||
Enter a two-factor code
|
||||
</a>
|
@ -0,0 +1,24 @@
|
||||
<%= if get_flash(@conn, :info) do %>
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<% end %>
|
||||
<%= if get_flash(@conn, :error) do %>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<% end %>
|
||||
|
||||
<h2>Two-factor authentication</h2>
|
||||
|
||||
<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
||||
<div class="input">
|
||||
<%= label f, :code, "Authentication code" %>
|
||||
<%= text_input f, :code %>
|
||||
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
||||
<%= hidden_input f, :state, value: @state %>
|
||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||
<%= hidden_input f, :challenge_type, value: "totp" %>
|
||||
</div>
|
||||
|
||||
<%= submit "Verify" %>
|
||||
<% end %>
|
||||
<a href="<%= mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
|
||||
Enter a two-factor recovery code
|
||||
</a>
|
@ -0,0 +1,13 @@
|
||||
<%= if @error do %>
|
||||
<h2><%= @error %></h2>
|
||||
<% end %>
|
||||
<h2>Two-factor authentication</h2>
|
||||
<p><%= @followee.nickname %></p>
|
||||
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
|
||||
<%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
|
||||
<%= text_input f, :code, placeholder: "Authentication code", required: true %>
|
||||
<br>
|
||||
<%= hidden_input f, :id, value: @followee.id %>
|
||||
<%= hidden_input f, :token, value: @mfa_token %>
|
||||
<%= submit "Authorize" %>
|
||||
<% end %>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue