# HG changeset patch # User Tero Marttila # Date 1238096770 -7200 # Node ID 43084f103c2a2c213563ea0ba703a78d0dedb1ab # Parent 0a13030f795d3a8f8ffe07cc4b3e0de4c24d7472 add irc_user module for irc_chan to track users on a channel diff -r 0a13030f795d -r 43084f103c2a TODO --- /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? diff -r 0a13030f795d -r 43084f103c2a src/CMakeLists.txt --- 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}) diff -r 0a13030f795d -r 43084f103c2a src/irc_chan.c --- 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 +#include #include 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 */ 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; diff -r 0a13030f795d -r 43084f103c2a src/irc_chan.h --- 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. diff -r 0a13030f795d -r 43084f103c2a src/irc_net.c --- 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 #include +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) * * 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) diff -r 0a13030f795d -r 43084f103c2a src/irc_net.h --- 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); diff -r 0a13030f795d -r 43084f103c2a src/irc_proto.c --- 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; +} + diff -r 0a13030f795d -r 43084f103c2a src/irc_proto.h --- 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 :Welcome to the Internet Relay Network !@ + * 001 "Welcome to the Internet Relay Network !@" + * + * The first "official" reply sent by the server after the NICK/USER registration was accepted. */ #define IRC_RPL_WELCOME "001" +/** + * 353 ( "=" | "*" | "@" ) ( [ "@" | "+" ] [ " " ... ] ) + * + * 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 "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 diff -r 0a13030f795d -r 43084f103c2a src/irc_user.c --- /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 +#include + +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); +} + diff -r 0a13030f795d -r 43084f103c2a src/irc_user.h --- /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 diff -r 0a13030f795d -r 43084f103c2a src/test.c --- 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)