add irc_chan_on_PART, irc_net_put_user and test_irc_chan_user_part
authorTero Marttila <terom@fixme.fi>
Thu, 26 Mar 2009 22:54:25 +0200
changeset 74 11ec458d1cbf
parent 73 2780a73c71f3
child 75 ff6272398d2e
add irc_chan_on_PART, irc_net_put_user and test_irc_chan_user_part
src/irc_chan.c
src/irc_chan.h
src/irc_net.c
src/irc_net.h
src/irc_user.c
src/irc_user.h
src/test.c
--- a/src/irc_chan.c	Thu Mar 26 22:03:20 2009 +0200
+++ b/src/irc_chan.c	Thu Mar 26 22:54:25 2009 +0200
@@ -27,7 +27,7 @@
     if (irc_chan_get_user(chan, nickname))
         return SUCCESS;
     
-    // lookup/create the irc_user state
+    // lookup/create the irc_user state, incrementing the refcount
     if ((err = irc_net_get_user(chan->net, &user, nickname)))
         return err;
 
@@ -48,6 +48,21 @@
 }
 
 /**
+ * Remove an irc_chan_user previously added by irc_chan_add_user().
+ */
+static void irc_chan_remove_user (struct irc_chan *chan, struct irc_chan_user *chan_user)
+{
+    // put the irc_user reference back, decrementing refcount
+    irc_net_put_user(chan->net, chan_user->user);
+
+    // remove from list
+    LIST_REMOVE(chan_user, chan_users);
+
+    // free
+    free(chan_user);
+}
+
+/**
  * :nm JOIN <channel>
  */ 
 static void irc_chan_on_JOIN (const struct irc_line *line, void *arg)
@@ -82,6 +97,49 @@
 }
 
 /**
+ * Someone, or us ourselves, left a channel
+ *
+ * :nm PART <channel> [<message>]
+ */
+static void irc_chan_on_PART (const struct irc_line *line, void *arg)
+{
+    struct irc_chan *chan = arg;
+    err_t err;
+
+    const char *msg = line->args[1];
+
+    // us?
+    if (irc_prefix_cmp_nick(line->prefix, chan->net->conn->nickname) == 0) {
+        // twiddle state
+        chan->joined = false;
+        chan->parted = true;
+
+        // invoke callback
+        IRC_CHAN_INVOKE_CALLBACK(chan, on_self_part);
+
+    } else {
+        // someone else
+        char prefix_buf[IRC_PREFIX_MAX];
+        struct irc_nm nm;
+        struct irc_chan_user *chan_user;
+        
+        // parse the nickname
+        if ((err = irc_nm_parse(&nm, prefix_buf, line->prefix)))
+            return log_warn("invalid prefix: %s", line->prefix);
+        
+        // invoke callback (source, msg)
+        IRC_CHAN_INVOKE_CALLBACK(chan, on_part, &nm, msg);
+
+        // look up the irc_chan_user
+        if ((chan_user = irc_chan_get_user(chan, nm.nickname)) == NULL)
+            return log_warn("PART'd user not on channel: %s, %s", irc_chan_name(chan), nm.nickname);
+
+        // remove them
+        irc_chan_remove_user(chan, chan_user);
+    }
+}
+
+/**
  * :nm PRIVMSG <channel> <message>
  */
 static void irc_chan_on_PRIVMSG (const struct irc_line *line, void *arg)
