add irc_user module for irc_chan to track users on a channel
authorTero Marttila <terom@fixme.fi>
Thu, 26 Mar 2009 21:46:10 +0200
changeset 72 43084f103c2a
parent 71 0a13030f795d
child 73 2780a73c71f3
add irc_user module for irc_chan to track users on a channel
TODO
src/CMakeLists.txt
src/irc_chan.c
src/irc_chan.h
src/irc_net.c
src/irc_net.h
src/irc_proto.c
src/irc_proto.h
src/irc_user.c
src/irc_user.h
src/test.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO	Thu Mar 26 21:46:10 2009 +0200
@@ -0,0 +1,17 @@
+sock:
+ * async connect + resolve
+ * async SSL handshake
+ * sock_openssl, or improve sock_gnutls
+ * tests for all of the above...
+
+irc_net:
+ * tracking of channel users list
+
+modules:
+ * proper unload support, there needs to be some kind of completion notification thing that can then destroy the module
+   once it's completely unloaded - maybe some kind of module-resource-reference-counting thing, which will eventually
+   be needed anyways?
+
+irc_log:
+ * logging of NICK/QUIT
+ * handling of SQL errors?
--- a/src/CMakeLists.txt	Mon Mar 16 23:55:30 2009 +0200
+++ b/src/CMakeLists.txt	Thu Mar 26 21:46:10 2009 +0200
@@ -10,7 +10,7 @@
 # define our source code modules
 set (CORE_SOURCES error.c log.c)
 set (SOCK_SOURCES sock.c sock_tcp.c sock_gnutls.c sock_test.c line_proto.c)
-set (IRC_SOURCES irc_line.c irc_conn.c irc_net.c irc_chan.c chain.c irc_cmd.c irc_proto.c irc_client.c)
+set (IRC_SOURCES irc_line.c irc_conn.c irc_net.c irc_chan.c chain.c irc_cmd.c irc_proto.c irc_client.c irc_user.c)
 
 set (NEXUS_SOURCES nexus.c ${CORE_SOURCES} ${SOCK_SOURCES} ${IRC_SOURCES} signals.c module.c)
 set (TEST_SOURCES test.c ${CORE_SOURCES} ${SOCK_SOURCES} ${IRC_SOURCES})
--- a/src/irc_chan.c	Mon Mar 16 23:55:30 2009 +0200
+++ b/src/irc_chan.c	Thu Mar 26 21:46:10 2009 +0200
@@ -3,6 +3,7 @@
 #include "log.h"
 
 #include <stdlib.h>
+#include <string.h>
 #include <assert.h>
 
 const char* irc_chan_name (struct irc_chan *chan)
@@ -11,11 +12,48 @@
 }
 
 /**
+ * Add or update a nickname to the irc_chan.users list.
+ *
+ * If the given nickname already exists in our users list, this does nothing. Otherwise, an irc_user is aquired using
+ * irc_net_get_user, and a new irc_chan_user struct added to our users list.
+ */
+static err_t irc_chan_add_user (struct irc_chan *chan, const char *nickname)
+{
+    struct irc_user *user;
+    struct irc_chan_user *chan_user;
+    err_t err;
+
+    // skip if already listed
+    if (irc_chan_get_user(chan, nickname))
+        return SUCCESS;
+    
+    // lookup/create the irc_user state
+    if ((err = irc_net_get_user(chan->net, &user, nickname)))
+        return err;
+
+    // alloc the new irc_chan_user
+    if ((chan_user = calloc(1, sizeof(*chan_user))) == NULL) {
+        // XXX: release irc_user
+        return ERR_CALLOC;
+    }
+
+    // store
+    chan_user->user = user;
+
+    // add to users list
+    LIST_INSERT_HEAD(&chan->users, chan_user, chan_users);
+
+    // ok
+    return SUCCESS;
+}
+
+/**
  * :nm JOIN <channel>
  */ 
 static void irc_chan_on_JOIN (const struct irc_line *line, void *arg)
 {
     struct irc_chan *chan = arg;
+    err_t err;
 
     // us?
     if (irc_prefix_cmp_nick(line->prefix, chan->net->conn->nickname) == 0) {
@@ -29,8 +67,19 @@
         IRC_CHAN_INVOKE_CALLBACK(chan, on_self_join);
 
     } else {
-        // XXX: who cares
+        char prefix_buf[IRC_PREFIX_MAX];
+        struct irc_nm nm;
         
+        // parse the nickname
+        if ((err = irc_nm_parse(&nm, prefix_buf, line->prefix)))
+            return log_warn("invalid prefix: %s", line->prefix);
+        
+        // add them
+        if ((err = irc_chan_add_user(chan, nm.nickname)))
+            return log_warn("irc_chan_add_user(%s, %s): %s", irc_chan_name(chan), nm.nickname, error_name(err));
+
+        // invoke callback (source)
+        IRC_CHAN_INVOKE_CALLBACK(chan, on_join, &nm);
     }
 }
 
