From ae1390b2bef4dc6a1a113cddaee37d745b559b1f Mon Sep 17 00:00:00 2001 From: Luke Bratch Date: Fri, 25 Nov 2022 00:03:17 +0000 Subject: Implement fake QUIT handling via a NOTICE when doing a replay. Since user QUIT messages don't contain channel names, clients that didn't know which channels a nick was in when receiving a quit message for that nick can't show a per-channel QUIT notification. Some clients just display the QUIT message in the server status window, others silently ignore it. Cater for those clients by sending a NOTICE about the QUIT rather than replaying it - assuming the nick isn't still in any of our channels. --- TODO | 6 ++-- functions.c | 32 +++++++++++++++++++++ functions.h | 4 +++ replay.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++--------- replay.h | 2 +- 5 files changed, 119 insertions(+), 19 deletions(-) diff --git a/TODO b/TODO index e10a4e9..8d254bd 100644 --- a/TODO +++ b/TODO @@ -2,16 +2,14 @@ All the TODOs sprinkled throughout the code! Configurable rotation of replay and debug logs. +Automatic rotation of replay log along with debug log? + Configurable timestamp format in logs. macOS compiler may need limits.h included in structures.h. "Starting log replay...." followed by "Unable to read replay log file!" even though replay seemed to work? -Sometimes replaymode = "lastspoke" will replay the last message you sent if you spoke last and sometimes it doesn't - change to always include your last message? -Time based issue due to "seconds ago" rather than specific timestamp? Perhaps extract timestamp and replay from there. -(Pending real world testing to be marked as fixed.) - Can memory usage be reduced further? (e.g. better channel struct management) Ability to load new certificate whilst running. diff --git a/functions.c b/functions.c index 6d85d8e..6f8d131 100644 --- a/functions.c +++ b/functions.c @@ -1446,6 +1446,38 @@ int updatenickinallchannels(char *nickuserhost, char *newnick, struct channel *c return 1; } +// Check if "nick" is in any channel or not. +// Return 1 if it is, or 0 if not. +int isnickinanychannel(struct channel *channels, int maxchannelcount, char *nick) { + debugprint(DEBUG_FULL, "isnickinanychannel(): given '%s'.\n", nick); + + // Make sure the nick has a length of at least one + if (strlen(nick) < 1) { + debugprint(DEBUG_CRIT, "isnickinanychannel(): nick has no length, returning 0!\n"); + return 0; + } + + // Go through all channels and see if nick is present + for (int i = 0; i < maxchannelcount; i++) { + // Don't bother checking this channel index if it isn't used + if (!channels[i].name[0]) { + continue; + } + // Go through all nicks in channel + for (int j = 0; j < MAXCHANNICKS; j++) { + // See if the nick is here + if (strlen(channels[i].nicks[j]) == strlen(nick) && !strcmp(channels[i].nicks[j], nick)) { + // Found it! + debugprint(DEBUG_FULL, "isnickinanychannel(): nick '%s' found in channel '%s', returning 1.\n", nick, channels[i].name); + return 1; + } + } + } + + debugprint(DEBUG_FULL, "isnickinanychannel(): nick '%s' not found in any channel '%s', returning 0.\n", nick); + return 0; +} + // 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, int maxchannelcount) { diff --git a/functions.h b/functions.h index 28b6413..5799aa6 100644 --- a/functions.h +++ b/functions.h @@ -205,6 +205,10 @@ int removenickfromallchannels(char *nickuserhost, struct channel *channels, int // Returns 1 on success or 0 on failure int updatenickinallchannels(char *nickuserhost, char *newnick, struct channel *channels, int maxchannelcount); +// Check if "nick" is in a channel or not. +// Return 1 if it is, or 0 if not. +int isnickinanychannel(struct channel *channels, int maxchannelcount, char *nick); + // 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, int maxchannelcount); diff --git a/replay.c b/replay.c index c63e859..b90431c 100644 --- a/replay.c +++ b/replay.c @@ -45,9 +45,11 @@ int gettimestamp(char *str) { } // Set 'str' to a string with the leading unixtime timestamp removed. +// Record the stripped timestamp in 'origtimestamp'. // Returns 1 on success, 0 on failure -int striptimestamp(char *str) { +int striptimestamp(char *str, int *origtimestamp) { char line[MAXCHAR]; + char origtimestr[MAXCHAR]; int count = 0; // Make sure we're starting with a digit @@ -58,10 +60,15 @@ int striptimestamp(char *str) { // Skip over each digit until we encounter a non-digit, record the position for (int i = 0; i < TIMELEN; i++) { if (isdigit(str[i])) { + // Copy timestamp into 'origtimestr' + origtimestr[i] = str[i]; count++; } } + origtimestr[count] = '\0'; + *origtimestamp = strtol(origtimestr, NULL, 10); + // Skip over the space count++; @@ -85,7 +92,8 @@ int striptimestamp(char *str) { // :foo!bar@baz PRIVMSG foo :[17:41:41] hello world // Or the same but e.g. [DD/MM/YY HH:MM:SS] if replaydates == 1. // Only inserts the formatted time for PRIVMSGs at the moment (and maybe only needs to!). -void formattime(char *str, int replaydates) { +// Saves the original timestamp in 'origtimestamp' either way. +void formattime(char *str, int replaydates, int *origtimestamp) { // Extract the timestamp for conversion into [HH:MM:SS] char timestr[TIMELEN]; sprintf(timestr, "%d", gettimestamp(str)); // Convert int time to string time @@ -99,8 +107,8 @@ void formattime(char *str, int replaydates) { strftime(timestampf, DATETIMELEN, "[%H:%M:%S]", &tm); } - // Strip the original unixtimestamp - striptimestamp(str); + // Strip the original unixtimestamp, recording the original timestamp in case it's needed + striptimestamp(str, origtimestamp); // Take note of the length int len = strlen(str); @@ -179,10 +187,12 @@ void formattime(char *str, int replaydates) { // 1. TOPIC/JOIN/PART but we're not in the channel any more // 2. TOPIC/JOIN/PART and it was from us // 3. NICK from us but not our current nick +// 4. QUIT from a nick that is not currently in any channel (in which case +// 'sourcefd' is used to fake the QUIT using a NOTICE) // Returns 1 if the replay line should go through as is // Returns 0 if the replay line should be skipped // Returns -1 on error -int sanitisereplay (char *str, struct ircdstate *ircdstate, struct channel *channels) { +int sanitisereplay (char *str, int sourcefd, int *origtimestamp, struct client *clients, struct settings *settings,struct ircdstate *ircdstate, struct channel *channels) { // Check if the replay line is a TOPIC, a JOIN, or a PART so we don't // replay those if we are not currently in the channel they are from // otherwise clients and state go a bit mad. @@ -208,6 +218,9 @@ int sanitisereplay (char *str, struct ircdstate *ircdstate, struct channel *chan strncpy(tokens[i], token, strlen(token) + 1); } + // Done with strcopy + free(strcopyPtr); + if (strncmp(tokens[1], "TOPIC", strlen("TOPIC")) == 0 || strncmp(tokens[1], "JOIN", strlen("JOIN")) == 0 || strncmp(tokens[1], "PART", strlen("PART")) == 0) { @@ -224,7 +237,6 @@ int sanitisereplay (char *str, struct ircdstate *ircdstate, struct channel *chan if (!inchannel(channels, ircdstate->maxchannelcount, tokens[2] + offset) || ((strlen(tokens[0]) == strlen(ircdstate->ircnick)) && (strncmp(tokens[0], ircdstate->ircnick, strlen(tokens[0])) == 0))) { debugprint(DEBUG_FULL, "sanitisereplay(): Not sending '%s' replay line '%s'.\n", tokens[1], str); - free(strcopyPtr); return 0; } } @@ -235,12 +247,63 @@ int sanitisereplay (char *str, struct ircdstate *ircdstate, struct channel *chan if ((strlen(tokens[0]) == strlen(ircdstate->ircnick)) && (strncmp(tokens[0], ircdstate->ircnick, strlen(tokens[0])) == 0)) { debugprint(DEBUG_FULL, "sanitisereplay(): Not sending '%s' replay line '%s'.\n", tokens[1], str); - free(strcopyPtr); return 0; } } - free(strcopyPtr); + // Fake QUIT handling. Since user QUIT messages don't contain channel names, clients that + // didn't know which channels a nick was in when receiving a quit message for that nick + // can't show a per-channel QUIT notification. Some clients just display the QUIT message + // in the server status window, others silently ignore it. Cater for those clients by + // sending a NOTICE about the QUIT rather than replaying it - assuming the nick isn't + // still in any of our channels. + if (strncmp(tokens[1], "QUIT", strlen("QUIT")) == 0) { + extractnickfromprefix(tokens[0], 1); + + // Only do any of this if the nick isn't in any channels + if (isnickinanychannel(channels, ircdstate->maxchannelcount, tokens[0])) { + return 1; + } + + debugprint(DEBUG_FULL, "sanitisereplay(): Faking QUIT message as NOTICE for '%s'.\n", tokens[0]); + + time_t rawtime = *origtimestamp; + struct tm tm; + char timestampf[DATETIMELEN]; // Formatted timestamp + + // Format time, "ddd yyyy-mm-dd hh:mm:ss zzz" + tm = *localtime(&rawtime); + if (settings->replaydates) { + strftime(timestampf, DATETIMELEN, "[%x %H:%M:%S]", &tm); + } else { + strftime(timestampf, DATETIMELEN, "[%H:%M:%S]", &tm); + } + + // Prepare a new temporary string for the original QUIT message + char *strcopyquit = strdup(str); + // Keep track of initial pointer for free()ing later + char *strcopyquitPtr = strcopyquit; + extractfinalparameter(strcopyquit); + // Remove trailing CRLF (if any) + if (strlen(strcopyquit) > 2) { + for (int i = 0; i < 2; i++) { + if (strcopyquit[strlen(strcopyquit) - 1] == '\n' || strcopyquit[strlen(strcopyquit) - 1] == '\r') { + strcopyquit[strlen(strcopyquit) - 1] = '\0'; + } + } + } + + char outgoingmsg[MAXDATASIZE]; + if (!snprintf(outgoingmsg, MAXDATASIZE, "NOTICE %s :%s %s has quit (%s).", ircdstate->ircnick, timestampf, tokens[0], strcopyquit)) { + debugprint(DEBUG_CRIT, "sanitisereplay(): error: couldn't prepare fake QUIT message as NOTICE!\n"); + } else { + sendtoclient(sourcefd, outgoingmsg, clients, settings, 0); + } + + free(strcopyquitPtr); + return 1; + } + return 1; } @@ -292,7 +355,7 @@ int replaylinestime(int seconds, char *basedir) { // if settings.replaydates == 1. // Returns 1 on success, 0 on failure, or -1 if the line should be ignored. // TODO - This is horribly inefficient since it re-reads the entire file each call, rewrite this! -int readreplayline(int seconds, int linenum, char *str, struct settings *settings, struct ircdstate *ircdstate) { +int readreplayline(int seconds, int linenum, char *str, int *origtimestamp, struct settings *settings, struct ircdstate *ircdstate) { FILE *fp; char line[MAXCHAR]; char filename[PATH_MAX]; @@ -339,7 +402,7 @@ int readreplayline(int seconds, int linenum, char *str, struct settings *setting } // ...then return it // Insert our formatted [HH:MM:SS] timestamp into the message - formattime(line, settings->replaydates); + formattime(line, settings->replaydates, origtimestamp); strncpy(str, line, strlen(line)); str[strlen(line)] = '\0'; @@ -465,7 +528,9 @@ int doreplaytime(int sourcefd, int replayseconds, struct client *clients, struct // Replay those lines! for (int i = 0; i < numlines; i++) { - int ret = readreplayline(replayseconds, i, outgoingmsg, settings, ircdstate); + int origtimestamp; +// origtimestamp[0] = '\0'; + int ret = readreplayline(replayseconds, i, outgoingmsg, &origtimestamp, settings, ircdstate); if (ret == 0) { debugprint(DEBUG_CRIT, "doreplaytime(): Error requesting replay line.\n"); return 0; @@ -474,7 +539,7 @@ int doreplaytime(int sourcefd, int replayseconds, struct client *clients, struct continue; } - if (sanitisereplay(outgoingmsg, ircdstate, channels) < 1) { + if (sanitisereplay(outgoingmsg, sourcefd, &origtimestamp, clients, settings, ircdstate, channels) < 1) { continue; } @@ -539,12 +604,13 @@ int doreplaylastspoke(int sourcefd, long linenumber, struct client *clients, str } // Insert our formatted [HH:MM:SS] timestamp into the message - formattime(line, settings->replaydates); + int origtimestamp; + formattime(line, settings->replaydates, &origtimestamp); strncpy(outgoingmsg, line, strlen(line)); outgoingmsg[strlen(line)] = '\0'; - if (sanitisereplay(outgoingmsg, ircdstate, channels) < 1) { + if (sanitisereplay(outgoingmsg, sourcefd, &origtimestamp, clients, settings, ircdstate, channels) < 1) { continue; } diff --git a/replay.h b/replay.h index dcf6771..d2d2c6d 100644 --- a/replay.h +++ b/replay.h @@ -42,7 +42,7 @@ // 'basedir' is the directory in which to find 'replay.log'. int replaylinestime(int seconds, char *basedir); -int readreplayline(int seconds, int linenum, char *str, struct settings *settings, struct ircdstate *ircdstate); +int readreplayline(int seconds, int linenum, char *str, int *origtimestamp, struct settings *settings, struct ircdstate *ircdstate); // Returns the line number in the replay log file on which 'nick' last spoke, or -1 if there is a problem. // 'basedir' is the directory in which to find 'replay.log'. -- cgit v1.2.3