Finished most of the IRC protocol-level work:
authorMatt Mullins <mmullins@mmlx.us>
Thu, 19 May 2011 20:15:10 +0000 (15:15 -0500)
committerMatt Mullins <mmullins@mmlx.us>
Thu, 19 May 2011 22:59:53 +0000 (17:59 -0500)
  * added module irc_util to hold the parser and writer for IRC commands
  * irc_conn module should at least be able to join channels

irc/irc_conn.erl
irc/irc_net_sup.erl
irc/irc_util.erl [new file with mode: 0644]
irc/irc_util.hrl [new file with mode: 0644]

index 31d1037..3af4399 100644 (file)
@@ -2,7 +2,7 @@
 -behavior(gen_server).
 
 -export([
-          start_link/5
+          start_link/2
         ]).
 
 -export([
           code_change/3
         ]).
 
--record(irc_state, {instance, host, port, nick, socket}).
+-include("irc_util.hrl").
+
+-record(irc_state, {instance,      % instance name
+                    config,        % initial configuration params
+                    socket,
+                    buffer = ""    % for the TCP session
+                   }).
 
 start_link(Instance, Config) ->
     gen_server:start_link(?MODULE,
         {Instance, Config}, []).
 
-init({Instance, {Host, Port, Nick}}) ->
+init({Instance, Config = {Host, Port, Nick, Realname, Channels}}) ->
     {ok, Socket} = gen_tcp:connect(Host, Port, [list]),
-    State = #irc_state{
-        instance=Instance, host=Host,
-        port=Port, nick=Nick, socket=Socket},
+    send_command(#irc_command{command = "NICK", middles = [Nick]}),
+    send_command(#irc_command{command = "USER",
+                              middles = ["user", "host", "server"],
+                              trailing = Realname
+                             }),
+    lists:foreach(fun join_channel/1, Channels),
+    State = #irc_state{instance = Instance,
+                       config = Config,
+                       socket = Socket
+                      },
     {ok, State}.
 
 handle_call(_Request, _From, _State) ->
     {reply, ok, _State}.
 
-handle_cast(_Request, _State) ->
-    {noreply, _State}.
+handle_cast({send_command, Command}, State) ->
+    gen_tcp:send(State#irc_state.socket,
+                 irc_util:command_to_list(Command) ++ "\r\n"
+                ),
+    {noreply, State}.
 
 handle_info(_Message, _State) ->
     {noreply, _State}.
@@ -41,3 +57,10 @@ terminate(_Reason, _State) ->
 
 code_change(_OldVsn, State, _Extra) ->
     {ok, State}.
+
+send_command(Command) ->
+    gen_server:cast(self(), {send_command, Command}).
+
+join_channel(Channel) ->
+    Command = #irc_command{command = "JOIN", middles = [Channel]},
+    send_command(Command).
index 3d26ad6..cd4be7a 100644 (file)
@@ -7,7 +7,7 @@
 -behavior(supervisor).
 
 -export([
-          start_link/4,
+          start_link/2
                ]).
 -export([init/1]).
 
diff --git a/irc/irc_util.erl b/irc/irc_util.erl
new file mode 100644 (file)
index 0000000..e2de915
--- /dev/null
@@ -0,0 +1,75 @@
+%% @doc Contains methods to parse and generate messages conforming to
+%% the IRC protocol.  Works with the record(s) in irc_util.hrl.
+
+-module(irc_util).
+-export([list_to_command/1, command_to_list/1]).
+
+-include("irc_util.hrl").
+
+%% @doc Parses a string into an irc_command record
+list_to_command(String) ->
+    {Prefix, Body} = parse_prefix(String),
+    {Command, Params} = parse_command(Body),
+    {Middles, Trailing} = parse_params(Params),
+    #irc_command{prefix = Prefix,
+                 command = Command,
+                 middles = Middles,
+                 trailing = Trailing
+                }.
+
+%% @doc Parses and splits off the prefix
+% If it has a : at the beginning, then we have a prefix
+parse_prefix(":" ++ Rest) ->
+    {Prefix, Body} = split_space(Rest),
+    {Prefix, Body};
+
+% Otherwise, no prefix, time for the next guy.
+parse_prefix(Body) ->
+    {none, Body}.
+
+%% @doc Parses and splits off the command portion
+parse_command(Body) ->
+    split_space(Body).
+
+%% @doc Splits the parameters into the middle and trailing sections
+% Base case is when there is nothing left
+parse_params("") ->
+    {[], none}; % no parameters at all
+
+% Or when we find a :, then the rest of the command line is trailing
+parse_params(":" ++ Rest) ->
+    {[], Rest};
+
+% Otherwise, split off a parameter and recurse!
+parse_params(String) ->
+    {Param, Rest} = split_space(String),
+    {Middles, Trailing} = parse_params(Rest),
+    {[Param | Middles], Trailing}.
+
+%% @doc Split a string at the space character
+split_space(String) ->
+    {Left, RHS} = lists:splitwith(fun (Char) -> Char /= $  end, String),
+    % drop any space character that would come with RHS:
+    Right = case RHS of
+        " " ++ Rest -> Rest;
+        Other -> Other
+    end,
+    {Left, Right}.
+
+%% @doc Returns the string representation (i.e. suitable for the IRC protocol)
+%% representing a particular command
+command_to_list(IrcCommand) ->
+    Prefix = case IrcCommand#irc_command.prefix of
+        none -> "";
+        PrefixString -> ":" ++ PrefixString ++ " "
+    end,
+    Command = IrcCommand#irc_command.command,
+    Middles = case IrcCommand#irc_command.middles of
+        [] -> "";
+        MiddleStrings -> " " ++ string:join(MiddleStrings, " ")
+    end,
+    Trailing = case IrcCommand#irc_command.trailing of
+        none -> "";
+        TrailingString -> " :" ++ TrailingString
+    end,
+    Prefix ++ Command ++ Middles ++ Trailing.
diff --git a/irc/irc_util.hrl b/irc/irc_util.hrl
new file mode 100644 (file)
index 0000000..cfa5b44
--- /dev/null
@@ -0,0 +1,6 @@
+-record(irc_command,
+    {prefix = none,  % prefix of IRC command
+     command,        % command -- the only element required
+     middles = [],   % [string] for arguments
+     trailing = none % follows the : at the end of a command
+    }).