~fnux/hanabi

6e16dbeb44785f0943fef4ab1b8fe0c4d0b2ff89 — Timothée Floure 2 years ago 46ec04a master
Add a basic structure for user modes (currently only support "r")
M CHANGELOG.md => CHANGELOG.md +2 -0
@@ 2,6 2,8 @@

## v.?.? (????-??-??)

* Add support for user modes (via the `Hanabi.User` module and the MODE command)
* Add support for the following user modes : "r"
* Add RPL_WELCOME, RPL_YOURHOST, RPL_CREATED and RPL_MYINFO to the "greeting"
  sequence (improving compatibility with 'recent' IRC clients)


M lib/hanabi/channel.ex => lib/hanabi/channel.ex +9 -0
@@ 5,6 5,7 @@ defmodule Hanabi.Channel do

  @hostname Application.get_env(:hanabi, :hostname)
  @table :hanabi_channels # ETS table, see Hanabi.Registry
  @available_modes []
  @moduledoc """
  Entry point to interact with channels.



@@ 267,4 268,12 @@ defmodule Hanabi.Channel do
    concatenated = if names, do: "#{names} #{name}", else: name
    get_names tail, concatenated
  end

  ###
  # Channel modes

  @doc """
  List the available channel modes.
  """
  def available_modes(), do: @available_modes
end

M lib/hanabi/irc/handler.ex => lib/hanabi/irc/handler.ex +60 -8
@@ 8,11 8,7 @@ defmodule Hanabi.IRC.Handler do
  @motd_file Application.get_env(:hanabi, :motd)
  @hostname Application.get_env(:hanabi, :hostname)
  @password Application.get_env(:hanabi, :password)

  # User and channel modes are not supported yet
  # See issues 9, 12, 13 and 14 on github
  @available_user_modes []
  @available_channel_modes []
  @authorized_umode_change []

  def start_link() do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)


@@ 45,7 41,7 @@ defmodule Hanabi.IRC.Handler do
    case msg.command do
      "JOIN" -> join(user, msg)
      "LIST" -> list(user, msg)
      "MODE" -> :not_implemented # @TODO
      "MODE" -> mode(user, msg)
      "MOTD" -> send_motd(user)
      "NAMES" -> names(user, msg)
      "NICK" -> set_nick(user, msg.middle)


@@ 73,8 69,8 @@ defmodule Hanabi.IRC.Handler do

    network_name = Application.get_env(:hanabi, :network_name)
    created_on = Application.get_env(:hanabi, :network_created_on)
    available_user_modes = List.to_string(@available_user_modes)
    available_channel_modes = List.to_string(@available_channel_modes)
    available_user_modes = List.to_string(User.available_modes)
    available_channel_modes = List.to_string(Channel.available_modes)

    # RPL_WELCOME
    User.send user, %Message{


@@ 199,6 195,62 @@ defmodule Hanabi.IRC.Handler do
  end

  # MODE
  def mode(%User{}=user, %Message{}=msg) do
    arguments = String.split(msg.middle)

    target = Enum.at(arguments, 0)
    modechange = Enum.at(arguments, 1)

    [_, change, mode] =
      if !is_nil(modechange) && Regex.match?(~r/^(\+|\-)(\D)$/, modechange) do
        Regex.run(~r/^(\+|\-)(\D)$/, modechange)
      else
        List.duplicate(nil, 3)
      end

    cond do
      is_nil Enum.at(arguments, 0) ->
        User.send user, %Message{
          prefix: @hostname,
          command: @err_needmoreparams,
          middle: "MODE",
          trailing: "Not enough parameters"
        }
      user.nick != target ->
        User.send user, %Message{
          prefix: @hostname,
          command: @err_userdontmatch,
          middle: user.nick,
          trailing: "Cannot change mode for other users"
        }
      Enum.count(arguments) == 1 ->
        User.send user, %Message{
          prefix: @hostname,
          command: @rpl_umodeis,
          middle: "#{user.nick} #{List.to_string(user.modes)}"
        }
      not Enum.member?(User.available_modes, mode) ->
        User.send user, %Message{
          prefix: @hostname,
          command: @err_umodeunknownflag,
          middle: user.nick,
          trailing: "Unknown MODE flag"
        }
      not Enum.member?(@authorized_umode_change, modechange) -> :noop #ignore
      true ->
        {:ok, _modes} = case change do
          "+" -> User.add_mode(user, mode)
          "-" -> User.remove_mode(user, mode)
        end

        User.send user, %Message{
          prefix: user.nick,
          command: "MODE",
          middle: target,
          trailing: modechange
        }
    end
  end

  # MOTD
  def send_motd(%User{}=user) do

M lib/hanabi/irc/numeric.ex => lib/hanabi/irc/numeric.ex +5 -0
@@ 25,6 25,8 @@ defmodule Hanabi.IRC.Numeric do
      @rpl_created "003"
      @rpl_myinfo "004"

      @rpl_umodeis "221"

      @rpl_whoisuser "311"
      @rpl_endofwhois "318"
      @rpl_liststart "321"


@@ 46,6 48,9 @@ defmodule Hanabi.IRC.Numeric do
      @err_notonchannel "442"
      @err_needmoreparams "461"
      @err_alreadyregistered "462"

      @err_umodeunknownflag "501"
      @err_userdontmatch "502"
    end
  end
end

M lib/hanabi/user.ex => lib/hanabi/user.ex +94 -1
@@ 4,6 4,7 @@ defmodule Hanabi.User do
  use Hanabi.IRC.Numeric

  @table :hanabi_users # ETS table name, see Hanabi.Registry
  @available_modes ["r"]
  @moduledoc """
  Entry point to interact with users.



