6e16dbeb44785f0943fef4ab1b8fe0c4d0b2ff89 — Timothée Floure 1 year, 5 months 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 @@     @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 @@ 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 @@ @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 @@ 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 @@       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 @@ 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 @@ @rpl_created "003"
        @rpl_myinfo "004"
  
+       @rpl_umodeis "221"
+ 
        @rpl_whoisuser "311"
        @rpl_endofwhois "318"
        @rpl_liststart "321"


@@ 46,6 48,9 @@ @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 @@ 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 @@ ```
    %Hanabi.User{
      channels: [],
+     modes: [],
      hostname: nil,
      is_pass_validated?: false,
      key: nil,


@@ 48,7 50,8 @@ port: nil,
      pid: nil,
      data: nil,
-     channels: []
+     channels: [],
+     modes: []
  
    ####
    # Registry access


@@ 327,4 330,94 @@ 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