@@ -50,22 +99,69 @@
     if ((err = irc_nm_parse(&nm, prefix_buf, line->prefix))) {
         log_warn("invalid prefix: %s", line->prefix);
         
-        // invoke callback with NULL prefix
+        // invoke callback with NULL source
         IRC_CHAN_INVOKE_CALLBACK(chan, on_msg, NULL, msg);
 
     } else {
-        // invoke callback (prefix, message)
+        // invoke callback (source, message)
         IRC_CHAN_INVOKE_CALLBACK(chan, on_msg, &nm, msg);
     }
 }
 
 /**
+ * Add/update nicknames to users list using irc_chan_add_user
+ *
+ * @see IRC_RPL_NAMREPLY
+ */
+static void irc_chan_on_RPL_NAMREPLY (const struct irc_line *line, void *arg)
+{
+    struct irc_chan *chan = arg;
+    char chanflags[IRC_CHANFLAGS_MAX];
+    const char *nickname;
+    err_t err;
+
+    // the args
+    const char *arg_names = line->args[3];
+
+    // copy the nicklist to a mutable buffer
+    char names_buf[strlen(arg_names) + 1], *names = names_buf;
+    strcpy(names, arg_names);
+
+    // iterate over each name
+    // XXX: nickflags
+    while ((nickname = strsep(&names, " "))) {
+        // parse off the channel flags
+        nickname = irc_nick_chanflags(nickname, chanflags);
+
+        // add/update
+        // XXX: do something with chanflags
+        if ((err = irc_chan_add_user(chan, nickname)))
+            log_warn("irc_chan_add_user(%s, %s): %s", irc_chan_name(chan), nickname, error_name(err));
+    }
+}
+
+/**
+ * Channel join sequence complete
+ *
+ * @see IRC_RPL_ENDOFNAMES
+ */
+static void irc_chan_on_RPL_ENDOFNAMES (const struct irc_line *line, void *arg)
+{
+    struct irc_chan *chan = arg;
+
+    // XXX: update state
+    log_info("channel join sync complete");
+}
+
+/**
  * Core command handlers
  */
 struct irc_cmd_handler _cmd_handlers[] = {
-    {   "JOIN",     &irc_chan_on_JOIN       },
-    {   "PRIVMSG",  &irc_chan_on_PRIVMSG    },
-    {   NULL,       NULL                    }
+    {   "JOIN",             &irc_chan_on_JOIN           },
+    {   "PRIVMSG",          &irc_chan_on_PRIVMSG        },
+    {   IRC_RPL_NAMREPLY,   &irc_chan_on_RPL_NAMREPLY   },
+    {   IRC_RPL_ENDOFNAMES, &irc_chan_on_RPL_ENDOFNAMES },
+    {   NULL,       NULL                                }
 };
 
 err_t irc_chan_create (struct irc_chan **chan_ptr, struct irc_net *net, const struct irc_chan_info *info, struct error_info *err)
@@ -81,6 +177,7 @@
     chan->info = *info;
 
     // init
+    LIST_INIT(&chan->users);
     irc_cmd_init(&chan->handlers);
     CHAIN_INIT(&chan->callbacks);
     
@@ -102,6 +199,8 @@
 
 void irc_chan_destroy (struct irc_chan *chan)
 {
+    // XXX: free chan_users list
+
     // free command handlers
     irc_cmd_free(&chan->handlers);
 
@@ -122,6 +221,22 @@
     chain_remove(&chan->callbacks, callbacks, arg);
 }
 