@@ 12,6 13,7 @@ defmodule Hanabi.User do
  ```
  %Hanabi.User{
    channels: [],
    modes: [],
    hostname: nil,
    is_pass_validated?: false,
    key: nil,


@@ 48,7 50,8 @@ defmodule Hanabi.User do
    port: nil,
    pid: nil,
    data: nil,
    channels: []
    channels: [],
    modes: []

  ####
  # Registry access


@@ 327,4 330,94 @@ defmodule Hanabi.User do
  end
  def remove(nil, _), do: :err
  def remove(user_key, part_msg), do: remove(User.get(user_key), part_msg)

  ###
  # User modes

  @doc """
  List the available user modes :

  * `"r"` : registered user mode
  """
  def available_modes, do: @available_modes

  defp check_modes_validity([]), do: true
  defp check_modes_validity([mode|tail]) do
    if check_modes_validity(mode), do: check_modes_validity(tail), else: false
  end
  defp check_modes_validity(mode), do: Enum.member?(available_modes(), mode)

  @doc """
  Add a mode to an user.

  * `user` is either the user's struct or identifier
  * `mode` is the mode to be added

  Return values:

  * `{:ok, new_list_of_modes}`
  * `{:error, "unknown user mode"}`

  ## Examples :
  ```
  iex> user.modes
  []
  iex> Hanabi.User.add_mode(user, "r")
  {:ok, ["r"]}
  iex> Hanabi.User.add_mode(user, "z") # unknown mode z
  {:error, "unknown user mode"}
  ```
  """
  def add_mode(%User{}=user, mode) do
    cond do
      not Enum.member?(available_modes(), mode) -> {:error, "unknown user mode"}
      Enum.member?(user.modes, mode) -> {:ok, user.modes}
      true -> {:ok, update(user, modes: user.modes ++ [mode]).modes}
    end
  end
  def add_mode(user_key, mode), do: add_mode User.get(user_key), mode

  @doc """
  Remove a mode from an user.

  * `user` is either the user's struct or identifier
  * `mode` is the mode to be removed

  Returns `{:ok, new_list_of_modes}`.

  ## Examples :
  ```
  iex> user.modes
  ["r"]
  iex> Hanabi.User.remove_mode(user, "r")
  {:ok, []}
  iex> Hanabi.User.remove_mode(user, "z") # unknown mode z
  {:ok, []}
  """
  def remove_mode(%User{}=user, mode) do
    if Enum.member?(user.modes, mode) do
      {:ok, update(user, modes: List.delete(user.modes, mode)).modes}
    else
      {:ok, user.modes}
    end
  end
  def remove_mode(user_key, mode), do: remove_mode User.get(user_key), mode

  @doc """
  Set the list of modes applied to the user.

  * `user` is either the user's struct or identifier
  * `modes` is a list of modes (ex: `["r", "i"]`)

  Returns either `{:ok, news_list_of_modes}` or `{:error, "unknown modes"}`
  (if `modes` contains items unknown to `available_modes/0`).
  """
  def set_modes(%User{}=user, modes) do
    if check_modes_validity(modes) do
      {:ok, update(user, modes: modes).modes}
    else
      {:error, "unknown modes"}
    end
  end
  def set_modes(user_key, modes), do: set_modes User.get(user_key), modes
end