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. --- replay.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 14 deletions(-) (limited to 'replay.c') 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; } -- cgit v1.2.3