2.9 KiB · text History 4b6fe3b
defmodule OhMyExOhIrcd.IRC.Stats do
  @moduledoc """
  Cluster-wide counters for the landing page / dashboards.

  Reads directly from the Horde registry + supervisor, so values reflect
  the merged view across every node currently in the cluster.
  """

  alias OhMyExOhIrcd.Cluster.{HordeRegistry, HordeSupervisor}

  @spec channel_count() :: non_neg_integer()
  def channel_count, do: length(channel_names())

  @spec channel_names() :: [String.t()]
  def channel_names do
    Horde.Registry.select(HordeRegistry, [
      {{{:channel, :"$1"}, :_, :_}, [], [:"$1"]}
    ])
  end

  @spec user_count() :: non_neg_integer()
  def user_count, do: length(user_nicks())

  @spec user_nicks() :: [String.t()]
  def user_nicks do
    Horde.Registry.select(HordeRegistry, [
      {{{:user, :"$1"}, :_, :_}, [], [:"$1"]}
    ])
  end

  @doc """
  Snapshot of every user currently connected anywhere in the cluster.

  Queries each registered user pid (cross-node) for its WHOIS info, so a
  slow / dead pid is skipped rather than blocking the whole list. Each
  entry is tagged `:registered` (authenticated to an account),
  `:anonymous` (connected without auth), or `:service` (ChanServ et al).
  """
  @spec users() :: [map()]
  def users do
    service_nicks =
      MapSet.new([
        String.downcase(OhMyExOhIrcd.IRC.ChanServ.nick()),
        String.downcase(OhMyExOhIrcd.IRC.ScrobbleServ.nick()),
        String.downcase(OhMyExOhIrcd.IRC.ImgServ.nick())
      ])

    Horde.Registry.select(HordeRegistry, [
      {{{:user, :"$1"}, :"$2", :_}, [], [{{:"$1", :"$2"}}]}
    ])
    |> Enum.map(fn {nick, pid} -> user_snapshot(nick, pid, service_nicks) end)
    |> Enum.reject(&is_nil/1)
    |> Enum.sort_by(&String.downcase(&1.nick))
  end

  defp user_snapshot(nick, pid, service_nicks) do
    info = GenServer.call(pid, :whois_info, 1_000)
    account = Map.get(info, :account)

    kind =
      cond do
        MapSet.member?(service_nicks, String.downcase(nick)) -> :service
        account -> :registered
        true -> :anonymous
      end

    %{
      nick: info.nick || nick,
      user: Map.get(info, :user),
      host: Map.get(info, :host),
      real_host: Map.get(info, :real_host),
      account: account,
      away: Map.get(info, :away),
      kind: kind,
      node: node(pid)
    }
  catch
    _, _ -> nil
  end

  @doc "Number of children of the Horde supervisor (all process types)."
  def supervisor_child_count do
    HordeSupervisor
    |> Horde.DynamicSupervisor.which_children()
    |> length()
  end

  @spec server_info() :: %{name: String.t(), network: String.t(), version: String.t()}
  def server_info do
    cfg = Application.get_env(:oh_my_ex_oh_ircd, OhMyExOhIrcd.IRC.Listener, [])

    %{
      name: Keyword.get(cfg, :server_name, "oh-my-ex-oh.irc"),
      network: Keyword.get(cfg, :network_name, "OhMyExOhIRC"),
      version: to_string(Application.spec(:oh_my_ex_oh_ircd, :vsn))
    }
  end
end