+struct irc_chan_user* irc_chan_get_user (struct irc_chan *chan, const char *nickname)
+{
+    struct irc_chan_user *chan_user = NULL;
+
+    // look for it...
+    LIST_FOREACH(chan_user, &chan->users, chan_users) {
+        if (irc_cmp_nick(nickname, chan_user->user->nickname) == 0) {
+            // found
+            return chan_user;
+        }
+    }
+    
+    // not found
+    return NULL;
+}
+
 err_t irc_chan_join (struct irc_chan *chan)
 {
     err_t err;
--- a/src/irc_chan.h	Mon Mar 16 23:55:30 2009 +0200
+++ b/src/irc_chan.h	Thu Mar 26 21:46:10 2009 +0200
@@ -10,6 +10,7 @@
 struct irc_chan;
 
 #include "irc_net.h"
+#include "irc_user.h"
 #include "irc_cmd.h"
 #include "irc_proto.h"
 #include "error.h"
@@ -33,6 +34,9 @@
     /** Joined the channel */
     void (*on_self_join) (struct irc_chan *chan, void *arg);
 
+    /** Someone joined the channel */
+    void (*on_join) (struct irc_chan *chan, const struct irc_nm *source, void *arg);
+
     /** Someone sent a message to the channel */
     void (*on_msg) (struct irc_chan *chan, const struct irc_nm *source, const char *msg, void *arg);
 };
@@ -53,6 +57,17 @@
     } while (0);
 
 /**
+ * Per-channel-user state
+ */
+struct irc_chan_user {
+    /** The per-network user info */
+    struct irc_user *user;
+
+    /** The irc_chan list */
+    LIST_ENTRY(irc_chan_user) chan_users;
+};
+
+/**
  * IRC channel state
  */
 struct irc_chan {
@@ -65,15 +80,22 @@
     /** Our identifying info */
     struct irc_chan_info info;
     
-    /** State flags @{ */
-        /** JOIN request sent, waiting for reply */
-        bool joining;
+    /** 
+     * @group State flags 
+     * @{ 
+     */
 
-        /** Currently joined to channel */
-        bool joined;
+    /** JOIN request sent, waiting for reply */
+    bool joining;
+
+    /** Currently joined to channel */
+    bool joined;
 
     // @}
     
+    /** List of users on channel */
+    LIST_HEAD(irc_chan_users_list, irc_chan_user) users;
+    
     /** General command handlers */
     irc_cmd_handlers_t handlers;
 
@@ -114,6 +136,15 @@
 void irc_chan_remove_callbacks (struct irc_chan *chan, const struct irc_chan_callbacks *callbacks, void *arg);
 
 /**
+ * Look up an irc_chan_user struct by nickname for this channel, returning NULL if no such chan_user exists.
+ *
+ * @param chan the channel context
+ * @param nickname the user's current nickname
+ * @param return the irc_chan_user state, or NULL if nickname not found
+ */
+struct irc_chan_user* irc_chan_get_user (struct irc_chan *chan, const char *nickname);
+
+/**
  * Send the initial JOIN message.
  *
  * The channel must be in the IRC_CHAN_INIT state, and will transition to the IRC_CHAN_JOINING state.
--- a/src/irc_net.c	Mon Mar 16 23:55:30 2009 +0200
+++ b/src/irc_net.c	Thu Mar 26 21:46:10 2009 +0200
@@ -4,6 +4,11 @@
 #include <stdlib.h>
 #include <string.h>
 
+const char* irc_net_name (struct irc_net *net)
+{
+    return net->info.network;
+}
+
 /**
  * Something happaned which caused our irc_conn to fail. Destroy it, and recover. XXX: somehow
  */
@@ -108,6 +113,26 @@
 }
 
 /**
+ * Propagate line to irc_chan based on args[1]
+ */ 
+static void irc_net_on_chan1 (const struct irc_line *line, void *arg)
+{
+    struct irc_net *net = arg;
+    
+    irc_net_propagate_chan(net, line, line->args[1]);
+}
+
+/**
+ * Propagate line to irc_chan based on args[2]
+ */ 
+static void irc_net_on_chan2 (const struct irc_line *line, void *arg)
+{
+    struct irc_net *net = arg;
+    
+    irc_net_propagate_chan(net, line, line->args[2]);
+}
+
+/**
  * :nm (PRIVMSG|NOTICE) <target> <message>
  *
  * Either propagate to channel if found, otherwise handle as a privmsg
@@ -131,18 +156,22 @@
  * Our irc_cmd handler list
  */
 static struct irc_cmd_handler _cmd_handlers[] = {
-    {   "JOIN",     &irc_net_on_chan0       },
-    {   "PART",     &irc_net_on_chan0       },
-    {   "MODE",     &irc_net_on_chan0       },
-    {   "TOPIC",    &irc_net_on_chan0       },
-    {   "KICK",     &irc_net_on_chan0       },
+    {   "JOIN",             &irc_net_on_chan0       },
+    {   "PART",             &irc_net_on_chan0       },
+    {   "MODE",             &irc_net_on_chan0       },
+    {   "TOPIC",            &irc_net_on_chan0       },
+    {   "KICK",             &irc_net_on_chan0       },
 
-    {   "PRIVMSG",  &irc_net_on_msg         },
-    {   "NOTICE",   &irc_net_on_msg         },
+    {   "PRIVMSG",          &irc_net_on_msg         },
+    {   "NOTICE",           &irc_net_on_msg         },
 
     // XXX: NICK/QUIT
 
-    {   NULL,       NULL                    }
+    // numerics
+    {   IRC_RPL_NAMREPLY,   &irc_net_on_chan2       },
+    {   IRC_RPL_ENDOFNAMES, &irc_net_on_chan1       },
+
+    {   NULL,               NULL                    }
 };
 
 err_t irc_net_create (struct irc_net **net_ptr, const struct irc_net_info *info, struct error_info *err)
