summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO4
-rw-r--r--blabouncer.c4
-rw-r--r--functions.c250
-rw-r--r--functions.h25
-rw-r--r--logging.c43
-rw-r--r--logging.h9
-rw-r--r--message.c60
-rw-r--r--structures.h2
8 files changed, 391 insertions, 6 deletions
diff --git a/TODO b/TODO
index 68e0122..32bfa2c 100644
--- a/TODO
+++ b/TODO
@@ -1,5 +1,3 @@
All the TODOs sprinkled throughout the code!
-Is there a way to log nick changes to the normal log despite not tracking nicks in each channel? (We do track channel names themselves.)
-
-Log QUIT messages in channels if the quitting user was in them - requires tracking users in channels.
+Log nick changes to the normal log.
diff --git a/blabouncer.c b/blabouncer.c
index 772b969..d6df294 100644
--- a/blabouncer.c
+++ b/blabouncer.c
@@ -438,6 +438,10 @@ void dochat(int *serversockfd, int *clientsockfd, struct settings *settings) {
// Set initial channel names to empty strings
for (int i = 0; i < MAXCHANNELS; i++) {
channels[i].name[0] = '\0';
+ // And all the nicks within it
+ for (int j = 0; j < MAXCHANNICKS; j++) {
+ channels[i].nicks[j][0] = '\0';
+ }
}
// Initialise OpenSSL (used for both client and server)
diff --git a/functions.c b/functions.c
index fe96e0a..3dd8045 100644
--- a/functions.c
+++ b/functions.c
@@ -179,6 +179,7 @@ void appendcrlf(char *string) {
}
// Remove leading colon ':' which is the starting character of a prefix in an IRC message
+// If no leading colon present, string is left unchanged
void stripprefix(char *string) {
// Make a copy to work with
char string2[strlen(string)];
@@ -643,8 +644,11 @@ int createchannel(struct channel *channels, char *name, char *topic, char *topic
strncpy(channels[i].topicwhen, topicwhen, strlen(topicwhen));
channels[i].topicwhen[strlen(topicwhen)] = '\0';
channels[i].gotnames = 0;
+ // Set nicks to blank
+ for (int j = 0; j < MAXCHANNICKS; j++) {
+ channels[i].nicks[j][0] = '\0';
+ }
return 1;
- break; // TODO - This should be safe to remove since return is hit first
}
}
@@ -714,6 +718,10 @@ int removechannel(struct channel *channels, char *name) {
if (strncmp(channels[i].name, name, strlen(name)) == 0) {
// ..and NULL its name (0th character = '\0')
channels[i].name[0] = '\0';
+ // Set nicks to blank
+ for (int j = 0; j < MAXCHANNICKS; j++) {
+ channels[i].nicks[j][0] = '\0';
+ }
debugprint(DEBUG_FULL, "removechannel(): channel '%s' removed and topicwhen set to '%s'.\n", name, channels[i].topicwhen);
return 1;
}
@@ -1319,3 +1327,243 @@ void replacechar(char *str, char find, char replace) {
}
}
}
+
+// Add nick (passed as a :nick!user@host) to channel 'channel'
+// Returns 1 on success or 0 on failure
+int addnicktochannel(char *nickuserhost, char *channel, struct channel *channels) {
+ debugprint(DEBUG_FULL, "addnicktochannel(): given '%s' and '%s'.\n", nickuserhost, channel);
+
+ // Get the nick from the prefix
+ extractnickfromprefix(nickuserhost);
+
+ // Make sure the channel exists
+ int chanfound = 0;
+ int chanindex;
+ for (chanindex = 0; chanindex < MAXCHANNELS; chanindex++) {
+ if (strlen(channels[chanindex].name) == strlen(channel) && !strcmp(channels[chanindex].name, channel)) {
+ chanfound = 1;
+ break;
+ }
+ }
+ if (!chanfound) {
+ debugprint(DEBUG_CRIT, "addnicktochannel(): channel '%s' not found in channel struct.\n", channel);
+ return 0;
+ }
+
+ // Add the nick to the channel
+ for (int i = 0; i < MAXCHANNICKS; i++) {
+ // Make sure the nick isn't already in the channel struct
+ if (strlen(channels[chanindex].nicks[i]) == strlen(nickuserhost) && !strcmp(channels[chanindex].nicks[i], nickuserhost)) {
+ // Unexectedly the nick is already here, hopefully it's OK so let's return 1
+ debugprint(DEBUG_FULL, "addnicktochannel(): nick '%s' already in channel '%s', returning.\n", nickuserhost, channel);
+ return 1;
+ }
+
+ // Find the first unoccupied slot and put the nick in
+ if (!channels[chanindex].nicks[i][0]) {
+ strcpy(channels[chanindex].nicks[i], nickuserhost);
+ debugprint(DEBUG_FULL, "addnicktochannel(): added nick '%s' to channel '%s'.\n", nickuserhost, channel);
+ return 1;
+ }
+ }
+
+ // We shouldn't get here, return error
+ debugprint(DEBUG_CRIT, "addnicktochannel(): got to the end of the function without adding nick '%s' to channel '%s', returning error.\n",
+ nickuserhost, channel);
+ return 0;
+}
+
+// Remove nick (passed as a :nick!user@host) from channel 'channel'
+// Returns 1 on success or 0 on failure
+int removenickfromchannel(char *nickuserhost, char *channel, struct channel *channels) {
+ debugprint(DEBUG_FULL, "removenickfromchannel(): given '%s' and '%s'.\n", nickuserhost, channel);
+
+ // Get the username from the prefix
+ extractnickfromprefix(nickuserhost);
+
+ // Make sure the channel exists
+ int chanfound = 0;
+ int chanindex;
+ for (chanindex = 0; chanindex < MAXCHANNELS; chanindex++) {
+ if (strlen(channels[chanindex].name) == strlen(channel) && !strcmp(channels[chanindex].name, channel)) {
+ chanfound = 1;
+ break;
+ }
+ }
+ if (!chanfound) {
+ debugprint(DEBUG_CRIT, "removenickfromchannel(): channel '%s' not found in channel struct.\n", channel);
+ return 0;
+ }
+
+ // Remove the nick from the channel
+ for (int i = 0; i < MAXCHANNICKS; i++) {
+ // Remove the the nick
+ if (strlen(channels[chanindex].nicks[i]) == strlen(nickuserhost) && !strcmp(channels[chanindex].nicks[i], nickuserhost)) {
+ // By null terminating its string
+ debugprint(DEBUG_FULL, "removenickfromchannel(): nick '%s' removed from channel '%s'.\n", nickuserhost, channel);
+ channels[chanindex].nicks[i][0] = '\0';
+ return 1;
+ }
+ }
+
+ // We shouldn't get here, return error
+ debugprint(DEBUG_CRIT, "removenickfromchannel(): got to the end of the function without removing nick '%s' from channel '%s', returning error.\n",
+ nickuserhost, channel);
+ return 0;
+}
+
+// Remove nick (passed as a :nick!user@host) from all channels
+// Returns 1 on success or 0 on failure
+int removenickfromallchannels(char *nickuserhost, struct channel *channels) {
+ debugprint(DEBUG_FULL, "removenickfromallchannels(): given '%s'.\n", nickuserhost);
+
+ // Get the nick from the prefix
+ extractnickfromprefix(nickuserhost);
+
+ // Go through all channels and remove nick if present
+ for (int i = 0; i < MAXCHANNELS; i++) {
+ // Go through all nicks in channel
+ for (int j = 0; j < MAXCHANNICKS; j++) {
+ // Remove the nick from the channel if present
+ if (strlen(channels[i].nicks[j]) == strlen(nickuserhost) && !strcmp(channels[i].nicks[j], nickuserhost)) {
+ // By null terminating its string
+ channels[i].nicks[j][0] = '\0';
+ debugprint(DEBUG_FULL, "removenickfromallchannels(): nick '%s' removed from channel '%s'.\n", nickuserhost, channels[i].name);
+ }
+ }
+ }
+
+ return 1;
+}
+
+// Update old nick (passed as a :nick!user@host) to 'newnick' in all channels
+// Returns 1 on success or 0 on failure
+int updatenickinallchannels(char *nickuserhost, char *newnick, struct channel *channels) {
+ debugprint(DEBUG_FULL, "updatenickinallchannels(): given '%s' and '%s'.\n", nickuserhost, newnick);
+
+ // Get the nick from the prefix
+ extractnickfromprefix(nickuserhost);
+
+ // Strip prefix from newnick
+ stripprefix(newnick);
+
+ // Go through all channels and update nick if present
+ for (int i = 0; i < MAXCHANNELS; i++) {
+ // Go through all nicks in channel
+ for (int j = 0; j < MAXCHANNICKS; j++) {
+ // Update the nick in the channel if present
+ if (strlen(channels[i].nicks[j]) == strlen(nickuserhost) && !strcmp(channels[i].nicks[j], nickuserhost)) {
+ strcpy(channels[i].nicks[j], newnick);
+ debugprint(DEBUG_FULL, "updatenickinallchannels(): nick '%s' updated to '%s' in channel '%s'.\n", nickuserhost, newnick, channels[i].name);
+ }
+ }
+ }
+
+ return 1;
+}
+
+// Populate our channels struct with all nicks in a RPL_NAMREPLY
+// Returns 1 on success or 0 on failure
+int addnamereplytochannel(char *namereply, struct channel *channels) {
+//:irc.tghost.co.uk 353 blabounce = #blabouncer :blabounce bbnick ~@l_bratch @l_blabnc Hughbla Bratchbot ars
+ debugprint(DEBUG_FULL, "addnamereplytochannel(): given '%s'.\n", namereply);
+
+ // Make a copy since we don't need to modify the original
+ char strcopy[MAXDATASIZE];
+ strcpy(strcopy, namereply);
+
+ // Strip the leading ':'
+ stripprefix(strcopy);
+
+ // Find the start of the channel name, which comes after the first '=' followed by a space
+ int channelpos = -1;
+ for (size_t i = 0; i < strlen(strcopy) - 2; i++) {
+ if (strcopy[i] == '=' && strcopy[i + 1] == ' ' && strcopy[i + 2] != '\0') {
+ // Name found
+ channelpos = i + 2;
+ break;
+ }
+ }
+ if (channelpos == -1) {
+ // Didn't find the name, abort
+ debugprint(DEBUG_FULL, "addnamereplytochannel(): couldn't find start of channel name in '%s'.\n", namereply);
+ return 0;
+ }
+
+ // Find the end of the channel name
+ char channelname[MAXCHANLENGTH];
+ for (size_t i = channelpos; i < strlen(strcopy); i++) {
+ // Stop when a space is found or if we're going to exceed MAXCHANLENGTH
+ if (strcopy[i] == ' ' || i - channelpos == MAXCHANLENGTH - 2) {
+ break;
+ }
+ channelname[i - channelpos] = strcopy[i];
+ channelname[i - channelpos + 1] = '\0';
+ }
+
+ // Start with a nice clean string that just consists of nicks at the end of the string
+ char nickstr[MAXDATASIZE];
+ strcpy(nickstr, strcopy + channelpos + strlen(channelname) + 1);
+
+ // Split nickstr up into its space-separated nick components
+
+ // Copy to a temporary string for feeding to strsep
+ char *nickcopy = strdup(nickstr);
+ // Keep track of initial pointer for free()ing later
+ char *nickcopyPtr = nickcopy;
+
+ // Track which CLRF-separated nick we're on
+ int nickcount = 0;
+ // Build array of each space-separated token
+ char nicks[MAXTOKENS][MAXDATASIZE];
+ // Split the string by ' ' and add each space-separated nick to an array
+ char *token;
+ while ((token = strsep(&nickcopy, " ")) != NULL) {
+ if (*token == '\0') continue; // Skip consecutive matches
+ if (nickcount >= MAXTOKENS) break; // Too many tokens
+ debugprint(DEBUG_FULL, "addnamereplytochannel(): Token: '%s', length '%ld'.\n", token, strlen(token));
+ // Make sure it's not too long
+ if (strlen(token) > MAXNICKLENGTH - 1) {
+ debugprint(DEBUG_CRIT, "addnamereplytochannel(): nick too long, discarding.\n");
+ continue;
+ }
+ // Copy into the token array (strlen + 1 to get the NULL terminator)
+ strncpy(nicks[nickcount], token, strlen(token) + 1);
+ nickcount++;
+ }
+
+ free(nickcopyPtr);
+
+ // Clean up each nick (remove prefixes and such)
+ for (int i = 0; i < nickcount; i++) {
+ stripprefixesfromnick(nicks[i]);
+ // And add to the channel
+ addnicktochannel(nicks[i], channelname, channels);
+ }
+
+ return 1;
+}
+
+// Strips all leading prefixes (colons, user modes) from a nick
+void stripprefixesfromnick(char *nick) {
+ debugprint(DEBUG_FULL, "stripprefixesfromnick(): given '%s'.\n", nick);
+
+ char nicktmp[MAXNICKLENGTH];
+ int pos = 0;
+
+ for (size_t i = 0; i < strlen(nick); i++) {
+ // Only copy non-prefix chars
+ if (nick[i] != ':' && nick[i] != '~' && nick[i] != '&' && nick[i] != '@' && nick[i] != '%' && nick[i] != '+') {
+ nicktmp[pos] = nick[i];
+ pos++;
+ }
+ }
+
+ // Null terminate
+ nicktmp[pos] = '\0';
+
+ debugprint(DEBUG_FULL, "stripprefixesfromnick(): produced '%s'.\n", nicktmp);
+
+ // Copy back to source string
+ strcpy(nick, nicktmp);
+}
diff --git a/functions.h b/functions.h
index 5afdca0..614137d 100644
--- a/functions.h
+++ b/functions.h
@@ -53,6 +53,8 @@
#define MAXCHANNELS 1024 // Let's assume 1024 is reasonable for now (it's configured per IRCd)
#define MAXRFCNICKLEN 9 // From RFC 1459
+#define MAXTOKENS 100 // For strsep string splitting
+
#define VERSION "0.1.1" // Blabouncer version
// Write debug string to file.
@@ -67,6 +69,7 @@ int getstdin(char *prompt, char *buff, size_t sz);
void appendcrlf(char *string);
// Remove leading colon ':' which is the starting character of a prefix in an IRC message
+// If no leading colon present, string is left unchanged
void stripprefix(char *string);
// Extract final parameter from IRC message, removing the leading colon ':'
@@ -183,4 +186,26 @@ int getclientcodetime(char *code, struct clientcodes *clientcodes);
// Replace any instances of "find" with "replace" in the string "str"
void replacechar(char *str, char find, char replace);
+// Add nick (passed as a :nick!user@host) to channel 'channel'
+// Returns 1 on success or 0 on failure
+int addnicktochannel(char *nickuserhost, char *channel, struct channel *channels);
+
+// Remove nick(passed as a :nick!user@host) from channel 'channel'
+// Returns 1 on success or 0 on failure
+int removenickfromchannel(char *nickuserhost, char *channel, struct channel *channels);
+
+// Remove nick (passed as a :nick!user@host) from all channels
+// Returns 1 on success or 0 on failure
+int removenickfromallchannels(char *nickuserhost, struct channel *channels);
+
+// Update old nick (passed as a :nick!user@host) to 'newnick' in all channels
+// Returns 1 on success or 0 on failure
+int updatenickinallchannels(char *nickuserhost, char *newnick, struct channel *channels);
+
+// Populate our channels struct with all nicks in a RPL_NAMREPLY
+// Returns 1 on success or 0 on failure
+int addnamereplytochannel(char *namereply, struct channel *channels);
+
+// Strips all leading prefixes (colons, user modes) from a nick
+void stripprefixesfromnick(char *nick);
#endif
diff --git a/logging.c b/logging.c
index ff41911..f6fa23e 100644
--- a/logging.c
+++ b/logging.c
@@ -40,6 +40,14 @@
// If LOG_NETWORK then it just logs the string verbatim in
// a file named 'ircdstate->ircdname'.log.
//
+// If LOG_QUIT then it expects a string in the format:
+// channelname :nick!bar@baz QUIT :bla bla bla
+// 'channelname' probably has to be prepended manually by the
+// caller since it doesn't feature in the raw message from
+// the IRCd. We need it in logline() to log to the relevant
+// channel log file. The caller probably has to call logline()
+// multiple times for each channel the nick was in.
+//
// With the ":foo!bar@baz "prefix being important for all
// types.
//
@@ -224,6 +232,37 @@ int logline(char *str, struct ircdstate *ircdstate, char *basedir, int type) {
break;
+ case LOG_QUIT:
+ // Build a friendly message (e.g. "#channel :nick!user@host QUIT :foo bar baz" -> "nick (user@host) has quit (foo bar baz)")
+
+ // Find the bang in the prefix
+ // (ret and posbang defined above in case LOG_JOINPART)
+ if ((ret = strstr(tokens[1], "!")) != NULL) {
+ // Position within str of "!"
+ posbang = ret - tokens[1];
+ } else {
+ // No idea what happened, let's abandon ship
+ debugprint(DEBUG_CRIT, "logline(): Unable to find '!' within nick!user@host, returning!\n");
+ return 0;
+ }
+
+ // Make it a null character
+ tokens[1][posbang] = '\0';
+
+ // Strip the prefix from the quit message
+ stripprefix(str);
+
+ // Build a friendly message (e.g. "nick (user@host) has quit (Quit: message)")
+ snprintf(line, MAXCHAR, "%s (%s) has quit (%s)", tokens[1] + 1, tokens[1] + posbang + 1, str);
+
+ // Build the log filename
+ if (!snprintf(filename, MAXCHAR, "%s/logs/%s.log", basedir, tokens[0])) {
+ debugprint(DEBUG_CRIT, "logline(): Error while preparing log filename for quit, returning!\n");
+ return 0;
+ }
+
+ break;
+
default :
debugprint(DEBUG_CRIT, "logline(): Unknown log type '%d', returning 0.\n", type);
return 0;
@@ -284,7 +323,7 @@ int logline(char *str, struct ircdstate *ircdstate, char *basedir, int type) {
return 0;
}
}
- } else if (type == LOG_JOINPART || type == LOG_TOPIC || type == LOG_NETWORK) {
+ } else if (type == LOG_JOINPART || type == LOG_TOPIC || type == LOG_NETWORK || type == LOG_QUIT) {
// Prepend the time string
char line2[MAXCHAR];
if (!snprintf(line2, MAXCHAR, "%s %s", timestr, line)) {
@@ -300,7 +339,7 @@ int logline(char *str, struct ircdstate *ircdstate, char *basedir, int type) {
// Ensure the line finishes with CRLF
appendcrlf(line);
- debugprint(DEBUG_FULL, "logline(): Complete log string to write: '%s', length '%ld'.\n", line, strlen(line));
+ debugprint(DEBUG_FULL, "logline(): Complete log string to write: '%s' to '%s', length '%ld'.\n", line, filename, strlen(line));
// Write complete line to file
if ((bytes = fprintf(fp, "%s", line)) < 0) {
diff --git a/logging.h b/logging.h
index 6371f8a..726a490 100644
--- a/logging.h
+++ b/logging.h
@@ -37,6 +37,7 @@
#define LOG_JOINPART 1
#define LOG_TOPIC 2
#define LOG_NETWORK 3
+#define LOG_QUIT 4
#define DEBUG_CRIT 0
#define DEBUG_SOME 1
#define DEBUG_FULL 2
@@ -64,6 +65,14 @@
// If LOG_NETWORK then it just logs the string verbatim in
// a file named 'ircdstate->ircdname'.log.
//
+// If LOG_QUIT then it expects a string in the format:
+// channelname :nick!bar@baz QUIT :bla bla bla
+// 'channelname' probably has to be prepended manually by the
+// caller since it doesn't feature in the raw message from
+// the IRCd. We need it in logline() to log to the relevant
+// channel log file. The caller probably has to call logline()
+// multiple times for each channel the nick was in.
+//
// With the ":foo!bar@baz "prefix being important for all
// types.
//
diff --git a/message.c b/message.c
index ed27173..869a1ce 100644
--- a/message.c
+++ b/message.c
@@ -197,6 +197,11 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int
debugprint(DEBUG_FULL, "Server JOIN: nick is NOT ours ('%s' vs '%s').\n", prefixcopy, ircdstate->ircnick);
}
+ // Add the JOINing nick to our local channel struct
+ if (!addnicktochannel(tokens[0], tokens[2], channels)) {
+ debugprint(DEBUG_CRIT, "Failed to add nick to channel struct.\n");
+ }
+
// And then send to all clients
sendtoallclients(clients, str, sourcefd, settings);
@@ -231,6 +236,11 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int
debugprint(DEBUG_FULL, "Server PART: nick is NOT ours ('%s' vs '%s').\n", prefixcopy, ircdstate->ircnick);
}
+ // Remove the PARTing nick from our local channel struct
+ if (!removenickfromchannel(tokens[0], tokens[2], channels)) {
+ debugprint(DEBUG_CRIT, "Failed to remove nick from channel struct.\n");
+ }
+
// And then send to all clients
sendtoallclients(clients, str, sourcefd, settings);
@@ -248,6 +258,45 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int
return 1;
}
+ // Server QUIT received? Tell all clients and also remove the user from our local channels struct.
+ if (strncmp(tokens[1], "QUIT", strlen(tokens[1])) == 0) {
+ debugprint(DEBUG_FULL, "Server QUIT found and it is: %s with length %zd! Next token is '%s'.\n", tokens[0], strlen(tokens[0]), tokens[2]);
+
+ // And then send to all clients
+ sendtoallclients(clients, str, sourcefd, settings);
+
+ // Write to replay log if replay logging enabled
+ if (settings->replaylogging) {
+ writereplayline(str, settings->basedir);
+ }
+
+ // Get each channel the QUITting user was in, and log the quit from that channel
+ if (settings->logging) {
+ char quitnick[MAXNICKLENGTH];
+ strcpy(quitnick, tokens[0]);
+ extractnickfromprefix(quitnick);
+ for (int i = 0; i < MAXCHANNELS; i++) {
+ if (channels[i].name[0]) {
+ for (int j = 0; j < MAXCHANNICKS; j++) {
+ if (strlen(channels[i].nicks[j]) == strlen(quitnick) && !strcmp(channels[i].nicks[j], quitnick)) {
+ char logstring[MAXDATASIZE];
+ snprintf(logstring, MAXDATASIZE, "%s %s", channels[i].name, str);
+ logline(logstring, ircdstate, settings->basedir, LOG_QUIT);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Remove the QUITting nick from our local channel struct
+ if (!removenickfromallchannels(tokens[0], channels)) {
+ debugprint(DEBUG_CRIT, "Failed to remove nick from channel structs.\n");
+ }
+
+ return 1;
+ }
+
// Channel topics/names/nicks/etc.
// Server 331 (RPL_NOTOPIC) the topic is blank which we track by having a set timestamp of 0
if (strncmp(tokens[1], "331", strlen(tokens[1])) == 0) {
@@ -288,6 +337,12 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int
}
}
}
+
+ // Update our local channels struct with all nicks from this RPL_NAMREPLY
+ if (!addnamereplytochannel(str, channels)) {
+ debugprint(DEBUG_CRIT, "Failed to add RPL_NAMREPLY to channels.\n");
+ }
+
return 1;
// Server 366 (RPL_ENDOFNAMES), relay to all clients if we've just JOINed the channel, or relay to
// and decrement from any clients who were waiting on RPL_NAMREPLY if it's an existing channel.
@@ -420,6 +475,11 @@ int processservermessage(SSL *server_ssl, char *str, struct client *clients, int
free(prefixcopy);
}
+ // Update old nick to the new nick in our local channel struct
+ if (!updatenickinallchannels(tokens[0], tokens[2], channels)) {
+ debugprint(DEBUG_CRIT, "Failed to update old nick to new nick in channels.\n");
+ }
+
// Relay to all clients
sendtoallclients(clients, str, sourcefd, settings);
diff --git a/structures.h b/structures.h
index 8d68083..02b13b8 100644
--- a/structures.h
+++ b/structures.h
@@ -30,6 +30,7 @@
#define CLIENTCODELEN 17 // Max length of a client code + 1 for null
#define MAXCLIENTCODES 64 // Max number of client codes to track
#define MAXCONFARR 10 // Max number of entries that a configuration array can have
+#define MAXCHANNICKS 8192 // Maximum number of nicks to track per channel
struct ircdstate {
char greeting001[MAXDATASIZE];
@@ -118,6 +119,7 @@ struct channel {
// TODO - Make this an int? It's just going to arrive and leave as a string every time anyway...
char topicwhen[11]; // 32-bit unixtime is up to 10 characters (+1 for null char) We use "0" to mean "not set".
int gotnames; // Have we finished getting the RPL_NAMREPLYs for this channel yet?
+ char nicks[MAXCHANNICKS][MAXNICKLENGTH]; // Nicks in the channel to track things like nick changes and quits for log files
};
#endif