aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/user.ex7
-rw-r--r--lib/pleroma/application.ex9
-rw-r--r--lib/pleroma/conversation/participation.ex4
-rw-r--r--lib/pleroma/filter.ex9
-rw-r--r--lib/pleroma/mfa.ex156
-rw-r--r--lib/pleroma/mfa/backup_codes.ex31
-rw-r--r--lib/pleroma/mfa/changeset.ex64
-rw-r--r--lib/pleroma/mfa/settings.ex24
-rw-r--r--lib/pleroma/mfa/token.ex106
-rw-r--r--lib/pleroma/mfa/totp.ex86
-rw-r--r--lib/pleroma/plugs/ensure_authenticated_plug.ex29
-rw-r--r--lib/pleroma/plugs/federating_plug.ex3
-rw-r--r--lib/pleroma/stats.ex2
-rw-r--r--lib/pleroma/user.ex29
-rw-r--r--lib/pleroma/user/query.ex11
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex89
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex5
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex27
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex26
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_validations.ex61
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/delete_validator.ex99
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/like_validator.ex4
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/recipients.ex34
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex4
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex44
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex32
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex2
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex82
-rw-r--r--lib/pleroma/web/admin_api/search.ex1
-rw-r--r--lib/pleroma/web/api_spec.ex7
-rw-r--r--lib/pleroma/web/api_spec/cast_and_validate.ex139
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex19
-rw-r--r--lib/pleroma/web/api_spec/operations/conversation_operation.ex61
-rw-r--r--lib/pleroma/web/api_spec/operations/filter_operation.ex227
-rw-r--r--lib/pleroma/web/api_spec/operations/follow_request_operation.ex65
-rw-r--r--lib/pleroma/web/api_spec/operations/instance_operation.ex169
-rw-r--r--lib/pleroma/web/api_spec/operations/list_operation.ex188
-rw-r--r--lib/pleroma/web/api_spec/operations/marker_operation.ex140
-rw-r--r--lib/pleroma/web/api_spec/operations/poll_operation.ex76
-rw-r--r--lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex96
-rw-r--r--lib/pleroma/web/api_spec/operations/subscription_operation.ex188
-rw-r--r--lib/pleroma/web/api_spec/render_error.ex3
-rw-r--r--lib/pleroma/web/api_spec/schemas/attachment.ex68
-rw-r--r--lib/pleroma/web/api_spec/schemas/conversation.ex41
-rw-r--r--lib/pleroma/web/api_spec/schemas/list.ex23
-rw-r--r--lib/pleroma/web/api_spec/schemas/poll.ex62
-rw-r--r--lib/pleroma/web/api_spec/schemas/push_subscription.ex66
-rw-r--r--lib/pleroma/web/api_spec/schemas/scheduled_status.ex54
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex25
-rw-r--r--lib/pleroma/web/auth/pleroma_authenticator.ex4
-rw-r--r--lib/pleroma/web/auth/totp_authenticator.ex45
-rw-r--r--lib/pleroma/web/common_api/common_api.ex4
-rw-r--r--lib/pleroma/web/common_api/utils.ex1
-rw-r--r--lib/pleroma/web/feed/user_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex57
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex26
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/views/filter_view.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/marker_view.ex13
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex47
-rw-r--r--lib/pleroma/web/oauth/mfa_controller.ex97
-rw-r--r--lib/pleroma/web/oauth/mfa_view.ex8
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex48
-rw-r--r--lib/pleroma/web/oauth/token/clean_worker.ex38
-rw-r--r--lib/pleroma/web/oauth/token/response.ex9
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex133
-rw-r--r--lib/pleroma/web/push/subscription.ex10
-rw-r--r--lib/pleroma/web/router.ex18
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex2
-rw-r--r--lib/pleroma/web/streamer/ping.ex37
-rw-r--r--lib/pleroma/web/streamer/state.ex82
-rw-r--r--lib/pleroma/web/streamer/streamer.ex244
-rw-r--r--lib/pleroma/web/streamer/streamer_socket.ex35
-rw-r--r--lib/pleroma/web/streamer/supervisor.ex37
-rw-r--r--lib/pleroma/web/streamer/worker.ex208
-rw-r--r--lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex24
-rw-r--r--lib/pleroma/web/templates/o_auth/mfa/totp.html.eex24
-rw-r--r--lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex13
-rw-r--r--lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex47
-rw-r--r--lib/pleroma/web/web.ex10
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex67
94 files changed, 3443 insertions, 820 deletions
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 40dd9bdc0..da140ac86 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -8,6 +8,8 @@ defmodule Mix.Tasks.Pleroma.User do
alias Ecto.Changeset
alias Pleroma.User
alias Pleroma.UserInviteToken
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
@shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
@@ -96,8 +98,9 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["rm", nickname]) do
start_pleroma()
- with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
- User.perform(:delete, user)
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
+ {:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
+ {:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
shell_info("User #{nickname} deleted.")
else
_ -> shell_error("No local user #{nickname}")
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 308d8cffa..a00bc0624 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -173,7 +173,14 @@ defmodule Pleroma.Application do
defp streamer_child(env) when env in [:test, :benchmark], do: []
defp streamer_child(_) do
- [Pleroma.Web.Streamer.supervisor()]
+ [
+ {Registry,
+ [
+ name: Pleroma.Web.Streamer.registry(),
+ keys: :duplicate,
+ partitions: System.schedulers_online()
+ ]}
+ ]
end
defp chat_child(_env, true) do
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 215265fc9..51bb1bda9 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -128,7 +128,7 @@ defmodule Pleroma.Conversation.Participation do
|> Pleroma.Pagination.fetch_paginated(params)
end
- def restrict_recipients(query, user, %{"recipients" => user_ids}) do
+ def restrict_recipients(query, user, %{recipients: user_ids}) do
user_binary_ids =
[user.id | user_ids]
|> Enum.uniq()
@@ -172,7 +172,7 @@ defmodule Pleroma.Conversation.Participation do
| last_activity_id: activity_id
}
end)
- |> Enum.filter(& &1.last_activity_id)
+ |> Enum.reject(&is_nil(&1.last_activity_id))
end
def get(_, _ \\ [])
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index 7cb49360f..4d61b3650 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -89,11 +89,10 @@ defmodule Pleroma.Filter do
|> Repo.delete()
end
- def update(%Pleroma.Filter{} = filter) do
- destination = Map.from_struct(filter)
-
- Pleroma.Filter.get(filter.filter_id, %{id: filter.user_id})
- |> cast(destination, [:phrase, :context, :hide, :expires_at, :whole_word])
+ def update(%Pleroma.Filter{} = filter, params) do
+ filter
+ |> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word])
+ |> validate_required([:phrase, :context])
|> Repo.update()
end
end
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
new file mode 100644
index 000000000..d353a4dad
--- /dev/null
+++ b/lib/pleroma/mfa.ex
@@ -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
diff --git a/lib/pleroma/mfa/backup_codes.ex b/lib/pleroma/mfa/backup_codes.ex
new file mode 100644
index 000000000..2b5ec34f8
--- /dev/null
+++ b/lib/pleroma/mfa/backup_codes.ex
@@ -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
diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex
new file mode 100644
index 000000000..9b020aa8e
--- /dev/null
+++ b/lib/pleroma/mfa/changeset.ex
@@ -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
diff --git a/lib/pleroma/mfa/settings.ex b/lib/pleroma/mfa/settings.ex
new file mode 100644
index 000000000..2764b889c
--- /dev/null
+++ b/lib/pleroma/mfa/settings.ex
@@ -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
diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex
new file mode 100644
index 000000000..25ff7fb29
--- /dev/null
+++ b/lib/pleroma/mfa/token.ex
@@ -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
diff --git a/lib/pleroma/mfa/totp.ex b/lib/pleroma/mfa/totp.ex
new file mode 100644
index 000000000..1407afc57
--- /dev/null
+++ b/lib/pleroma/mfa/totp.ex
@@ -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
diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex
index 9c8f5597f..3fe550806 100644
--- a/lib/pleroma/plugs/ensure_authenticated_plug.ex
+++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex
@@ -15,26 +15,25 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
end
@impl true
- def perform(%{assigns: %{user: %User{}}} = conn, _) do
+ def perform(
+ %{
+ assigns: %{
+ auth_credentials: %{password: _},
+ user: %User{multi_factor_authentication_settings: %{enabled: true}}
+ }
+ } = conn,
+ _
+ ) do
conn
+ |> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.")
+ |> halt()
end
- def perform(conn, options) do
- perform =
- cond do
- options[:if_func] -> options[:if_func].()
- options[:unless_func] -> !options[:unless_func].()
- true -> true
- end
-
- if perform do
- fail(conn)
- else
- conn
- end
+ def perform(%{assigns: %{user: %User{}}} = conn, _) do
+ conn
end
- def fail(conn) do
+ def perform(conn, _) do
conn
|> render_error(:forbidden, "Invalid credentials.")
|> halt()
diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex
index 7d947339f..09038f3c6 100644
--- a/lib/pleroma/plugs/federating_plug.ex
+++ b/lib/pleroma/plugs/federating_plug.ex
@@ -19,6 +19,9 @@ defmodule Pleroma.Web.FederatingPlug do
def federating?, do: Pleroma.Config.get([:instance, :federating])
+ # Definition for the use in :if_func / :unless_func plug options
+ def federating?(_conn), do: federating?()
+
defp fail(conn) do
conn
|> put_status(404)
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index 8d2809bbb..6b3a8a41f 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -91,7 +91,7 @@ defmodule Pleroma.Stats do
peers: peers,
stats: %{
domain_count: domain_count,
- status_count: status_count,
+ status_count: status_count || 0,
user_count: user_count
}
}
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index aa675a521..921bdd93a 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -20,6 +20,7 @@ defmodule Pleroma.User do
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Keys
+ alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
@@ -115,7 +116,6 @@ defmodule Pleroma.User do
field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil)
- field(:magic_key, :string, default: nil)
field(:uri, Types.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
@@ -191,6 +191,12 @@ defmodule Pleroma.User do
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
+ embeds_one(
+ :multi_factor_authentication_settings,
+ MFA.Settings,
+ on_replace: :delete
+ )
+
timestamps()
end
@@ -389,7 +395,6 @@ defmodule Pleroma.User do
:banner,
:locked,
:last_refreshed_at,
- :magic_key,
:uri,
:follower_address,
:following_address,
@@ -929,6 +934,7 @@ defmodule Pleroma.User do
end
end
+ @spec get_by_nickname(String.t()) :: User.t() | nil
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
@@ -1429,8 +1435,6 @@ defmodule Pleroma.User do
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
- {:ok, _user} = ActivityPub.delete(user)
-
# Remove all relationships
user
|> get_followers()
@@ -1540,18 +1544,20 @@ defmodule Pleroma.User do
})
end
- def delete_user_activities(%User{ap_id: ap_id}) do
+ def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
- |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
+ |> Stream.each(fn activities ->
+ Enum.each(activities, fn activity -> delete_activity(activity, user) end)
+ end)
|> Stream.run()
end
- defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
- activity
- |> Object.normalize()
- |> ActivityPub.delete()
+ defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
+ {:ok, delete_data, _} = Builder.delete(user, object)
+
+ Pipeline.common_pipeline(delete_data, local: true)
end
defp delete_activity(%{data: %{"type" => type}} = activity) when type in ["Like", "Announce"] do
@@ -1560,11 +1566,10 @@ defmodule Pleroma.User do
|> get_cached_by_ap_id()
{:ok, undo, _} = Builder.undo(actor, activity)
-
Pipeline.common_pipeline(undo, local: true)
end
- defp delete_activity(_activity), do: "Doing nothing"
+ defp delete_activity(_activity, _user), do: "Doing nothing"
def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index ac77aab71..3a3b04793 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -45,6 +45,7 @@ defmodule Pleroma.User.Query do
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
+ exclude_service_users: boolean(),
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
@@ -88,6 +89,10 @@ defmodule Pleroma.User.Query do
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
end
+ defp compose_query({:exclude_service_users, _}, query) do
+ where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch"))
+ end
+
defp compose_query({key, value}, query)
when key in @equal_criteria and not_empty_string(value) do
where(query, [u], ^[{key, value}])
@@ -98,7 +103,7 @@ defmodule Pleroma.User.Query do
end
defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
- Enum.reduce(tags, query, &prepare_tag_criteria/2)
+ where(query, [u], fragment("? && ?", u.tags, ^tags))
end
defp compose_query({:is_admin, _}, query) do
@@ -192,10 +197,6 @@ defmodule Pleroma.User.Query do
defp compose_query(_unsupported_param, query), do: query
- defp prepare_tag_criteria(tag, query) do
- or_where(query, [u], fragment("? = any(?)", ^tag, u.tags))
- end
-
defp location_query(query, local) do
where(query, [u], u.local == ^local)
|> where([u], not is_nil(u.nickname))
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 78e8c0cbe..5abeb94c6 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -170,12 +170,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
- Notification.create_notifications(activity)
-
- conversation = create_or_bump_conversation(activity, map["actor"])
- participations = get_participations(conversation)
- stream_out(activity)
- stream_out_participations(participations)
{:ok, activity}
else
%Activity{} = activity ->
@@ -198,6 +192,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ def notify_and_stream(activity) do
+ Notification.create_notifications(activity)
+
+ conversation = create_or_bump_conversation(activity, activity.actor)
+ participations = get_participations(conversation)
+ stream_out(activity)
+ stream_out_participations(participations)
+ end
+
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
%User{} = user <- User.get_cached_by_ap_id(actor),
@@ -274,6 +277,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
_ <- increase_poll_votes_if_vote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -301,6 +305,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
additional
),
{:ok, activity} <- insert(listen_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@@ -325,6 +330,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|> Utils.maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@@ -344,6 +350,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
},
data <- Utils.maybe_put(data, "id", activity_id),
{:ok, activity} <- insert(data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@@ -365,6 +372,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
{:ok, activity} <- insert(reaction_data, local),
{:ok, object} <- add_emoji_reaction_to_object(activity, object),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
@@ -394,6 +402,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
@@ -414,6 +423,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp do_follow(follower, followed, activity_id, local) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -435,6 +445,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -443,67 +454,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- @spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
- def delete(entity, options \\ []) do
- with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
- result
- end
- end
-
- defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
- with data <- %{
- "to" => [follower_address],
- "type" => "Delete",
- "actor" => ap_id,
- "object" => %{"type" => "Person", "id" => ap_id}
- },
- {:ok, activity} <- insert(data, true, true, true),
- :ok <- maybe_federate(activity) do
- {:ok, user}
- end
- end
-
- defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
- local = Keyword.get(options, :local, true)
- activity_id = Keyword.get(options, :activity_id, nil)
- actor = Keyword.get(options, :actor, actor)
-
- user = User.get_cached_by_ap_id(actor)
- to = (object.data["to"] || []) ++ (object.data["cc"] || [])
-
- with create_activity <- Activity.get_create_by_object_ap_id(id),
- data <-
- %{
- "type" => "Delete",
- "actor" => actor,
- "object" => id,
- "to" => to,
- "deleted_activity_id" => create_activity && create_activity.id
- }
- |> maybe_put("id", activity_id),
- {:ok, activity} <- insert(data, local, false),
- {:ok, object, _create_activity} <- Object.delete(object),
- stream_out_participations(object, user),
- _ <- decrease_replies_count_if_reply(object),
- {:ok, _actor} <- decrease_note_count_if_public(user, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- else
- {:error, error} ->
- Repo.rollback(error)
- end
- end
-
- defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
- activity =
- ap_id
- |> Activity.Queries.by_object_id()
- |> Activity.Queries.by_type("Delete")
- |> Repo.one()
-
- {:ok, activity}
- end
-
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
@@ -525,6 +475,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with true <- outgoing_blocks,
block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -558,6 +509,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(stripped_activity) do
User.all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
@@ -581,7 +533,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
}
with true <- origin.ap_id in target.also_known_as,
- {:ok, activity} <- insert(params, local) do
+ {:ok, activity} <- insert(params, local),
+ _ <- notify_and_stream(activity) do
maybe_federate(activity)
BackgroundWorker.enqueue("move_following", %{
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index f607931ab..976ff243e 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -34,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
plug(
EnsureAuthenticatedPlug,
- [unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
+ [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
)
# Note: :following and :followers must be served even without authentication (as via :api)
@@ -415,7 +415,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
true <- user.is_moderator || user.ap_id == object.data["actor"],
- {:ok, delete} <- ActivityPub.delete(object) do
+ {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
+ {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete}
else
_ -> {:error, dgettext("errors", "Can't delete object")}
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 380d8f565..b0f447e28 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -23,6 +23,33 @@ defmodule Pleroma.Web.ActivityPub.Builder do
}, []}
end
+ @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
+ def delete(actor, object_id) do
+ object = Object.normalize(object_id, false)
+
+ user = !object && User.get_cached_by_ap_id(object_id)
+
+ to =
+ case {object, user} do
+ {%Object{}, _} ->
+ # We are deleting an object, address everyone who was originally mentioned
+ (object.data["to"] || []) ++ (object.data["cc"] || [])
+
+ {_, %User{follower_address: follower_address}} ->
+ # We are deleting a user, address the followers of that user
+ [follower_address]
+ end
+
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "object" => object_id,
+ "to" => to,
+ "type" => "Delete"
+ }, []}
+ end
+
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
def like(actor, object) do
object_actor = User.get_cached_by_ap_id(object.data["actor"])
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index b6937d2e1..8e043287d 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -11,8 +11,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta)
@@ -25,6 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def validate(%{"type" => "Delete"} = object, meta) do
+ with cng <- DeleteValidator.cast_and_validate(object),
+ do_not_federate <- DeleteValidator.do_not_federate?(cng),
+ {:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do
+ object = stringify_keys(object)
+ meta = Keyword.put(meta, :do_not_federate, do_not_federate)
+ {:ok, object, meta}
+ end
+ end
+
def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <-
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
@@ -33,13 +45,25 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def stringify_keys(%{__struct__: _} = object) do
+ object
+ |> Map.from_struct()
+ |> stringify_keys
+ end
+
def stringify_keys(object) do
object
|> Map.new(fn {key, val} -> {to_string(key), val} end)
end
+ def fetch_actor(object) do
+ with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
+ User.get_or_fetch_by_ap_id(actor)
+ end
+ end
+
def fetch_actor_and_object(object) do
- User.get_or_fetch_by_ap_id(object["actor"])
+ fetch_actor(object)
Object.normalize(object["object"])
:ok
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
index 067ee4f9a..2ada9f09e 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
@@ -9,7 +9,29 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
alias Pleroma.Object
alias Pleroma.User
- def validate_actor_presence(cng, field_name \\ :actor) do
+ def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
+ non_empty =
+ fields
+ |> Enum.map(fn field -> get_field(cng, field) end)
+ |> Enum.any?(fn
+ [] -> false
+ _ -> true
+ end)
+
+ if non_empty do
+ cng
+ else
+ fields
+ |> Enum.reduce(cng, fn field, cng ->
+ cng
+ |> add_error(field, "no recipients in any field")
+ end)
+ end
+ end
+
+ def validate_actor_presence(cng, options \\ []) do
+ field_name = Keyword.get(options, :field_name, :actor)
+
cng
|> validate_change(field_name, fn field_name, actor ->
if User.get_cached_by_ap_id(actor) do
@@ -20,14 +42,39 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end)
end
- def validate_object_presence(cng, field_name \\ :object) do
+ def validate_object_presence(cng, options \\ []) do
+ field_name = Keyword.get(options, :field_name, :object)
+ allowed_types = Keyword.get(options, :allowed_types, false)
+
cng
- |> validate_change(field_name, fn field_name, object ->
- if Object.get_cached_by_ap_id(object) || Activity.get_by_ap_id(object) do
- []
- else
- [{field_name, "can't find object"}]
+ |> validate_change(field_name, fn field_name, object_id ->
+ object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object)
+
+ cond do
+ !object ->
+ [{field_name, "can't find object"}]
+
+ object && allowed_types && object.data["type"] not in allowed_types ->
+ [{field_name, "object not in allowed types"}]
+
+ true ->
+ []
end
end)
end
+
+ def validate_object_or_user_presence(cng, options \\ []) do
+ field_name = Keyword.get(options, :field_name, :object)
+ options = Keyword.put(options, :field_name, field_name)
+
+ actor_cng =
+ cng
+ |> validate_actor_presence(options)
+
+ object_cng =
+ cng
+ |> validate_object_presence(options)
+
+ if actor_cng.valid?, do: actor_cng, else: object_cng
+ end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
new file mode 100644
index 000000000..e06de3dff
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -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
diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
index 1bce739bd..034f25492 100644
--- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
@@ -20,8 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:context, :string)
- field(:to, {:array, :string}, default: [])
- field(:cc, {:array, :string}, default: [])
+ field(:to, Types.Recipients, default: [])
+ field(:cc, Types.Recipients, default: [])
end
def cast_and_validate(data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
new file mode 100644
index 000000000..48fe61e1a
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
@@ -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
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
index d5abb7567..657cdfdb1 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -44,7 +44,9 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do
- if local do
+ do_not_federate = meta[:do_not_federate]
+
+ if !do_not_federate && local do
Federator.publish(activity)
{:ok, :federated}
else
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 3fad6e4d8..5049cb54e 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
def handle(object, meta \\ [])
@@ -33,6 +34,49 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
end
+ # Tasks this handles:
+ # - Delete and unpins the create activity
+ # - Replace object with Tombstone
+ # - Set up notification
+ # - Reduce the user note count
+ # - Reduce the reply count
+ # - Stream out the activity
+ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
+ deleted_object =
+ Object.normalize(deleted_object, false) || User.get_cached_by_ap_id(deleted_object)
+
+ result =
+ case deleted_object do
+ %Object{} ->
+ with {:ok, deleted_object, activity} <- Object.delete(deleted_object),
+ %User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do
+ User.remove_pinnned_activity(user, activity)
+
+ {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
+
+ if in_reply_to = deleted_object.data["inReplyTo"] do
+ Object.decrease_replies_count(in_reply_to)
+ end
+
+ ActivityPub.stream_out(object)
+ ActivityPub.stream_out_participations(deleted_object, user)
+ :ok
+ end
+
+ %User{} ->
+ with {:ok, _} <- User.delete(deleted_object) do
+ :ok
+ end
+ end
+
+ if result == :ok do
+ Notification.create_notifications(object)
+ {:ok, object, meta}
+ else
+ {:error, result}
+ end
+ end
+
# Nothing to do
def handle(object, meta) do
{:ok, object, meta}
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 65ae643ed..26b5bda0e 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -735,36 +735,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- # TODO: We presently assume that any actor on the same origin domain as the object being
- # deleted has the rights to delete that object. A better way to validate whether or not
- # the object should be deleted is to refetch the object URI, which should return either
- # an error or a tombstone. This would allow us to verify that a deletion actually took
- # place.
def handle_incoming(
- %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
+ %{"type" => "Delete"} = data,
_options
) do
- object_id = Utils.get_ap_id(object_id)
-
- with actor <- Containment.get_actor(data),
- {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <- get_obj_helper(object_id),
- :ok <- Containment.contain_origin(actor.ap_id, object.data),
- {:ok, activity} <-
- ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
+ with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
- else
- nil ->
- case User.get_cached_by_ap_id(object_id) do
- %User{ap_id: ^actor} = user ->
- User.delete(user)
-
- nil ->
- :error
- end
-
- _e ->
- :error
end
end
@@ -1129,6 +1105,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "conversation", object["context"])
end
+ def set_sensitive(%{"sensitive" => true} = object) do
+ object
+ end
+
def set_sensitive(object) do
tags = object["tag"] || []
Map.put(object, "sensitive", "nsfw" in tags)
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 95fb382f0..09b80fa57 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -512,7 +512,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Announce-related helpers
@doc """
- Retruns an existing announce activity if the notice has already been announced
+ Returns an existing announce activity if the notice has already been announced
"""
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 816c11e01..9f1fd3aeb 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.ConfigDB
+ alias Pleroma.MFA
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
@@ -17,6 +18,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
@@ -59,6 +62,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:right_add,
:right_add_multiple,
:right_delete,
+ :disable_mfa,
:right_delete_multiple,
:update_user_credentials
]
@@ -93,7 +97,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
- when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
+ when action in [:list_statuses, :list_user_statuses, :list_instance_statuses, :status_show]
)
plug(
@@ -133,23 +137,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(:errors)
- def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
- user = User.get_cached_by_nickname(nickname)
- User.delete(user)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: [user],
- action: "delete"
- })
-
- conn
- |> json(nickname)
+ def user_delete(conn, %{"nickname" => nickname}) do
+ user_delete(conn, %{"nicknames" => [nickname]})
end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
- User.delete(users)
+ users =
+ nicknames
+ |> Enum.map(&User.get_cached_by_nickname/1)
+
+ users
+ |> Enum.each(fn user ->
+ {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
+ Pipeline.common_pipeline(delete_data, local: true)
+ end)
ModerationLog.insert_log(%{
actor: admin,
@@ -392,29 +393,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
email: params["email"]
}
- with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
- {:ok, users, count} <- filter_service_users(users, count),
- do:
- conn
- |> json(
- AccountView.render("index.json",
- users: users,
- count: count,
- page_size: page_size
- )
- )
- end
-
- defp filter_service_users(users, count) do
- filtered_users = Enum.reject(users, &service_user?/1)
- count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
-
- {:ok, filtered_users, count}
- end
-
- defp service_user?(user) do
- String.match?(user.ap_id, ~r/.*\/relay$/) or
- String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
+ with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
+ json(
+ conn,
+ AccountView.render("index.json", users: users, count: count, page_size: page_size)
+ )
+ end
end
@filters ~w(local external active deactivated is_admin is_moderator)
@@ -692,6 +676,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
json_response(conn, :no_content, "")
end
+ @doc "Disable mfa for user's account."
+ def disable_mfa(conn, %{"nickname" => nickname}) do
+ case User.get_by_nickname(nickname) do
+ %User{} = user ->
+ MFA.disable(user)
+ json(conn, nickname)
+
+ _ ->
+ {:error, :not_found}
+ end
+ end
+
@doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
@@ -837,6 +833,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end
+ def status_show(conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id) do
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", %{activity: activity})
+ else
+ _ -> errors(conn, {:error, :not_found})
+ end
+ end
+
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex
index 29cea1f44..c28efadd5 100644
--- a/lib/pleroma/web/admin_api/search.ex
+++ b/lib/pleroma/web/admin_api/search.ex
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.AdminAPI.Search do
query =
params
|> Map.drop([:page, :page_size])
+ |> Map.put(:exclude_service_users, true)
|> User.Query.build()
|> order_by([u], u.nickname)
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index b3c1e3ea2..79fd5f871 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -39,7 +39,12 @@ defmodule Pleroma.Web.ApiSpec do
password: %OpenApiSpex.OAuthFlow{
authorizationUrl: "/oauth/authorize",
tokenUrl: "/oauth/token",
- scopes: %{"read" => "read", "write" => "write", "follow" => "follow"}
+ scopes: %{
+ "read" => "read",
+ "write" => "write",
+ "follow" => "follow",
+ "push" => "push"
+ }
}
}
}
diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
new file mode 100644
index 000000000..bd9026237
--- /dev/null
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index fe9548b1b..470fc0215 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
alias Pleroma.Web.ApiSpec.Schemas.ActorType
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.List
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
@@ -646,28 +647,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
- defp list do
- %Schema{
- title: "List",
- description: "Response schema for a list",
- type: :object,
- properties: %{
- id: %Schema{type: :string},
- title: %Schema{type: :string}
- },
- example: %{
- "id" => "123",
- "title" => "my list"
- }
- }
- end
-
defp array_of_lists do
%Schema{
title: "ArrayOfLists",
description: "Response schema for lists",
type: :array,
- items: list(),
+ items: List,
example: [
%{"id" => "123", "title" => "my list"},
%{"id" => "1337", "title" => "anotehr list"}
diff --git a/lib/pleroma/web/api_spec/operations/conversation_operation.ex b/lib/pleroma/web/api_spec/operations/conversation_operation.ex
new file mode 100644
index 000000000..475468893
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/conversation_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex
new file mode 100644
index 000000000..53e57b46b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
new file mode 100644
index 000000000..ac4aee6da
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex
new file mode 100644
index 000000000..880bd3f1b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex
new file mode 100644
index 000000000..c88ed5dd0
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/list_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/marker_operation.ex b/lib/pleroma/web/api_spec/operations/marker_operation.ex
new file mode 100644
index 000000000..06620492a
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/poll_operation.ex b/lib/pleroma/web/api_spec/operations/poll_operation.ex
new file mode 100644
index 000000000..e15c7dc95
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/poll_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
new file mode 100644
index 000000000..fe675a923
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
new file mode 100644
index 000000000..663b8fa11
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex
index b5877ca9c..d476b8ef3 100644
--- a/lib/pleroma/web/api_spec/render_error.ex
+++ b/lib/pleroma/web/api_spec/render_error.ex
@@ -17,6 +17,9 @@ defmodule Pleroma.Web.ApiSpec.RenderError do
def call(conn, errors) do
errors =
Enum.map(errors, fn
+ %{name: nil, reason: :invalid_enum} = err ->
+ %OpenApiSpex.Cast.Error{err | name: err.value}
+
%{name: nil} = err ->
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex
new file mode 100644
index 000000000..c146c416e
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/attachment.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/schemas/conversation.ex b/lib/pleroma/web/api_spec/schemas/conversation.ex
new file mode 100644
index 000000000..d8ff5ba26
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/conversation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex
new file mode 100644
index 000000000..b7d1685c9
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/list.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
index 0474b550b..c62096db0 100644
--- a/lib/pleroma/web/api_spec/schemas/poll.ex
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -11,26 +11,72 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
OpenApiSpex.schema(%{
title: "Poll",
- description: "Response schema for account custom fields",
+ description: "Represents a poll attached to a status",
type: :object,
properties: %{
id: FlakeID,
- expires_at: %Schema{type: :string, format: "date-time"},
- expired: %Schema{type: :boolean},
- multiple: %Schema{type: :boolean},
- votes_count: %Schema{type: :integer},
- voted: %Schema{type: :boolean},
- emojis: %Schema{type: :array, items: Emoji},
+ expires_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ nullable: true,
+ description: "When the poll ends"
+ },
+ expired: %Schema{type: :boolean, description: "Is the poll currently expired?"},
+ multiple: %Schema{
+ type: :boolean,
+ description: "Does the poll allow multiple-choice answers?"
+ },
+ votes_count: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "How many votes have been received. Number, or null if `multiple` is false."
+ },
+ voted: %Schema{
+ type: :boolean,
+ nullable: true,
+ description:
+ "When called with a user token, has the authorized user voted? Boolean, or null if no current user."
+ },
+ emojis: %Schema{
+ type: :array,
+ items: Emoji,
+ description: "Custom emoji to be used for rendering poll options."
+ },
options: %Schema{
type: :array,
items: %Schema{
+ title: "PollOption",
type: :object,
properties: %{
title: %Schema{type: :string},
votes_count: %Schema{type: :integer}
}
- }
+ },
+ description: "Possible answers for the poll."
}
+ },
+ example: %{
+ id: "34830",
+ expires_at: "2019-12-05T04:05:08.302Z",
+ expired: true,
+ multiple: false,
+ votes_count: 10,
+ voters_count: nil,
+ voted: true,
+ own_votes: [
+ 1
+ ],
+ options: [
+ %{
+ title: "accept",
+ votes_count: 6
+ },
+ %{
+ title: "deny",
+ votes_count: 4
+ }
+ ],
+ emojis: []
}
})
end
diff --git a/lib/pleroma/web/api_spec/schemas/push_subscription.ex b/lib/pleroma/web/api_spec/schemas/push_subscription.ex
new file mode 100644
index 000000000..cc91b95b8
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/push_subscription.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
new file mode 100644
index 000000000..0520d0848
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index aef0588d4..7a804461f 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.ApiSpec.Schemas.Status do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll
@@ -50,22 +51,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
language: %Schema{type: :string, nullable: true},
media_attachments: %Schema{
type: :array,
- items: %Schema{
- type: :object,
- properties: %{
- id: %Schema{type: :string},
- url: %Schema{type: :string, format: :uri},
- remote_url: %Schema{type: :string, format: :uri},
- preview_url: %Schema{type: :string, format: :uri},
- text_url: %Schema{type: :string, format: :uri},
- description: %Schema{type: :string},
- type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
- pleroma: %Schema{
- type: :object,
- properties: %{mime_type: %Schema{type: :string}}
- }
- }
- }
+ items: Attachment
},
mentions: %Schema{
type: :array,
@@ -86,7 +72,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
properties: %{
content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
conversation_id: %Schema{type: :integer},
- direct_conversation_id: %Schema{type: :string, nullable: true},
+ direct_conversation_id: %Schema{
+ type: :integer,
+ nullable: true,
+ description:
+ "The ID of the Mastodon direct message conversation the status is associated with (if any)"
+ },
emoji_reactions: %Schema{
type: :array,
items: %Schema{
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index cb09664ce..a8f554aa3 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -19,8 +19,8 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
{_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
{:ok, user}
else
- error ->
- {:error, error}
+ {:error, _reason} = error -> error
+ error -> {:error, error}
end
end
diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex
new file mode 100644
index 000000000..98aca9a51
--- /dev/null
+++ b/lib/pleroma/web/auth/totp_authenticator.ex
@@ -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
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 2a1eb7f37..1e6bbbab7 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -87,8 +87,8 @@ defmodule Pleroma.Web.CommonAPI do
{:find_activity, Activity.get_by_id_with_object(activity_id)},
%Object{} = object <- Object.normalize(activity),
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
- {:ok, _} <- unpin(activity_id, user),
- {:ok, delete} <- ActivityPub.delete(object) do
+ {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
+ {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete}
else
{:find_activity, _} -> {:error, :not_found}
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 6540fa5d1..793f2e7f8 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -402,6 +402,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
+ @spec confirm_current_password(User.t(), String.t()) :: {:ok, User.t()} | {:error, String.t()}
def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index e27f85929..1b72e23dc 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.Feed.UserController do
when format in ["json", "activity+json"] do
with %{halted: false} = conn <-
Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn,
- unless_func: &Pleroma.Web.FederatingPlug.federating?/0
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
) do
ActivityPubController.call(conn, :user)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 2b208ddab..b9ed2d7b2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 408e11474..a516b6c20 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
@local_mastodon_name "Mastodon-Local"
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index c44641526..f35ec3596 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -13,9 +13,12 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ConversationOperation
+
@doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do
participations = Participation.for_user_with_last_activity_id(user, params)
@@ -26,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
end
@doc "POST /api/v1/conversations/:id/read"
- def mark_as_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
+ def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
with %Participation{} = participation <-
Repo.get_by(Participation, id: participation_id, user_id: user.id),
{:ok, participation} <- Participation.mark_as_read(participation) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
index 000ad743f..c5f47c5df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
:skip_plug,
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
index c4fa383f2..825b231ab 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug(
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index 7fd0562c9..abbf0ce02 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)
plug(
@@ -17,60 +18,60 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation
+
@doc "GET /api/v1/filters"
def index(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
- render(conn, "filters.json", filters: filters)
+ render(conn, "index.json", filters: filters)
end
@doc "POST /api/v1/filters"
- def create(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context} = params
- ) do
+ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
query = %Filter{
user_id: user.id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", false),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
+ phrase: params.phrase,
+ context: params.context,
+ hide: params.irreversible,
+ whole_word: params.whole_word
+ # TODO: support `expires_in` parameter (as in Mastodon API)
}
{:ok, response} = Filter.create(query)
- render(conn, "filter.json", filter: response)
+ render(conn, "show.json", filter: response)
end
@doc "GET /api/v1/filters/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
filter = Filter.get(filter_id, user)
- render(conn, "filter.json", filter: filter)
+ render(conn, "show.json", filter: filter)
end
@doc "PUT /api/v1/filters/:id"
def update(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
+ %{assigns: %{user: user}, body_params: params} = conn,
+ %{id: filter_id}
) do
- query = %Filter{
- user_id: user.id,
- filter_id: filter_id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", nil),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
- }
-
- {:ok, response} = Filter.update(query)
- render(conn, "filter.json", filter: response)
+ params =
+ params
+ |> Map.delete(:irreversible)
+ |> Map.put(:hide, params[:irreversible])
+ |> Enum.reject(fn {_key, value} -> is_nil(value) end)
+ |> Map.new()
+
+ # TODO: support `expires_in` parameter (as in Mastodon API)
+
+ with %Filter{} = filter <- Filter.get(filter_id, user),
+ {:ok, %Filter{} = filter} <- Filter.update(filter, params) do
+ render(conn, "show.json", filter: filter)
+ end
end
@doc "DELETE /api/v1/filters/:id"
- def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ def delete(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
query = %Filter{
user_id: user.id,
filter_id: filter_id
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
index 25f2269b9..748b6b475 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
alias Pleroma.Web.CommonAPI
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:assign_follower when action != :index)
action_fallback(:errors)
@@ -21,6 +22,8 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
%{scopes: ["follow", "write:follows"]} when action != :index
)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FollowRequestOperation
+
@doc "GET /api/v1/follow_requests"
def index(%{assigns: %{user: followed}} = conn, _params) do
follow_requests = User.get_follow_requests(followed)
@@ -42,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
end
end
- defp assign_follower(%{params: %{"id" => id}} = conn, _) do
+ defp assign_follower(%{params: %{id: id}} = conn, _) do
case User.get_cached_by_id(id) do
%User{} = follower -> assign(conn, :follower, follower)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index 237f85677..d8859731d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -5,12 +5,16 @@
defmodule Pleroma.Web.MastodonAPI.InstanceController do
use Pleroma.Web, :controller
+ plug(OpenApiSpex.Plug.CastAndValidate)
+
plug(
:skip_plug,
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
when action in [:show, :peers]
)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation
+
@doc "GET /api/v1/instance"
def show(conn, _params) do
render(conn, "show.json")
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index bfe856025..acdc76fd2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -9,20 +9,17 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
- plug(:list_by_id_and_user when action not in [:index, :create])
-
@oauth_read_actions [:index, :show, :list_accounts]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:list_by_id_and_user when action not in [:index, :create])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:lists"]}
- when action not in @oauth_read_actions
- )
+ plug(OAuthScopesPlug, %{scopes: ["write:lists"]} when action not in @oauth_read_actions)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ListOperation
+
# GET /api/v1/lists
def index(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
@@ -30,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# POST /api/v1/lists
- def create(%{assigns: %{user: user}} = conn, %{"title" => title}) do
+ def create(%{assigns: %{user: user}, body_params: %{title: title}} = conn, _) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
render(conn, "show.json", list: list)
end
@@ -42,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# PUT /api/v1/lists/:id
- def update(%{assigns: %{list: list}} = conn, %{"title" => title}) do
+ def update(%{assigns: %{list: list}, body_params: %{title: title}} = conn, _) do
with {:ok, list} <- Pleroma.List.rename(list, title) do
render(conn, "show.json", list: list)
end
@@ -65,7 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# POST /api/v1/lists/:id/accounts
- def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
+ def add_to_list(%{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, _) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
@@ -76,7 +73,10 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# DELETE /api/v1/lists/:id/accounts
- def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
+ def remove_from_list(
+ %{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn,
+ _
+ ) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
@@ -86,7 +86,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
json(conn, %{})
end
- defp list_by_id_and_user(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
case Pleroma.List.get(id, user) do
%Pleroma.List{} = list -> assign(conn, :list, list)
nil -> conn |> render_error(:not_found, "List not found") |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
index 9f9d4574e..85310edfa 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"]}
@@ -16,14 +18,18 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MarkerOperation
+
# GET /api/v1/markers
def index(%{assigns: %{user: user}} = conn, params) do
- markers = Pleroma.Marker.get_markers(user, params["timeline"])
+ markers = Pleroma.Marker.get_markers(user, params[:timeline])
render(conn, "markers.json", %{markers: markers})
end
# POST /api/v1/markers
- def upsert(%{assigns: %{user: user}} = conn, params) do
+ def upsert(%{assigns: %{user: user}, body_params: params} = conn, _) do
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
with {:ok, result} <- Pleroma.Marker.upsert(user, params),
markers <- Map.values(result) do
render(conn, "markers.json", %{markers: markers})
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index a14c86893..596b85617 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index]
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index af9b66eff..db46ffcfc 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -15,6 +15,8 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :show
@@ -22,8 +24,10 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation
+
@doc "GET /api/v1/polls/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
@@ -35,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
end
@doc "POST /api/v1/polls/:id/votes"
- def vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
+ def vote(%{assigns: %{user: user}, body_params: %{choices: choices}} = conn, %{id: id}) do
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index f65c5c62b..405167108 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index 899b78873..1719c67ea 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -11,17 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
alias Pleroma.ScheduledActivity
alias Pleroma.Web.MastodonAPI.MastodonAPI
- plug(:assign_scheduled_activity when action != :index)
-
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
+ plug(:assign_scheduled_activity when action != :index)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
+
@doc "GET /api/v1/scheduled_statuses"
def index(%{assigns: %{user: user}} = conn, params) do
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
|> add_link_headers(scheduled_activities)
@@ -35,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
@doc "PUT /api/v1/scheduled_statuses/:id"
- def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
+ def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
@@ -48,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
end
- defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index d184ea1d0..34eac97c5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -11,14 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
action_fallback(:errors)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:restrict_push_enabled)
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
- plug(:restrict_push_enabled)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation
# Creates PushSubscription
# POST /api/v1/push/subscription
#
- def create(%{assigns: %{user: user, token: token}} = conn, params) do
+ def create(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
with {:ok, _} <- Subscription.delete_if_exists(user, token),
{:ok, subscription} <- Subscription.create(user, token, params) do
render(conn, "show.json", subscription: subscription)
@@ -28,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# Gets PushSubscription
# GET /api/v1/push/subscription
#
- def get(%{assigns: %{user: user, token: token}} = conn, _params) do
+ def show(%{assigns: %{user: user, token: token}} = conn, _params) do
with {:ok, subscription} <- Subscription.get(user, token) do
render(conn, "show.json", subscription: subscription)
end
@@ -37,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# Updates PushSubscription
# PUT /api/v1/push/subscription
#
- def update(%{assigns: %{user: user, token: token}} = conn, params) do
+ def update(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
with {:ok, subscription} <- Subscription.update(user, token, params) do
render(conn, "show.json", subscription: subscription)
end
@@ -66,7 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
- |> json(dgettext("errors", "Not found"))
+ |> json(%{error: dgettext("errors", "Record not found")})
end
def errors(conn, _) do
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index 97fd1e83f..aeff646f5 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -7,11 +7,11 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.FilterView
- def render("filters.json", %{filters: filters} = opts) do
- render_many(filters, FilterView, "filter.json", opts)
+ def render("index.json", %{filters: filters}) do
+ render_many(filters, FilterView, "show.json")
end
- def render("filter.json", %{filter: filter}) do
+ def render("show.json", %{filter: filter}) do
expires_at =
if filter.expires_at do
Utils.to_masto_date(filter.expires_at)
diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex
index 985368fe5..9705b7a91 100644
--- a/lib/pleroma/web/mastodon_api/views/marker_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex
@@ -6,12 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do
use Pleroma.Web, :view
def render("markers.json", %{markers: markers}) do
- Enum.reduce(markers, %{}, fn m, acc ->
- Map.put_new(acc, m.timeline, %{
- last_read_id: m.last_read_id,
- version: m.lock_version,
- updated_at: NaiveDateTime.to_iso8601(m.updated_at)
- })
+ Map.new(markers, fn m ->
+ {m.timeline,
+ %{
+ last_read_id: m.last_read_id,
+ version: m.lock_version,
+ updated_at: NaiveDateTime.to_iso8601(m.updated_at)
+ }}
end)
end
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 5652a37c1..6ef3fe2dd 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -12,6 +12,11 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
@behaviour :cowboy_websocket
+ # Cowboy timeout period.
+ @timeout :timer.seconds(30)
+ # Hibernate every X messages
+ @hibernate_every 100
+
@streams [
"public",
"public:local",
@@ -25,9 +30,6 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
]
@anonymous_streams ["public", "public:local", "hashtag"]
- # Handled by periodic keepalive in Pleroma.Web.Streamer.Ping.
- @timeout :infinity
-
def init(%{qs: qs} = req, state) do
with params <- :cow_qs.parse_qs(qs),
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
@@ -42,7 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
req
end
- {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
+ {:cowboy_websocket, req, %{user: user, topic: topic, count: 0}, %{idle_timeout: @timeout}}
else
{:error, code} ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
@@ -57,7 +59,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
def websocket_init(state) do
- send(self(), :subscribe)
+ Logger.debug(
+ "#{__MODULE__} accepted websocket connection for user #{
+ (state.user || %{id: "anonymous"}).id
+ }, topic #{state.topic}"
+ )
+
+ Streamer.add_socket(state.topic, state.user)
{:ok, state}
end
@@ -66,19 +74,24 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
{:ok, state}
end
- def websocket_info(:subscribe, state) do
- Logger.debug(
- "#{__MODULE__} accepted websocket connection for user #{
- (state.user || %{id: "anonymous"}).id
- }, topic #{state.topic}"
- )
+ def websocket_info({:render_with_user, view, template, item}, state) do
+ user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
- Streamer.add_socket(state.topic, streamer_socket(state))
- {:ok, state}
+ unless Streamer.filtered_by_user?(user, item) do
+ websocket_info({:text, view.render(template, user, item)}, %{state | user: user})
+ else
+ {:ok, state}
+ end
end
def websocket_info({:text, message}, state) do
- {:reply, {:text, message}, state}
+ # If the websocket processed X messages, force an hibernate/GC.
+ # We don't hibernate at every message to balance CPU usage/latency with RAM usage.
+ if state.count > @hibernate_every do
+ {:reply, {:text, message}, %{state | count: 0}, :hibernate}
+ else
+ {:reply, {:text, message}, %{state | count: state.count + 1}}
+ end
end
def terminate(reason, _req, state) do
@@ -88,7 +101,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
}, topic #{state.topic || "?"}: #{inspect(reason)}"
)
- Streamer.remove_socket(state.topic, streamer_socket(state))
+ Streamer.remove_socket(state.topic)
:ok
end
@@ -136,8 +149,4 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
defp expand_topic(topic, _), do: topic
-
- defp streamer_socket(state) do
- %{transport_pid: self(), assigns: state}
- end
end
diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex
new file mode 100644
index 000000000..e52cccd85
--- /dev/null
+++ b/lib/pleroma/web/oauth/mfa_controller.ex
@@ -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
diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex
new file mode 100644
index 000000000..e88e7066b
--- /dev/null
+++ b/lib/pleroma/web/oauth/mfa_view.ex
@@ -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
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 685269877..7c804233c 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper
+ alias Pleroma.MFA
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Registration
alias Pleroma.Repo
@@ -14,6 +15,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.MFAController
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
@@ -121,7 +123,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
%{"authorization" => _} = params,
opts \\ []
) do
- with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
+ with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
+ {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
after_create_authorization(conn, auth, params)
else
error ->
@@ -181,6 +184,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
+ {:mfa_required, user, auth, _},
+ params
+ ) do
+ {:ok, token} = MFA.Token.create_token(user, auth)
+
+ data = %{
+ "mfa_token" => token.token,
+ "redirect_uri" => params["authorization"]["redirect_uri"],
+ "state" => params["authorization"]["state"]
+ }
+
+ MFAController.show(conn, data)
+ end
+
+ defp handle_create_authorization_error(
+ %Plug.Conn{} = conn,
{:account_status, :password_reset_pending},
%{"authorization" => _} = params
) do
@@ -231,7 +250,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
json(conn, Token.Response.build(user, token, response_attrs))
else
- _error -> render_invalid_credentials_error(conn)
+ error ->
+ handle_token_exchange_error(conn, error)
end
end
@@ -244,6 +264,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:account_status, :active} <- {:account_status, User.account_status(user)},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
+ {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build(user, token))
else
@@ -270,13 +291,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build_for_client_credentials(token))
else
- _error -> render_invalid_credentials_error(conn)
+ _error ->
+ handle_token_exchange_error(conn, :invalid_credentails)
end
end
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
+ defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
+ conn
+ |> put_status(:forbidden)
+ |> json(build_and_response_mfa_token(user, auth))
+ end
+
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
render_error(
conn,
@@ -434,7 +462,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
- {_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)},
+ {_, {:ok, auth, _user}} <-
+ {:create_authorization, do_create_authorization(conn, params)},
%User{} = user <- Repo.preload(auth, :user).user,
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
conn
@@ -500,8 +529,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
{:ok, scopes} <- validate_scopes(app, auth_attrs),
- {:account_status, :active} <- {:account_status, User.account_status(user)} do
- Authorization.create_authorization(app, user, scopes)
+ {:account_status, :active} <- {:account_status, User.account_status(user)},
+ {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
+ {:ok, auth, user}
end
end
@@ -515,6 +545,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
+ defp build_and_response_mfa_token(user, auth) do
+ with {:ok, token} <- MFA.Token.create_token(user, auth) do
+ Token.Response.build_for_mfa_token(user, token)
+ end
+ end
+
@spec validate_scopes(App.t(), map()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(%App{} = app, params) do
diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
new file mode 100644
index 000000000..2c3bb9ded
--- /dev/null
+++ b/lib/pleroma/web/oauth/token/clean_worker.ex
@@ -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
diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex
index 6f4713dee..0e72c31e9 100644
--- a/lib/pleroma/web/oauth/token/response.ex
+++ b/lib/pleroma/web/oauth/token/response.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.Token.Response do
@moduledoc false
+ alias Pleroma.MFA
alias Pleroma.User
alias Pleroma.Web.OAuth.Token.Utils
@@ -32,5 +33,13 @@ defmodule Pleroma.Web.OAuth.Token.Response do
}
end
+ def build_for_mfa_token(user, mfa_token) do
+ %{
+ error: "mfa_required",
+ mfa_token: mfa_token.token,
+ supported_challenge_types: MFA.supported_methods(user)
+ }
+ end
+
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 6fd3cfce5..6971cd9f8 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Router
plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
- unless_func: &Pleroma.Web.FederatingPlug.federating?/0
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
)
plug(
diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
new file mode 100644
index 000000000..eb9989cdf
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
@@ -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
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index b99b0c5fb..3e401a490 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -25,9 +25,9 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps()
end
- @supported_alert_types ~w[follow favourite mention reblog]
+ @supported_alert_types ~w[follow favourite mention reblog]a
- defp alerts(%{"data" => %{"alerts" => alerts}}) do
+ defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types)
%{"alerts" => alerts}
end
@@ -44,9 +44,9 @@ defmodule Pleroma.Web.Push.Subscription do
%User{} = user,
%Token{} = token,
%{
- "subscription" => %{
- "endpoint" => endpoint,
- "keys" => %{"auth" => key_auth, "p256dh" => key_p256dh}
+ subscription: %{
+ endpoint: endpoint,
+ keys: %{auth: key_auth, p256dh: key_p256dh}
}
} = params
) do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 5b00243e9..7a171f9fb 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -132,6 +132,7 @@ defmodule Pleroma.Web.Router do
post("/users/follow", AdminAPIController, :user_follow)
post("/users/unfollow", AdminAPIController, :user_unfollow)
+ put("/users/disable_mfa", AdminAPIController, :disable_mfa)
delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
@@ -188,6 +189,7 @@ defmodule Pleroma.Web.Router do
post("/reports/:id/notes", AdminAPIController, :report_notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
+ get("/statuses/:id", AdminAPIController, :status_show)
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
get("/statuses", AdminAPIController, :list_statuses)
@@ -257,6 +259,16 @@ defmodule Pleroma.Web.Router do
post("/follow_import", UtilController, :follow_import)
end
+ scope "/api/pleroma", Pleroma.Web.PleromaAPI do
+ pipe_through(:authenticated_api)
+
+ get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
+ get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
+ get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
+ post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm)
+ delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable)
+ end
+
scope "/oauth", Pleroma.Web.OAuth do
scope [] do
pipe_through(:oauth)
@@ -268,6 +280,10 @@ defmodule Pleroma.Web.Router do
post("/revoke", OAuthController, :token_revoke)
get("/registration_details", OAuthController, :registration_details)
+ post("/mfa/challenge", MFAController, :challenge)
+ post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
+ get("/mfa", MFAController, :show)
+
scope [] do
pipe_through(:browser)
@@ -426,7 +442,7 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/push/subscription", SubscriptionController, :create)
- get("/push/subscription", SubscriptionController, :get)
+ get("/push/subscription", SubscriptionController, :show)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index 7a35238d7..c3efb6651 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
plug(:assign_id)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
- unless_func: &Pleroma.Web.FederatingPlug.federating?/0
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
)
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
diff --git a/lib/pleroma/web/streamer/ping.ex b/lib/pleroma/web/streamer/ping.ex
deleted file mode 100644
index 7a08202a9..000000000
--- a/lib/pleroma/web/streamer/ping.ex
+++ /dev/null
@@ -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
diff --git a/lib/pleroma/web/streamer/state.ex b/lib/pleroma/web/streamer/state.ex
deleted file mode 100644
index 999550b88..000000000
--- a/lib/pleroma/web/streamer/state.ex
+++ /dev/null
@@ -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
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex
index 814d5a729..5ad4aa936 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -3,53 +3,241 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer do
- alias Pleroma.Web.Streamer.State
- alias Pleroma.Web.Streamer.Worker
+ 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.StreamerView
- @timeout 60_000
@mix_env Mix.env()
+ @registry Pleroma.Web.StreamerRegistry
+
+ def registry, do: @registry
- def add_socket(topic, socket) do
- State.add_socket(topic, socket)
+ def add_socket(topic, %User{} = user) do
+ if should_env_send?(), do: Registry.register(@registry, user_topic(topic, user), true)
end
- def remove_socket(topic, socket) do
- State.remove_socket(topic, socket)
+ def add_socket(topic, _) do
+ if should_env_send?(), do: Registry.register(@registry, topic, false)
end
- def get_sockets do
- State.get_sockets()
+ def remove_socket(topic) do
+ if should_env_send?(), do: Registry.unregister(@registry, topic)
end
- def stream(topics, items) do
- if should_send?() do
- Task.async(fn ->
- :poolboy.transaction(
- :streamer_worker,
- &Worker.stream(&1, topics, items),
- @timeout
- )
+ def stream(topics, item) when is_list(topics) do
+ if should_env_send?() do
+ Enum.each(topics, fn t ->
+ spawn(fn -> do_stream(t, item) end)
end)
end
+
+ :ok
end
- def supervisor, do: Pleroma.Web.Streamer.Supervisor
+ def stream(topic, items) when is_list(items) do
+ if should_env_send?() do
+ Enum.each(items, fn i ->
+ spawn(fn -> do_stream(topic, i) end)
+ end)
- defp should_send? do
- handle_should_send(@mix_env)
+ :ok
+ end
end
- defp handle_should_send(:test) do
- case Process.whereis(:streamer_worker) do
- nil ->
- false
+ def stream(topic, item) do
+ if should_env_send?() do
+ spawn(fn -> do_stream(topic, item) end)
+ end
+
+ :ok
+ end
- pid ->
- Process.alive?(pid)
+ def filtered_by_user?(%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
+ false
+ else
+ _ -> true
end
end
- defp handle_should_send(:benchmark), do: false
+ def filtered_by_user?(%User{} = user, %Notification{activity: activity}) do
+ filtered_by_user?(user, activity)
+ end
+
+ defp do_stream("direct", 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(user_topic, item)
+ end)
+ end
+
+ defp do_stream("participation", participation) do
+ user_topic = "direct:#{participation.user_id}"
+ Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
- defp handle_should_send(_), do: true
+ push_to_socket(user_topic, participation)
+ end
+
+ defp do_stream("list", 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(list_topic, item)
+ end)
+ end
+
+ defp do_stream(topic, %Notification{} = item)
+ when topic in ["user", "user:notification"] do
+ Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list ->
+ Enum.each(list, fn {pid, _auth} ->
+ send(pid, {:render_with_user, StreamerView, "notification.json", item})
+ end)
+ end)
+ end
+
+ defp do_stream("user", 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(topic, item)
+ end)
+ end
+
+ defp do_stream(topic, item) do
+ Logger.debug("Trying to push to #{topic}")
+ Logger.debug("Pushing item to #{topic}")
+ push_to_socket(topic, item)
+ end
+
+ defp push_to_socket(topic, %Participation{} = participation) do
+ rendered = StreamerView.render("conversation.json", participation)
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, _} ->
+ send(pid, {:text, rendered})
+ end)
+ end)
+ end
+
+ defp push_to_socket(topic, %Activity{
+ data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}
+ }) do
+ rendered = Jason.encode!(%{event: "delete", payload: to_string(deleted_activity_id)})
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, _} ->
+ send(pid, {:text, rendered})
+ end)
+ end)
+ end
+
+ defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
+
+ defp push_to_socket(topic, item) do
+ anon_render = StreamerView.render("update.json", item)
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, auth?} ->
+ if auth? do
+ send(pid, {:render_with_user, StreamerView, "update.json", item})
+ else
+ send(pid, {:text, anon_render})
+ end
+ end)
+ end)
+ end
+
+ 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
+
+ # In test environement, only return true if the registry is started.
+ # In benchmark environment, returns false.
+ # In any other environment, always returns true.
+ cond do
+ @mix_env == :test ->
+ def should_env_send? do
+ case Process.whereis(@registry) do
+ nil ->
+ false
+
+ pid ->
+ Process.alive?(pid)
+ end
+ end
+
+ @mix_env == :benchmark ->
+ def should_env_send?, do: false
+
+ true ->
+ def should_env_send?, do: true
+ end
+
+ defp user_topic(topic, user)
+ when topic in ~w[user user:notification direct] do
+ "#{topic}:#{user.id}"
+ end
+
+ defp user_topic(topic, _) do
+ topic
+ end
end
diff --git a/lib/pleroma/web/streamer/streamer_socket.ex b/lib/pleroma/web/streamer/streamer_socket.ex
deleted file mode 100644
index 7d5dcd34e..000000000
--- a/lib/pleroma/web/streamer/streamer_socket.ex
+++ /dev/null
@@ -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
diff --git a/lib/pleroma/web/streamer/supervisor.ex b/lib/pleroma/web/streamer/supervisor.ex
deleted file mode 100644
index bd9029bc0..000000000
--- a/lib/pleroma/web/streamer/supervisor.ex
+++ /dev/null
@@ -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
diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex
deleted file mode 100644
index f6160fa4d..000000000
--- a/lib/pleroma/web/streamer/worker.ex
+++ /dev/null
@@ -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
diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
new file mode 100644
index 000000000..750f65386
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
@@ -0,0 +1,24 @@
+<%= if get_flash(@conn, :info) do %>
+<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>
diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
new file mode 100644
index 000000000..af6e546b0
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
@@ -0,0 +1,24 @@
+<%= if get_flash(@conn, :info) do %>
+<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>
diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
new file mode 100644
index 000000000..adc3a3e3d
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
@@ -0,0 +1,13 @@
+<%= if @error do %>
+<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 %>
diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
index 89da760da..521dc9322 100644
--- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
@@ -8,10 +8,12 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
require Logger
alias Pleroma.Activity
+ alias Pleroma.MFA
alias Pleroma.Object.Fetcher
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.Auth.Authenticator
+ alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.CommonAPI
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
@@ -68,6 +70,8 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
# POST /ostatus_subscribe
#
+ # adds a remote account in followers if user already is signed in.
+ #
def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
@@ -78,9 +82,33 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
end
end
+ # POST /ostatus_subscribe
+ #
+ # step 1.
+ # checks login\password and displays step 2 form of MFA if need.
+ #
def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do
- with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+ with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{_, {:ok, user}, _} <- {:auth, Authenticator.get_user(conn), followee},
+ {_, _, _, false} <- {:mfa_required, followee, user, MFA.require?(user)},
+ {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
+ redirect(conn, to: "/users/#{followee.id}")
+ else
+ error ->
+ handle_follow_error(conn, error)
+ end
+ end
+
+ # POST /ostatus_subscribe
+ #
+ # step 2
+ # checks TOTP code. otherwise displays form with errors
+ #
+ def do_follow(conn, %{"mfa" => %{"code" => code, "token" => token, "id" => id}}) do
+ with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+ {_, _, {:ok, %{user: user}}} <- {:mfa_token, followee, MFA.Token.validate(token)},
+ {_, _, _, {:ok, _}} <-
+ {:verify_mfa_code, followee, token, TOTPAuthenticator.verify(code, user)},
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
redirect(conn, to: "/users/#{followee.id}")
else
@@ -94,6 +122,23 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
end
+ defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do
+ render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
+ end
+
+ defp handle_follow_error(conn, {:verify_mfa_code, followee, token, _} = _) do
+ render(conn, "follow_mfa.html", %{
+ error: "Wrong authentication code",
+ followee: followee,
+ mfa_token: token
+ })
+ end
+
+ defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
+ {:ok, %{token: token}} = MFA.Token.create_token(user)
+ render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
+ end
+
defp handle_follow_error(conn, {:auth, _, followee} = _) do
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
end
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index 08e42a7e5..4f9281851 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -200,11 +200,17 @@ defmodule Pleroma.Web do
@impl Plug
@doc """
- If marked as skipped, returns `conn`, otherwise calls `perform/2`.
+ Before-plug hook that
+ * ensures the plug is not skipped
+ * processes `:if_func` / `:unless_func` functional pre-run conditions
+ * adds plug to the list of called plugs and calls `perform/2` if checks are passed
+
Note: multiple invocations of the same plug (with different or same options) are allowed.
"""
def call(%Plug.Conn{} = conn, options) do
- if PlugHelper.plug_skipped?(conn, __MODULE__) do
+ if PlugHelper.plug_skipped?(conn, __MODULE__) ||
+ (options[:if_func] && !options[:if_func].(conn)) ||
+ (options[:unless_func] && options[:unless_func].(conn)) do
conn
else
conn =
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 7ffd0e51b..71ccf251a 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -86,54 +86,24 @@ defmodule Pleroma.Web.WebFinger do
|> XmlBuilder.to_doc()
end
- defp get_magic_key("data:application/magic-public-key," <> magic_key) do
- {:ok, magic_key}
- end
-
- defp get_magic_key(nil) do
- Logger.debug("Undefined magic key.")
- {:ok, nil}
- end
+ defp webfinger_from_xml(doc) do
+ subject = XML.string_from_xpath("//Subject", doc)
- defp get_magic_key(_) do
- {:error, "Missing magic key data."}
- end
+ subscribe_address =
+ ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
+ |> XML.string_from_xpath(doc)
- defp webfinger_from_xml(doc) do
- with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
- {:ok, magic_key} <- get_magic_key(magic_key),
- topic <-
- XML.string_from_xpath(
- ~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
- doc
- ),
- subject <- XML.string_from_xpath("//Subject", doc),
- subscribe_address <-
- XML.string_from_xpath(
- ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
- doc
- ),
- ap_id <-
- XML.string_from_xpath(
- ~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
- doc
- ) do
- data = %{
- "magic_key" => magic_key,
- "topic" => topic,
- "subject" => subject,
- "subscribe_address" => subscribe_address,
- "ap_id" => ap_id
- }
+ ap_id =
+ ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
+ |> XML.string_from_xpath(doc)
- {:ok, data}
- else
- {:error, e} ->
- {:error, e}
+ data = %{
+ "subject" => subject,
+ "subscribe_address" => subscribe_address,
+ "ap_id" => ap_id
+ }
- e ->
- {:error, e}
- end
+ {:ok, data}
end
defp webfinger_from_json(doc) do
@@ -146,9 +116,6 @@ defmodule Pleroma.Web.WebFinger do
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"])
- {_, "http://ostatus.org/schema/1.0/subscribe"} ->
- Map.put(data, "subscribe_address", link["template"])
-
_ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}")
data
@@ -194,13 +161,15 @@ defmodule Pleroma.Web.WebFinger do
URI.parse(account).host
end
+ encoded_account = URI.encode("acct:#{account}")
+
address =
case find_lrdd_template(domain) do
{:ok, template} ->
- String.replace(template, "{uri}", URI.encode(account))
+ String.replace(template, "{uri}", encoded_account)
_ ->
- "https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
+ "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
end
with response <-