2.1 KiB · text History 4b6fe3b
defmodule OhMyExOhIrcd.IRC.Klines do
  @moduledoc """
  K-line / G-line ban context. Since everything is stored in the
  cluster-wide Postgres instance, K-lines and G-lines are functionally
  identical in this implementation — both are persisted host-mask bans
  enforced on every node.

  Enforcement runs at registration time (`OhMyExOhIrcd.IRC.Connection`
  calls `match/1` once nick/user/host are all known) and on demand for
  existing connections (`disconnect_matching!/1` after KLINE).
  """

  import Ecto.Query

  alias OhMyExOhIrcd.Repo
  alias OhMyExOhIrcd.IRC.Klines.Kline

  @doc """
  Add a new K-line. `duration_seconds` is optional — `nil` means
  permanent. `set_by` is the oper's nick.
  """
  def add(mask, set_by, opts \\ []) do
    expires_at =
      case Keyword.get(opts, :duration_seconds) do
        nil -> nil
        secs when is_integer(secs) and secs > 0 -> DateTime.add(DateTime.utc_now(), secs)
      end

    attrs = %{
      mask: mask,
      set_by: set_by,
      reason: Keyword.get(opts, :reason, ""),
      expires_at: expires_at
    }

    %Kline{}
    |> Kline.changeset(attrs)
    |> Repo.insert()
  end

  @doc "Remove the K-line matching `mask` exactly. Returns the number of rows deleted."
  def remove(mask) do
    {n, _} = Repo.delete_all(from k in Kline, where: k.mask == ^mask)
    n
  end

  @doc "All active (non-expired) K-lines."
  def list_active do
    now = DateTime.utc_now()

    Repo.all(
      from k in Kline,
        where: is_nil(k.expires_at) or k.expires_at > ^now,
        order_by: [asc: k.inserted_at]
    )
  end

  @doc """
  Match `user_at_host` (a `user@host` string built from the joining
  client's prefix) against every active K-line. Returns the first
  matching record or `nil`.
  """
  def match(user_at_host) do
    Enum.find(list_active(), fn k -> match_glob?(user_at_host, k.mask) end)
  rescue
    _ -> nil
  end

  defp match_glob?(string, mask) do
    pattern =
      mask
      |> Regex.escape()
      |> String.replace("\\*", ".*")
      |> String.replace("\\?", ".")

    Regex.match?(~r/\A#{pattern}\z/i, string)
  end
end