@@ -157,6 +186,7 @@
     // initialize
     net->info = *info;
     TAILQ_INIT(&net->channels);
+    LIST_INIT(&net->users);
 
     if (info->raw_sock) {
         log_info("connected using raw socket: %p", info->raw_sock);
@@ -225,6 +255,8 @@
         irc_chan_destroy(chan);
     }
 
+    // XXX: our users
+
     // ourselves
     free(net);
 }
@@ -269,6 +301,34 @@
     return NULL;
 }
 
+err_t irc_net_get_user (struct irc_net *net, struct irc_user **user_ptr, const char *nickname)
+{
+    struct irc_user *user = NULL;
+    err_t err;
+
+    // look for it...
+    LIST_FOREACH(user, &net->users, net_users) {
+        if (irc_cmp_nick(nickname, user->nickname) == 0) {
+            // found existing
+            *user_ptr = user;
+
+            return SUCCESS;
+        }
+    }
+    
+    // create a new user struct
+    if ((err = irc_user_create(&user, net, nickname)))
+        return err;
+
+    // add to our list
+    LIST_INSERT_HEAD(&net->users, user, net_users);
+
+    // ok
+    *user_ptr = user;
+
+    return SUCCESS;
+}
+
 err_t irc_net_quit (struct irc_net *net, const char *message)
 {
    if (!net->conn)
--- a/src/irc_net.h	Mon Mar 16 23:55:30 2009 +0200
+++ b/src/irc_net.h	Thu Mar 26 21:46:10 2009 +0200
@@ -7,6 +7,9 @@
  * Support for IRC networks. This is similar to an IRC connection, but we keep track of channel state, and handle
  * reconnects.
  */
+
+struct irc_net;
+
 #include "error.h"
 #include "irc_conn.h"
 #include "irc_chan.h"
@@ -48,11 +51,19 @@
     /** The list of IRC channel states */
     TAILQ_HEAD(irc_net_chan_list, irc_chan) channels;
 
+    /** Our set of valid irc_user items for use with irc_chan */
+    LIST_HEAD(irc_net_users_list, irc_user) users;
+
     /** The irc_client list */
     TAILQ_ENTRY(irc_net) client_networks;
 };
 
 /**
+ * Return the networks's name
+ */
+const char* irc_net_name (struct irc_net *net);
+
+/**
  * Create a new IRC network state, using the given network info to connect/register.
  *
  * Errors are returned via *err, also returning the error code.
@@ -84,12 +95,21 @@
 /**
  * Look up an existing irc_chan by name, returning NULL if not found.
  *
- * @param net the network state
+ * @param net the network context
  * @param channel the channel name
  */
 struct irc_chan* irc_net_get_chan (struct irc_net *net, const char *channel);
 
 /**
+ * Look up an existing irc_user by nickname, or create a new one if not found.
+ *
+ * @param user_ptr set to the new irc_user state
+ * @param net the network context
+ * @param nickname the user's current nickname
+ */
+err_t irc_net_get_user (struct irc_net *net, struct irc_user **user_ptr, const char *nickname);
+
+/**
  * Quit from the IRC network, this sends a QUIT message to the server, and waits for the connection to be closed cleanly.
  */
 err_t irc_net_quit (struct irc_net *net, const char *message);
