refactor(anti-spam): make anti-spam agnostic from Akismet
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
259
lib/service/anti_spam/akismet.ex
Normal file
259
lib/service/anti_spam/akismet.ex
Normal file
@@ -0,0 +1,259 @@
|
||||
defmodule Mobilizon.Service.AntiSpam.Akismet do
|
||||
@moduledoc """
|
||||
Validate user data
|
||||
"""
|
||||
|
||||
alias Exkismet.Comment, as: AkismetComment
|
||||
alias Mobilizon.{Actors, Discussions, Events, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Reports.Report
|
||||
alias Mobilizon.Service.AntiSpam.Provider
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@env Application.compile_env(:mobilizon, :env)
|
||||
|
||||
@impl Provider
|
||||
@spec check_user(String.t(), String.t(), String.t()) ::
|
||||
:ham | :spam | :discard | {:error, HTTPoison.Response.t()}
|
||||
def check_user(email, ip, user_agent) do
|
||||
check_content(%AkismetComment{
|
||||
blog: homepage(),
|
||||
user_ip: ip,
|
||||
comment_author_email: email,
|
||||
user_agent: user_agent,
|
||||
comment_type: "signup"
|
||||
})
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
@spec check_profile(String.t(), String.t(), String.t() | nil, String.t(), String.t()) ::
|
||||
:ham | :spam | :discard | {:error, HTTPoison.Response.t()}
|
||||
def check_profile(username, summary, email \\ nil, ip \\ "127.0.0.1", user_agent \\ nil) do
|
||||
check_content(%AkismetComment{
|
||||
blog: homepage(),
|
||||
user_ip: ip,
|
||||
comment_author: username,
|
||||
comment_author_email: email,
|
||||
comment_content: summary,
|
||||
user_agent: user_agent,
|
||||
comment_type: "signup"
|
||||
})
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
@spec check_event(String.t(), String.t(), String.t() | nil, String.t(), String.t()) ::
|
||||
:ham | :spam | :discard | {:error, HTTPoison.Response.t()}
|
||||
def check_event(event_body, username, email \\ nil, ip \\ "127.0.0.1", user_agent \\ nil) do
|
||||
check_content(%AkismetComment{
|
||||
blog: homepage(),
|
||||
user_ip: ip,
|
||||
comment_author: username,
|
||||
comment_author_email: email,
|
||||
comment_content: event_body,
|
||||
user_agent: user_agent,
|
||||
comment_type: "blog-post"
|
||||
})
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
@spec check_comment(String.t(), String.t(), boolean(), String.t() | nil, String.t(), String.t()) ::
|
||||
:ham | :spam | :discard | {:error, HTTPoison.Response.t()}
|
||||
def check_comment(
|
||||
comment_body,
|
||||
username,
|
||||
is_reply?,
|
||||
email \\ nil,
|
||||
ip \\ "127.0.0.1",
|
||||
user_agent \\ nil
|
||||
) do
|
||||
check_content(%AkismetComment{
|
||||
blog: homepage(),
|
||||
user_ip: ip,
|
||||
comment_author: username,
|
||||
comment_author_email: email,
|
||||
comment_content: comment_body,
|
||||
user_agent: user_agent,
|
||||
comment_type: if(is_reply?, do: "reply", else: "comment")
|
||||
})
|
||||
end
|
||||
|
||||
@spec report_ham(Report.t()) :: :ok | {:error, atom()} | {:error, HTTPoison.Response.t()}
|
||||
def report_ham(%Report{} = report) do
|
||||
report
|
||||
|> report_to_akismet_comment()
|
||||
|> submit_ham()
|
||||
end
|
||||
|
||||
@spec report_spam(Report.t()) :: :ok | {:error, atom()} | {:error, HTTPoison.Response.t()}
|
||||
def report_spam(%Report{} = report) do
|
||||
report
|
||||
|> report_to_akismet_comment()
|
||||
|> submit_spam()
|
||||
end
|
||||
|
||||
@spec homepage() :: String.t()
|
||||
defp homepage do
|
||||
Endpoint.url()
|
||||
end
|
||||
|
||||
defp check_content(%AkismetComment{} = comment) do
|
||||
if @env != :test and ready?() do
|
||||
comment
|
||||
|> Exkismet.comment_check(key: api_key())
|
||||
else
|
||||
:ham
|
||||
end
|
||||
end
|
||||
|
||||
defp api_key do
|
||||
Application.get_env(:mobilizon, __MODULE__) |> get_in([:key])
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def ready?, do: !is_nil(api_key())
|
||||
|
||||
@spec report_to_akismet_comment(Report.t()) :: AkismetComment.t() | {:error, atom()}
|
||||
defp report_to_akismet_comment(%Report{comments: [comment | _]}) do
|
||||
with %Comment{text: body, actor: %Actor{} = actor} <-
|
||||
Discussions.get_comment_with_preload(comment.id),
|
||||
{email, preferred_username, ip} <- actor_details(actor) do
|
||||
%AkismetComment{
|
||||
blog: homepage(),
|
||||
comment_content: body,
|
||||
comment_author_email: email,
|
||||
comment_author: preferred_username,
|
||||
user_ip: ip
|
||||
}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
defp report_to_akismet_comment(%Report{event: %Event{id: event_id}}) do
|
||||
with %Event{description: body, organizer_actor: %Actor{} = actor} <-
|
||||
Events.get_event_with_preload!(event_id),
|
||||
{email, preferred_username, ip} <- actor_details(actor) do
|
||||
%AkismetComment{
|
||||
blog: homepage(),
|
||||
comment_content: body,
|
||||
comment_author_email: email,
|
||||
comment_author: preferred_username,
|
||||
user_ip: ip
|
||||
}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
defp report_to_akismet_comment(%Report{reported_id: reported_id}) do
|
||||
case reported_id |> Actors.get_actor_with_preload!() |> actor_details() do
|
||||
{email, preferred_username, ip} ->
|
||||
%AkismetComment{
|
||||
blog: homepage(),
|
||||
comment_author_email: email,
|
||||
comment_author: preferred_username,
|
||||
user_ip: ip
|
||||
}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec actor_details(Actor.t()) :: {String.t(), String.t(), any()} | {:error, :invalid_actor}
|
||||
defp actor_details(%Actor{
|
||||
type: :Person,
|
||||
preferred_username: preferred_username,
|
||||
user: %User{
|
||||
current_sign_in_ip: current_sign_in_ip,
|
||||
last_sign_in_ip: last_sign_in_ip,
|
||||
email: email
|
||||
}
|
||||
}) do
|
||||
{email, preferred_username, current_sign_in_ip || last_sign_in_ip}
|
||||
end
|
||||
|
||||
defp actor_details(%Actor{
|
||||
type: :Person,
|
||||
preferred_username: preferred_username,
|
||||
user_id: user_id
|
||||
})
|
||||
when not is_nil(user_id) do
|
||||
case user_id |> Users.get_user() |> user_details() do
|
||||
{email, ip} ->
|
||||
{preferred_username, email, ip}
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_actor}
|
||||
end
|
||||
end
|
||||
|
||||
defp actor_details(%Actor{
|
||||
type: :Person,
|
||||
preferred_username: preferred_username,
|
||||
user_id: nil
|
||||
}) do
|
||||
{nil, preferred_username, "127.0.0.1"}
|
||||
end
|
||||
|
||||
defp actor_details(_) do
|
||||
{:error, :invalid_actor}
|
||||
end
|
||||
|
||||
@spec user_details(User.t()) :: {String.t(), any()} | {:error, :user_not_found}
|
||||
defp user_details(%User{
|
||||
current_sign_in_ip: current_sign_in_ip,
|
||||
last_sign_in_ip: last_sign_in_ip,
|
||||
email: email
|
||||
}) do
|
||||
{email, current_sign_in_ip || last_sign_in_ip}
|
||||
end
|
||||
|
||||
defp user_details(_), do: {:error, :user_not_found}
|
||||
|
||||
@spec submit_spam(AkismetComment.t() | :error) ::
|
||||
:ok | {:error, atom()} | {:error, HTTPoison.Response.t()}
|
||||
defp submit_spam(%AkismetComment{} = comment) do
|
||||
comment
|
||||
|> tap(fn comment ->
|
||||
Logger.info("Submitting content to Akismet as spam: #{inspect(comment)}")
|
||||
end)
|
||||
|> Exkismet.submit_spam(key: api_key())
|
||||
|> log_response()
|
||||
end
|
||||
|
||||
defp submit_spam({:error, err}), do: {:error, err}
|
||||
|
||||
@spec submit_ham(AkismetComment.t() | :error) ::
|
||||
:ok | {:error, atom()} | {:error, HTTPoison.Response.t()}
|
||||
defp submit_ham(%AkismetComment{} = comment) do
|
||||
comment
|
||||
|> tap(fn comment ->
|
||||
Logger.info("Submitting content to Akismet as ham: #{inspect(comment)}")
|
||||
end)
|
||||
|> Exkismet.submit_ham(key: api_key())
|
||||
|> log_response()
|
||||
end
|
||||
|
||||
defp submit_ham({:error, err}), do: {:error, err}
|
||||
|
||||
defp log_response(res),
|
||||
do: tap(res, fn res -> Logger.debug("Return from Akismet is: #{inspect(res)}") end)
|
||||
end
|
||||
17
lib/service/anti_spam/anti_spam.ex
Normal file
17
lib/service/anti_spam/anti_spam.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule Mobilizon.Service.AntiSpam do
|
||||
@moduledoc """
|
||||
Module to load the service adapter defined inside the configuration.
|
||||
|
||||
See `Mobilizon.Service.AntiSpam.Provider`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns the appropriate service adapter.
|
||||
|
||||
According to the config behind
|
||||
`config :mobilizon, Mobilizon.Service.AntiSpam,
|
||||
service: Mobilizon.Service.AntiSpam.Module`
|
||||
"""
|
||||
@spec service :: module
|
||||
def service, do: get_in(Application.get_env(:mobilizon, __MODULE__), [:service])
|
||||
end
|
||||
58
lib/service/anti_spam/provider.ex
Normal file
58
lib/service/anti_spam/provider.ex
Normal file
@@ -0,0 +1,58 @@
|
||||
defmodule Mobilizon.Service.AntiSpam.Provider do
|
||||
@moduledoc """
|
||||
Provider Behaviour for anti-spam detection.
|
||||
|
||||
## Supported backends
|
||||
|
||||
* `Mobilizon.Service.AntiSpam.Akismet` [🔗](https://akismet.com/)
|
||||
|
||||
"""
|
||||
|
||||
@type spam_result :: :ham | :spam | :discard
|
||||
@type result :: spam_result() | {:error, any()}
|
||||
|
||||
@doc """
|
||||
Make sure the provider is ready
|
||||
"""
|
||||
@callback ready?() :: boolean()
|
||||
|
||||
@doc """
|
||||
Check an user details
|
||||
"""
|
||||
@callback check_user(email :: String.t(), ip :: String.t(), user_agent :: String.t()) ::
|
||||
result()
|
||||
|
||||
@doc """
|
||||
Check a profile details
|
||||
"""
|
||||
@callback check_profile(
|
||||
username :: String.t(),
|
||||
summary :: String.t(),
|
||||
email :: String.t() | nil,
|
||||
ip :: String.t(),
|
||||
user_agent :: String.t() | nil
|
||||
) :: result()
|
||||
|
||||
@doc """
|
||||
Check an event details
|
||||
"""
|
||||
@callback check_event(
|
||||
event_body :: String.t(),
|
||||
username :: String.t(),
|
||||
email :: String.t() | nil,
|
||||
ip :: String.t(),
|
||||
user_agent :: String.t() | nil
|
||||
) :: result()
|
||||
|
||||
@doc """
|
||||
Check a comment details
|
||||
"""
|
||||
@callback check_comment(
|
||||
comment_body :: String.t(),
|
||||
username :: String.t(),
|
||||
is_reply? :: boolean(),
|
||||
email :: String.t() | nil,
|
||||
ip :: String.t(),
|
||||
user_agent :: String.t() | nil
|
||||
) :: result()
|
||||
end
|
||||
Reference in New Issue
Block a user