@@ -156,6 +214,7 @@
  */
 struct irc_cmd_handler _cmd_handlers[] = {
     {   "JOIN",             &irc_chan_on_JOIN           },
+    {   "PART",             &irc_chan_on_PART           },
     {   "PRIVMSG",          &irc_chan_on_PRIVMSG        },
     {   IRC_RPL_NAMREPLY,   &irc_chan_on_RPL_NAMREPLY   },
     {   IRC_RPL_ENDOFNAMES, &irc_chan_on_RPL_ENDOFNAMES },
@@ -197,7 +256,12 @@
 
 void irc_chan_destroy (struct irc_chan *chan)
 {
-    // XXX: free chan_users list
+    struct irc_chan_user *chan_user;
+
+    // free users
+    while ((chan_user = LIST_FIRST(&chan->users))) {
+        irc_chan_remove_user(chan, chan_user);
+    }
 
     // free command handlers
     irc_cmd_free(&chan->handlers);
--- a/src/irc_chan.h	Thu Mar 26 22:03:20 2009 +0200
+++ b/src/irc_chan.h	Thu Mar 26 22:54:25 2009 +0200
@@ -34,9 +34,15 @@
     /** Joined the channel */
     void (*on_self_join) (struct irc_chan *chan, void *arg);
 
+    /** We parted the channel */
+    void (*on_self_part) (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 parted the channel. The irc_chan_user is still present */
+    void (*on_part) (struct irc_chan *chan, const struct irc_nm *source, const char *msg, 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);
 };
@@ -91,6 +97,9 @@
     /** Currently joined to channel */
     bool joined;
 
+    /** We PART'd the channel */
+    bool parted;
+
     // @}
     
     /** List of users on channel */
--- a/src/irc_net.c	Thu Mar 26 22:03:20 2009 +0200
+++ b/src/irc_net.c	Thu Mar 26 22:54:25 2009 +0200
@@ -3,6 +3,7 @@
 
 #include <stdlib.h>
 #include <string.h>
+#include <assert.h>
 
 const char* irc_net_name (struct irc_net *net)
 {
@@ -303,25 +304,33 @@
 
 err_t irc_net_get_user (struct irc_net *net, struct irc_user **user_ptr, const char *nickname)
 {
-    struct irc_user *user = NULL;
+    struct irc_user *user_iter = NULL, *user = NULL;
     err_t err;
 
     // look for it...
-    LIST_FOREACH(user, &net->users, net_users) {
-        if (irc_cmp_nick(nickname, user->nickname) == 0) {
+    LIST_FOREACH(user_iter, &net->users, net_users) {
+        if (irc_cmp_nick(nickname, user_iter->nickname) == 0) {
             // found existing
-            *user_ptr = user;
-
-            return SUCCESS;
+            user = user_iter;
+            break;
         }
     }
     
-    // create a new user struct
-    if ((err = irc_user_create(&user, net, nickname)))
-        return err;
+    if (!user) {
+        // 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);
+        // add to our list
+        LIST_INSERT_HEAD(&net->users, user, net_users);
+
+    } else {
+        // check for refcount overflow
+        assert(user->refcount < SIZE_MAX);
+    }
+
+    // 'get' it
+    user->refcount++;
 
     // ok
     *user_ptr = user;
@@ -329,6 +338,23 @@
     return SUCCESS;
 }
 
+void irc_net_put_user (struct irc_net *net, struct irc_user *user)
+{
+    (void) net;
+
+    assert(user->refcount > 0);
+
+    // nothing if it's still in use elsewhere
+    if (--user->refcount)
+        return;
+
+    // remove from list
+    LIST_REMOVE(user, net_users);
+
+    // destroy
+    irc_user_destroy(user);
+}
+
 err_t irc_net_quit (struct irc_net *net, const char *message)
 {
    if (!net->conn)
--- a/src/irc_net.h	Thu Mar 26 22:03:20 2009 +0200
+++ b/src/irc_net.h	Thu Mar 26 22:54:25 2009 +0200
@@ -103,6 +103,9 @@
 /**
  * Look up an existing irc_user by nickname, or create a new one if not found.
  *
+ * This will increment the reference count on the irc_user object, so do call irc_net_put_user once you're done with
+ * it, or you'll get reference-counting bugs...
+ *
  * @param user_ptr set to the new irc_user state
  * @param net the network context
  * @param nickname the user's current nickname
@@ -110,6 +113,11 @@
 err_t irc_net_get_user (struct irc_net *net, struct irc_user **user_ptr, const char *nickname);
 
 /**
+ * Release a previously get'd irc_user, decrementing its refcount and destroying it if unused.
+ */
+void irc_net_put_user (struct irc_net *net, struct irc_user *user);
+
+/**
  * 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_user.c	Thu Mar 26 22:03:20 2009 +0200
+++ b/src/irc_user.c	Thu Mar 26 22:54:25 2009 +0200
@@ -1,19 +1,28 @@
 #include "irc_user.h"
+#include "log.h"
 
 #include <stdlib.h>
 #include <string.h>
+#include <assert.h>
+
+// XXX: prototype of function from irc_net
+void irc_net_remove_user (struct irc_net *net, struct irc_user *user);
 
 err_t irc_user_create (struct irc_user **user_ptr, struct irc_net *net, const char *nickname)
 {
     struct irc_user *user;
+
+    (void) net;
     
     // allocate
     if ((user = calloc(1, sizeof(*user))) == NULL)
         return ERR_CALLOC;
 
-    // copy nickname
+    // init
     if ((user->nickname = strdup(nickname)) == NULL)
         return ERR_STRDUP;
+    
+    user->refcount = 0;
 
     // ok
     *user_ptr = user;
@@ -23,6 +32,9 @@
 
 void irc_user_destroy (struct irc_user *user)
 {
+    if (user->refcount > 0)
+        log_warn("refcount=%zu", user->refcount);
+
     free(user->nickname);
     free(user);
 }
--- a/src/irc_user.h	Thu Mar 26 22:03:20 2009 +0200
+++ b/src/irc_user.h	Thu Mar 26 22:54:25 2009 +0200
@@ -20,12 +20,17 @@
     /** The user's nickname */
     char *nickname;
 
+    /** Refcount for irc_chan.users lists */
+    size_t refcount;
+
     /** 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.
+ *
+ * XXX: this is a private function for use by irc_net
  */
 err_t irc_user_create (struct irc_user **user_ptr, struct irc_net *net, const char *nickname);
 
--- a/src/test.c	Thu Mar 26 22:03:20 2009 +0200
+++ b/src/test.c	Thu Mar 26 22:54:25 2009 +0200
@@ -516,7 +516,7 @@
     struct irc_chan *chan;
     
     /** Flags for callbacks called*/
-    bool on_chan_self_join, on_chan_join;
+    bool on_chan_self_join, on_chan_self_part, on_chan_join, on_chan_part;
 
 };
 
@@ -544,9 +544,25 @@
     log_debug("on_join");
 }
 
+void _on_chan_part (struct irc_chan *chan, const struct irc_nm *source, const char *msg, void *arg)
+{
+    struct test_chan_ctx *ctx = arg;
+
+    assert(chan == ctx->chan);
+
+    // XXX: verify source
+    // XXX: verify msg
+
+    ctx->on_chan_part = true;
+
+    log_debug("on_part");
+}
+
+
 struct irc_chan_callbacks _chan_callbacks = {
     .on_self_join       = &_on_chan_self_join,
     .on_join            = &_on_chan_join,
+    .on_part            = &_on_chan_part,
 };
 
 /**
@@ -624,6 +640,28 @@
 }
 
 /**
+ * Ensure that an irc_chan_user exists/doesn't exist for the given channel/nickname, and return it
+ */
+struct irc_chan_user* check_chan_user (struct irc_chan *chan, const char *nickname, bool exists)
+{
+    struct irc_chan_user *chan_user = irc_chan_get_user(chan, nickname);
+    
+    if (exists && chan_user == NULL)
+        FATAL("user %s not found in channel %s", dump_str(nickname), dump_str(irc_chan_name(chan)));
+    
+    if (!exists && chan_user)
+        FATAL("user %s should not be on channel %s anymore", dump_str(nickname), dump_str(irc_chan_name(chan)));
+
+    log_debug("%s, exists=%d -> %p: user=%p, nickname=%s", 
+            nickname, exists, chan_user, chan_user ? chan_user->user : NULL, chan_user ? chan_user->user->nickname : NULL);
+    
+    if (chan_user)
+        assert_strcmp(chan_user->user->nickname, nickname);
+
+    return chan_user;
+}
+
+/**
  * 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.
@@ -665,9 +703,25 @@
 }
 
 /**
+ * Sends a short RPL_NAMREPLY/RPL_ENDOFNAMES reply and checks that the users list matches
+ */
+void do_irc_chan_namreply (struct sock_test *sock, struct test_chan_ctx *ctx)
+{
+    // RPL_NAMREPLY
+    test_sock_push(sock, "353 mynick = #test :mynick userA +userB @userC\r\n");
+    test_sock_push(sock, "366 mynick #test :End of NAMES\r\n");
+    
+    // XXX: this should be an exclusive test, i.e. these should be the only ones...
+    check_chan_user(ctx->chan, "mynick", true);
+    check_chan_user(ctx->chan, "userA", true);
+    check_chan_user(ctx->chan, "userB", true);
+    check_chan_user(ctx->chan, "userC", true);
+}
+
+/**
  * 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)
+struct irc_chan* setup_irc_chan_join (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);
@@ -677,6 +731,20 @@
 }
 
 /**
+ * Creates an irc_chan on the given irc_net, sends the JOIN stuff plus RPL_NAMREPLY
+ */
+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);
+    do_irc_chan_namreply(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.
  */
@@ -706,38 +774,15 @@
     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);
+    setup_irc_chan_join(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");
+    do_irc_chan_namreply(sock, &ctx);
 
     // cleanup
     irc_net_destroy(net);
@@ -750,19 +795,28 @@
     struct irc_net *net = setup_irc_net(sock);
     struct irc_chan *chan = setup_irc_chan(sock, net, &ctx);
 
-    // 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");
-
     // have a user join
     log_info("test irc_chan_on_JOIN");
     test_sock_push(sock, ":newuser!someone@somewhere JOIN #test\r\n");
     assert(ctx.on_chan_join);
-    check_chan_user(chan, "newuser");
+    check_chan_user(chan, "newuser", true);
+
+    // cleanup
+    irc_net_destroy(net);
+}
+
+void test_irc_chan_user_part (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);
+
+    // have a user join
+    log_info("test irc_chan_on_PART");
+    test_sock_push(sock, ":userA!someone@somewhere PART #test\r\n");
+    assert(ctx.on_chan_part); ctx.on_chan_part = NULL;
+    check_chan_user(chan, "userA", false);
 
     // cleanup
     irc_net_destroy(net);
@@ -787,6 +841,7 @@
     {   "irc_chan_add_offline", &test_irc_chan_add_offline  },
     {   "irc_chan_namreply",    &test_irc_chan_namreply     },
     {   "irc_chan_user_join",   &test_irc_chan_user_join    },
+    {   "irc_chan_user_part",   &test_irc_chan_user_part    },
     {   NULL,                   NULL                        }
 };