--- a/src/irc_proto.c	Mon Mar 16 23:55:30 2009 +0200
+++ b/src/irc_proto.c	Thu Mar 26 21:46:10 2009 +0200
@@ -81,3 +81,19 @@
         // doesn't match
         return 1;
 }
+
+const char* irc_nick_chanflags (const char *nick, char chanflags[IRC_CHANFLAGS_MAX])
+{
+    char *cf = chanflags;
+
+    // consume the chanflags, using strchr to look for the char in the set of chanflags...
+    while (strchr(IRC_CHANFLAGS, *nick) && (cf < chanflags + IRC_CHANFLAGS_MAX - 1))
+        *cf++ = *nick++;
+
+    // NUL-terminate chanflags
+    *cf = '\0';
+
+    // then return the pointer to the first nickname char
+    return nick;
+}
+
--- a/src/irc_proto.h	Mon Mar 16 23:55:30 2009 +0200
+++ b/src/irc_proto.h	Thu Mar 26 21:46:10 2009 +0200
@@ -12,7 +12,7 @@
 /**
  * Maximum length of an IRC nickname including terminating NUL.
  */
-#define IRC_NICK_MAX 31
+#define IRC_NICK_MAX (30 + 1)
 
 /**
  * Maximum length of an IRC prefix/nickmask string, including any termianting NULs.
@@ -22,6 +22,16 @@
 #define IRC_PREFIX_MAX 510
 
 /**
+ * Maximum length of an IRC nickname channel flags prefix, including any terminating NUL
+ */
+#define IRC_CHANFLAGS_MAX (8 + 1)
+
+/**
+ * The set of characters that are considered nickname channel flag prefixes (i.e. op, voice, etc).
+ */
+#define IRC_CHANFLAGS "@+"
+
+/**
  * Parsed nickmask
  */
 struct irc_nm {
@@ -66,15 +76,47 @@
 int irc_prefix_cmp_nick (const char *prefix, const char *nick);
 
 /**
+ * Copy the prefixed flags from the given nickname into the given flags buffer (of at least IRC_CHANFLAGS_MAX bytes) and
+ * NUL-terminate it. Returns a pointer to the actual nickname itself.
+ *
+ * @param nick the nickname, including prefixed chanflags
+ * @param chanflags the buffer to store the parsed chanflags into
+ * @returns a pointer to the first char of the actual nickname
+ */
+const char* irc_nick_chanflags (const char *nick, char chanflags[IRC_CHANFLAGS_MAX]);
+
+/**
  * @group IRC command numerics
  * @{
  */
 
 /**
- * 001 <nick> :Welcome to the Internet Relay Network <nick>!<user>@<host>
+ * 001 <nick> "Welcome to the Internet Relay Network <nick>!<user>@<host>"
+ *
+ * The first "official" reply sent by the server after the NICK/USER registration was accepted.
  */
 #define IRC_RPL_WELCOME         "001"
 
+/**
+ * 353 <nick> ( "=" | "*" | "@" ) <channel> ( [ "@" | "+" ] <nick> [ " " ... ] )
+ *
+ * Sent by the server after a JOIN/NAMES command to give the full list of users currently on a channel. The list may
+ * be split into multiple messages RPL_NAMREPLY messages, which are then terminated by a RPL_ENDOFNAMES reply.
+ *
+ * The first argument char denotes the "channel type", and is, apparently, one of the following, for those who care:
+ *  @   secret
+ *  *   private
+ *  =   others (public)
+ */
+#define IRC_RPL_NAMREPLY        "353"
+
+/**
+ * 366 <nick> <channel> "End of NAMES list"
+ *
+ * Sent by the server to terminate a sequence of zero or more RPL_NAMREPLY messages from a JOIN/NAMES command.
+ */
+#define IRC_RPL_ENDOFNAMES      "366"
+
 // @}
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/irc_user.c	Thu Mar 26 21:46:10 2009 +0200
@@ -0,0 +1,29 @@
+#include "irc_user.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+err_t irc_user_create (struct irc_user **user_ptr, struct irc_net *net, const char *nickname)
+{
+    struct irc_user *user;
+    
+    // allocate
+    if ((user = calloc(1, sizeof(*user))) == NULL)
+        return ERR_CALLOC;
+
+    // copy nickname
+    if ((user->nickname = strdup(nickname)) == NULL)
+        return ERR_STRDUP;
+
+    // ok
+    *user_ptr = user;
+
+    return SUCCESS;
+}
+
+void irc_user_destroy (struct irc_user *user)
+{
+    free(user->nickname);
+    free(user);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/irc_user.h	Thu Mar 26 21:46:10 2009 +0200
@@ -0,0 +1,37 @@
+#ifndef IRC_USER_H
+#define IRC_USER_H
+
+/**
+ * Minimal state for tracking per-user state for an irc_net. Mainly, the purpose of this module is to support the
+ * irc_chan.users implementation, such that when a user's nickname is changed, one does not need to modify all
+ * irc_chan lists, but rather, just replace the nickname field here.
+ *
+ * XXX: should this tie in to irc_net to register its own NICK/QUIT handlers?
+ */
+#include "irc_net.h"
+#include "error.h"
+
+/**
+ * The basic per-network user info. The lifetime of an irc_user struct is strictly for the duration between the user
+ * JOIN'ing onto the first irc_chan we are on (or us JOIN'ing onto it while they are on it), and them PART'ing the
+ * channel or QUIT'ing.
+ */
+struct irc_user {
+    /** The user's nickname */
+    char *nickname;
+
+    /** Our entry in the irc_net list */
+    LIST_ENTRY(irc_user) net_users;
+};
+
+/**
+ * Create a new irc_user with the given nickname. The nickname is copied for storage.
+ */
+err_t irc_user_create (struct irc_user **user_ptr, struct irc_net *net, const char *nickname);
+
+/**
+ * Destroy an irc_user, releasing memory
+ */
+void irc_user_destroy (struct irc_user *user);
+
+#endif
--- a/src/test.c	Mon Mar 16 23:55:30 2009 +0200
+++ b/src/test.c	Thu Mar 26 21:46:10 2009 +0200
@@ -220,6 +220,18 @@
     return sock_test_add_recv_str(sock, str);
 }
 
+/**
+ * Create an empty sock_test
+ */
+struct sock_test* setup_sock_test (void)
+{
+    struct sock_test *sock;
+   
+    assert ((sock = sock_test_create()) != NULL);
+
+    return sock;
+}
+
 void test_dump_str (void)
 {
     log_info("dumping example strings on stdout:");
@@ -498,15 +510,17 @@
     irc_conn_destroy(conn);
 }
 
-struct _test_net_ctx {
+struct test_chan_ctx {
+    /** The channel we're supposed to be testing */
     struct irc_chan *chan;
-
+    
+    /** on_self_join callback called */
     bool on_chan_self_join;
 };
 
 void _on_chan_self_join (struct irc_chan *chan, void *arg)
 {
-    struct _test_net_ctx *ctx = arg;
+    struct test_chan_ctx *ctx = arg;
 
     assert(chan == ctx->chan);
     
@@ -519,60 +533,195 @@
     .on_self_join       = &_on_chan_self_join,
 };
 
-void test_irc_net (void)
+/**
+ * Setup an irc_net using the given socket, and consume the register request output, but do not push the RPL_WELCOME
+ */
+struct irc_net* setup_irc_net_unregistered (struct sock_test *sock)
 {
-    struct sock_test *sock;
     struct irc_net *net;
     struct irc_net_info net_info = {
         .register_info = {
             "nick", "user", "realname"
         },
     };
+    struct error_info err;
+
+    // create the irc_net
+    net_info.raw_sock = SOCK_TEST_BASE(sock);
+    assert_success(irc_net_create(&net, &net_info, &err));
+
+    // test register output
+    assert_sock_data(sock, "NICK nick\r\nUSER user 0 * realname\r\n");
+    
+    // ok
+    return net; 
+}
+
+/**
+ * Push to RPL_WELCOME reply, and test state
+ */
+void do_irc_net_welcome (struct sock_test *sock, struct irc_net *net)
+{
+    // registration reply
+    test_sock_push(sock, "001 mynick :Blaa blaa blaa\r\n");
+    assert(net->conn->registered);
+    assert_strcmp(net->conn->nickname, "mynick");
+
+}
+
+/**
+ * Creates an irc_net and puts it into the registered state
+ */
+struct irc_net* setup_irc_net (struct sock_test *sock)
+{
+    struct irc_net *net;   
+
+    net = setup_irc_net_unregistered(sock);
+    do_irc_net_welcome(sock, net);
+    
+    // ok
+    return net;
+}
+
+/**
+ * General test for irc_net to handle startup
+ */
+void test_irc_net (void)
+{
+    struct sock_test *sock = setup_sock_test();
+    
+    // create the network
+    log_info("test irc_net_create");
+    struct irc_net *net = setup_irc_net_unregistered(sock);
+
+    // send the registration reply
+    log_info("test irc_conn_on_RPL_WELCOME");
+    do_irc_net_welcome(sock, net);
+
+    // test errors by setting EOF
+    log_info("test irc_net_error");
+    sock_test_set_recv_eof(sock);
+    assert(net->conn == NULL);
+
+    // cleanup
+    irc_net_destroy(net);
+}
+
+/**
+ * Creates an irc_chan on the given irc_net, but does not check any output (useful for testing offline add).
+ *
+ * You must pass a test_chan_ctx for use with later operations, this will be initialized for you.
+ */
+struct irc_chan* setup_irc_chan_raw (struct irc_net *net, struct test_chan_ctx *ctx)
+{
     struct irc_chan *chan;
     struct irc_chan_info chan_info = {
         .channel        = "#test",
     };
     struct error_info err;
-    struct _test_net_ctx ctx = { NULL, false };
-
-    // create the test socket
-    assert((sock = sock_test_create()));
     
-    // create the irc_net
-    net_info.raw_sock = SOCK_TEST_BASE(sock);
-    assert_success(irc_net_create(&net, &net_info, &err));
+    // initialize the given ctx
+    memset(ctx, 0, sizeof(*ctx));
 
     // add a channel
-    log_info("test offline irc_net_add_chan");
-    assert((chan = irc_net_add_chan(net, &chan_info)));
-    assert(!chan->joining && !chan->joined);
-    assert_success(irc_chan_add_callbacks(chan, &_chan_callbacks, &ctx));
-    ctx.chan = chan;
-    
-    // test register output
-    assert_sock_data(sock, "NICK nick\r\nUSER user 0 * realname\r\n");
+    assert_success(irc_net_add_chan(net, &chan, &chan_info, &err));
+    assert(!chan->joined);
+    assert_success(irc_chan_add_callbacks(chan, &_chan_callbacks, ctx));
+    ctx->chan = chan;
     
-    // registration reply
-    log_info("test irc_conn_on_RPL_WELCOME");
-    test_sock_push(sock, "001 mynick :Blaa blaa blaa\r\n");
-    assert(net->conn->registered);
-    assert_strcmp(net->conn->nickname, "mynick");
-    
+    // ok
+    return chan;
+}
+
+/**
+ * Checks that the JOIN request for a channel was sent, and sends the basic JOIN reply
+ */
+void do_irc_chan_join (struct sock_test *sock, struct test_chan_ctx *ctx)
+{
     // JOIN request
-    log_info("test irc_net_conn_registered -> irc_chan_join");
-    assert(chan->joining);
+    assert(ctx->chan->joining);
     assert_sock_data(sock, "JOIN #test\r\n");
 
     // JOIN reply
-    log_info("test irc_chan_on_JOIN");
     test_sock_push(sock, ":mynick!user@host JOIN #test\r\n");
-    assert(!chan->joining && chan->joined);
-    assert(ctx.on_chan_self_join);
+    assert(!ctx->chan->joining && ctx->chan->joined);
+    assert(ctx->on_chan_self_join);
+}
 
-    // test errors by setting EOF
-    log_info("test irc_net_error");
-    sock_test_set_recv_eof(sock);
-    assert(net->conn == NULL);
+/**
+ * Creates an irc_chan on the given irc_net, and checks up to the JOIN reply
+ */
+struct irc_chan* setup_irc_chan (struct sock_test *sock, struct irc_net *net, struct test_chan_ctx *ctx)
+{
+    setup_irc_chan_raw(net, ctx);
+    do_irc_chan_join(sock, ctx);
+
+    // ok
+    return ctx->chan;
+}
+
+/**
+ * Call irc_net_add_chan while offline, and ensure that we send the JOIN request after RPL_WELCOME, and handle the join
+ * reply OK.
+ */
+void test_irc_chan_add_offline (void)
+{
+    struct test_chan_ctx ctx;
+
+    struct sock_test *sock = setup_sock_test();
+
+    log_info("test irc_net_create");
+    struct irc_net *net = setup_irc_net_unregistered(sock);
+
+    // add an offline channel
+    log_info("test offline irc_net_add_chan");
+    struct irc_chan *chan = setup_irc_chan_raw(net, &ctx);
+    assert(!chan->joining && !chan->joined);
+
+    // send the registration reply
+    log_info("test irc_conn_on_RPL_WELCOME");
+    do_irc_net_welcome(sock, net);
+    
+    // test the join sequence
+    log_info("test irc_chan_join/irc_chan_on_JOIN");
+    do_irc_chan_join(sock, &ctx);
+    
+    // cleanup
+    irc_net_destroy(net);
+}
+
+/**
+ * Ensure that an irc_chan_user exists for the given channel/nickname, and return it
+ */
+struct irc_chan_user* check_chan_user (struct irc_chan *chan, const char *nickname)
+{
+    struct irc_chan_user *chan_user;
+
+    if ((chan_user = irc_chan_get_user(chan, nickname)) == NULL)
+        FATAL("user %s not found in channel %s", dump_str(nickname), dump_str(irc_chan_name(chan)));
+
+    log_debug("%s -> %p: user=%p, nickname=%s", nickname, chan_user, chan_user->user, chan_user->user->nickname);
+
+    assert_strcmp(chan_user->user->nickname, nickname);
+
+    return chan_user;
+}
+
+void test_irc_chan_namreply (void)
+{
+    struct test_chan_ctx ctx;
+    struct sock_test *sock = setup_sock_test();
+    struct irc_net *net = setup_irc_net(sock);
+    struct irc_chan *chan = setup_irc_chan(sock, net, &ctx);
+
+    // RPL_NAMREPLY
+    log_info("test irc_chan_on_RPL_NAMREPLY");
+    test_sock_push(sock, "353 mynick = #test :mynick userA +userB @userC\r\n");
+
+    check_chan_user(chan, "mynick");
+    check_chan_user(chan, "userA");
+    check_chan_user(chan, "userB");
+    check_chan_user(chan, "userC");
 
     // cleanup
     irc_net_destroy(net);
@@ -589,12 +738,14 @@
     void (*func) (void);
 
 } _tests[] = {
-    {   "dump_str",     &test_dump_str      },
-    {   "sock_test",    &test_sock_test     },
-    {   "line_proto",   &test_line_proto    },
-    {   "irc_conn",     &test_irc_conn      },
-    {   "irc_net",      &test_irc_net       },
-    {   NULL,           NULL                }
+    {   "dump_str",             &test_dump_str              },
+    {   "sock_test",            &test_sock_test             },
+    {   "line_proto",           &test_line_proto            },
+    {   "irc_conn",             &test_irc_conn              },
+    {   "irc_net",              &test_irc_net               },
+    {   "irc_chan_add_offline", &test_irc_chan_add_offline  },
+    {   "irc_chan_namreply",    &test_irc_chan_namreply     },
+    {   NULL,                   NULL                        }
 };
 
 int main (int argc, char **argv)