From c57e382f9c0918f993032483128d1c6617626b7e Mon Sep 17 00:00:00 2001 From: Geo Date: Sat, 12 Sep 2026 18:40:41 -0400 Subject: [PATCH 01/16] Add basic batch handling/tracking --- src/mod/server.mod/batch.c | 283 +++++++++++++++++++++++++++++++++++ src/mod/server.mod/server.c | 6 +- src/mod/server.mod/server.h | 23 +++ src/mod/server.mod/servmsg.c | 17 +++ 4 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/mod/server.mod/batch.c diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c new file mode 100644 index 000000000..bb142c65f --- /dev/null +++ b/src/mod/server.mod/batch.c @@ -0,0 +1,283 @@ +/* + * batch.c -- part of server.mod + * support for the IRCv3 batch capability + * + * https://ircv3.net/specs/extensions/batch + */ +/* + * Copyright (C) 2026 Eggheads Development Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +static int batchcount = 0; +static batch_t *batchlist = NULL; /* List of batches the server has opened but not yet closed. */ +static batch_t *current_batch = NULL; /* The batch that the line currently being dispatched belongs */ + /* to, or NULL if that line carried no batch tag. */ + +static void batch_end(batch_t *b); + +static int batch_valid_reftag(const char *reftag) +{ + if (!reftag || !*reftag) + return 0; + if (strlen(reftag) > BATCHREFMAX) + return 0; + /* Valid if the run of allowed characters extends to the terminator. */ + return reftag[strspn(reftag, BATCHREFCHARS)] == '\0'; +} + +/* Look up an open batch by reference tag. Comparison is case-sensitive */ +static batch_t *batch_find(const char *reftag) +{ + batch_t *b; + + if (!reftag || !*reftag) + return NULL; + for (b = batchlist; b; b = b->next) { + if (!strcmp(reftag, b->reftag)) + return b; + } + return NULL; +} + +/* Exported so handlers that take no tag arg can still see the batch context */ +/* TODO: Is this right? */ +static batch_t *batch_get_current(void) +{ + return current_batch; +} + +/* Free every open batch, good for disconnections/capability removal */ +static void batch_free_all(void) +{ + batch_t *b, *next; + + for (b = batchlist; b; b = next) { + next = b->next; + putlog(LOG_DEBUG, "*", "BATCH: discarding unterminated batch %s (type %s)", + b->reftag, b->type); + nfree(b); + } + batchlist = NULL; + current_batch = NULL; + batchcount = 0; +} + +/* Unlink and free a single batch record, without touching its children */ +static void batch_free_one(batch_t *b) +{ + batch_t **prev; + + if (current_batch == b) + current_batch = NULL; + for (prev = &batchlist; *prev; prev = &(*prev)->next) { + if (*prev == b) { + *prev = b->next; + batchcount--; + nfree(b); + return; + } + } +} + +/* Is maybe an ancestor of b, or b itself? */ +static int batch_is_ancestor(const batch_t *maybe, const batch_t *b) +{ + for (; b; b = b->parent) { + if (b == maybe) + return 1; + } + return 0; +} + +/* The batch open the longest, ignoring protect and everything it is nested + * inside. Evicting a batch cascades to its children, so an ancestor of the + * batch we are about to open must be excluded, or the new record would be + * left with a parent pointer into freed memory. + */ +static batch_t *batch_oldest(const batch_t *protect) +{ + batch_t *b, *oldest = NULL; + + for (b = batchlist; b; b = b->next) { + if (batch_is_ancestor(b, protect)) + continue; + if (!oldest || b->started < oldest->started) + oldest = b; + } + return oldest; +} + +/* Open a batch. parent is the batch that the BATCH + line itself was tagged + * with, or NULL for a top-level batch. + */ +static batch_t *batch_start(const char *reftag, const char *type, + const char *args, batch_t *parent) +{ + batch_t *b; + + /* Drop the stalest batch rather than refusing the new one, so a server that + * leaks batches costs us one old record instead of the ability to track any + * new ones until we disconnect. + */ + while (batchcount >= BATCHMAX) { + batch_t *old = batch_oldest(parent); + + if (!old) { + putlog(LOG_DEBUG, "*", "BATCH: refusing to open %s, the %d open batch " + "limit is entirely nesting above it", reftag, BATCHMAX); + return NULL; + } + putlog(LOG_DEBUG, "*", "BATCH: at the %d open batch limit, discarding " + "oldest batch %s (type %s) to make room for %s", BATCHMAX, + old->reftag, old->type, reftag); + batch_end(old); + } + b = nmalloc(sizeof *b); + memset(b, 0, sizeof *b); + strlcpy(b->reftag, reftag, sizeof b->reftag); + strlcpy(b->type, type, sizeof b->type); + if (args) + strlcpy(b->args, args, sizeof b->args); + b->parent = parent; + b->started = now; + b->next = batchlist; + batchlist = b; + batchcount++; + return b; +} + +/* Close a batch and free it, along with anything nested inside it. + * + * A nested batch cannot outlive its parent: its end line would have to carry + * a tag naming the parent, and the spec forbids referring to a batch after it + * has ended, so once the parent closes no legal line can ever close the + * child. Those records are unreachable, so they go now rather than sitting in + * the list until we disconnect. + * + * The scan restarts after each removal because freeing a child also frees its + * own descendants, which can invalidate a saved next pointer. + */ +static void batch_end(batch_t *b) +{ + batch_t *cur; + int found; + + do { + found = 0; + for (cur = batchlist; cur; cur = cur->next) { + if (cur->parent == b) { + putlog(LOG_DEBUG, "*", "BATCH: discarding nested batch %s, its parent " + "%s closed first", cur->reftag, b->reftag); + batch_end(cur); + found = 1; + break; + } + } + } while (found); + batch_free_one(b); +} + +/* Resolve the batch tag on an incoming line to an open batch record. Returns + * NULL if the line carried no batch tag, or if it named a batch we have no + * record of. + */ +static batch_t *batch_from_tagdict(Tcl_Obj *tagdict) +{ + Tcl_Obj *key, *value = NULL; + batch_t *b; + char *reftag; + + if (!tagdict) + return NULL; + key = Tcl_NewStringObj("batch", -1); + Tcl_IncrRefCount(key); + if ((Tcl_DictObjGet(interp, tagdict, key, &value) != TCL_OK) || !value) { + Tcl_DecrRefCount(key); + return NULL; + } + Tcl_DecrRefCount(key); + reftag = Tcl_GetString(value); + if (!*reftag) + return NULL; + b = batch_find(reftag); + if (!b) + putlog(LOG_DEBUG, "*", "BATCH: received a line tagged for batch %s, but I " + "have no record of that batch", reftag); + return b; +} + +/* Got BATCH + * :server BATCH + [ ...] + * :server BATCH - + */ +static int gotbatch(char *from, char *msg) +{ + char *reftag, *type; + char prefix; + batch_t *b; + + // Check for malformed states. Probably need to add more handling later + reftag = newsplit(&msg); + if (!*reftag) { + putlog(LOG_DEBUG, "*", "BATCH: %s sent a BATCH with no reference tag", from); + return 0; + } + prefix = *reftag++; + if (!batch_valid_reftag(reftag)) { + putlog(LOG_DEBUG, "*", "BATCH: %s sent an invalid reference tag, ignoring", + from); + return 0; + } + if (prefix == '+') { + if (batch_find(reftag)) { + putlog(LOG_DEBUG, "*", "BATCH: %s tried to open batch %s, which is " + "already open", from, reftag); + return 0; + } + type = newsplit(&msg); + if (!*type) { + putlog(LOG_DEBUG, "*", "BATCH: %s opened batch %s with no type", from, + reftag); + return 0; + } + b = batch_start(reftag, type, msg, current_batch); + if (b) + putlog(LOG_DEBUG, "*", "BATCH: opened %s (type %s)%s%s", b->reftag, + b->type, b->parent ? ", nested in " : "", + b->parent ? b->parent->reftag : ""); + } else if (prefix == '-') { + b = batch_find(reftag); + if (!b) { + putlog(LOG_DEBUG, "*", "BATCH: %s closed batch %s, but I have no record " + "of it being opened", from, reftag); + return 0; + } + + /* The spec requires the start and end lines of a batch to refer to the + * same parent batch. Mismatches are logged but not fatal. + */ + if (current_batch != b->parent) + putlog(LOG_DEBUG, "*", "BATCH: %s closed batch %s from a different batch " + "context than it was opened in", from, reftag); + putlog(LOG_DEBUG, "*", "BATCH: closed %s (type %s)", b->reftag, b->type); + batch_end(b); + } else { + putlog(LOG_DEBUG, "*", "BATCH: %s sent a BATCH with an unrecognized prefix " + "'%c'", from, prefix); + } + return 0; +} diff --git a/src/mod/server.mod/server.c b/src/mod/server.mod/server.c index 264289c71..0ec991d1a 100644 --- a/src/mod/server.mod/server.c +++ b/src/mod/server.mod/server.c @@ -131,9 +131,11 @@ static void free_server(struct server_list *); static int away_notify = 0; static int invite_notify = 0; static int message_tags = 0; +static int batch = 0; static char cap_request[CAPMAX - 9]; +#include "batch.c" #include "isupport.c" #include "tclisupport.c" #include "servmsg.c" @@ -1768,6 +1770,7 @@ static tcl_ints my_tcl_ints[] = { {"away-notify", &away_notify, 0}, {"invite-notify", &invite_notify, 0}, {"message-tags", &message_tags, 0}, + {"batch", &batch, 0}, {"extended-join", &extended_join, 0}, {"account-notify", &account_notify, 0}, {"account-tag", &account_tag, 0}, @@ -2364,7 +2367,8 @@ static Function server_table[] = { (Function) encode_msgtags, /* 52 - 55 */ (Function) & H_monitor, - (Function) isupport_get_prefixchars + (Function) isupport_get_prefixchars, + (Function) batch_get_current }; char *server_start(Function *global_funcs) diff --git a/src/mod/server.mod/server.h b/src/mod/server.mod/server.h index 909fe2116..4a5c470b6 100644 --- a/src/mod/server.mod/server.h +++ b/src/mod/server.mod/server.h @@ -31,6 +31,13 @@ #define RECVLINEMAX TOTALTAGMAX + MSGMAX #define NEWSERVERMAX 256 #define NEWSERVERPASSMAX 128 +#define BATCHREFCHARS "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-" +#define BATCHREFMAX 64 /* Max length of a batch reference tag */ +#define BATCHTYPEMAX 128 /* Max length of a batch type */ +#define BATCHARGSMAX 512 /* Max length of stored batch arguments */ +#define BATCHMAX 32 /* Max concurrently open batches */ + + #define check_tcl_ctcp(a,b,c,d,e,f) check_tcl_ctcpr(a,b,c,d,e,f,H_ctcp) #define check_tcl_ctcr(a,b,c,d,e,f) check_tcl_ctcpr(a,b,c,d,e,f,H_ctcr) @@ -103,6 +110,7 @@ /* 52 - 55 */ #define H_monitor (*(p_tcl_bind_list *)(server_funcs[52])) #define isupport_get_prefixchars ((const char *(*)(void))server_funcs[53]) +#define batch_get_current ((batch_t *(*)(void))(server_funcs[54])) #endif /* MAKING_SERVER */ @@ -141,6 +149,21 @@ typedef struct monitor_list { struct monitor_list *next; /* Linked list y'all */ } monitor_list_t; +/* An IRCv3 batch opened by the server and not yet closed. Nesting is tracked + * through parent rather than through position in the list because batches + * are permitted to interleave. + */ +typedef struct batch_list { + char reftag[BATCHREFMAX + 1]; /* Case-sensitive identifier */ + char type[BATCHTYPEMAX + 1]; /* Batch type */ + char args[BATCHARGSMAX + 1]; /* Raw remainder of the BATCH + line */ + struct batch_list *parent; /* Enclosing batch, or NULL */ + time_t started; + struct batch_list *next; +} batch_t; + + + /* Available net types. */ enum { NETT_DALNET, /* DALnet */ diff --git a/src/mod/server.mod/servmsg.c b/src/mod/server.mod/servmsg.c index d00779b2a..e524cb7b0 100644 --- a/src/mod/server.mod/servmsg.c +++ b/src/mod/server.mod/servmsg.c @@ -1090,6 +1090,7 @@ static void disconnect_server(int idx) while (cap != NULL) { del_capability(cap->name); } + batch_free_all(); server_online = 0; if (realservername) nfree(realservername); @@ -1227,6 +1228,7 @@ static void server_activity(int idx, char *tagmsg, int len) char *from, *code, *msgptr; char rawmsg[RECVLINEMAX+7]; int ret; + batch_t *saved_batch; Tcl_Obj *tagdict = Tcl_NewDictObj(); Tcl_IncrRefCount(tagdict); @@ -1263,6 +1265,11 @@ static void server_activity(int idx, char *tagmsg, int len) from = newsplit(&msgptr); } code = newsplit(&msgptr); + + /* Make the batch context of this line visible to handlers duration */ + saved_batch = current_batch; + current_batch = batch_from_tagdict(tagdict); + if (raw_log && ((strcmp(code, "PRIVMSG") && strcmp(code, "NOTICE")) || !match_ignore(from))) { putlog(LOG_RAW, "*", "[@] %s", rawmsg); @@ -1275,6 +1282,7 @@ static void server_activity(int idx, char *tagmsg, int len) if (!ret) { check_tcl_raw(from, code, msgptr); } + current_batch = saved_batch; Tcl_DecrRefCount(tagdict); } @@ -1454,6 +1462,11 @@ static int del_capability(char *name) { prev = curr; } } + /* Remove any remaining/hanging batch commands */ + if (!strcasecmp(name, "batch")) { + batch_free_all(); + } + putlog(LOG_SERV, "*", "CAP: %s not found, can't remove", name); return -1; } @@ -1581,6 +1594,9 @@ static int gotcap(char *from, char *msg) { } else if (!strcmp(current->name, "message-tags")) { if ((message_tags) && (!current->enabled)) add_req(current->name); + } else if (!strcmp(current->name, "batch")) { + if ((batch) && (!current->enabled)) + add_req(current->name); } /* Add any custom capes the user listed */ strlcpy(cape, cap_request, sizeof cape); @@ -1905,6 +1921,7 @@ static cmd_t my_raw_binds[] = { {"KICK", "", (IntFunc) gotkick, NULL}, {"CAP", "", (IntFunc) gotcap, NULL}, {"SETNAME", "", (IntFunc) gotsetname, NULL}, + {"BATCH", "", (IntFunc) gotbatch, NULL}, {NULL, NULL, NULL, NULL} }; From e053baa401b77afeeb1179199db4b9b62f31190f Mon Sep 17 00:00:00 2001 From: Geo Date: Sat, 12 Sep 2026 19:00:17 -0400 Subject: [PATCH 02/16] Fix sequencing --- src/mod/server.mod/batch.c | 22 +++++++++++++++------- src/mod/server.mod/server.c | 2 +- src/mod/server.mod/server.h | 1 + 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index bb142c65f..80205e7f8 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -23,6 +23,7 @@ */ static int batchcount = 0; +static unsigned long batchseq = 0; /* Tracks 'oldest' non-closed session for expiration */ static batch_t *batchlist = NULL; /* List of batches the server has opened but not yet closed. */ static batch_t *current_batch = NULL; /* The batch that the line currently being dispatched belongs */ /* to, or NULL if that line carried no batch tag. */ @@ -93,6 +94,14 @@ static void batch_free_one(batch_t *b) } } +/* Was sequence number a assigned before b? + * Optimized for comparing after a sequence wrap. + */ +static int batch_seq_older(unsigned long a, unsigned long b) +{ + return (a - b) > (ULONG_MAX / 2); +} + /* Is maybe an ancestor of b, or b itself? */ static int batch_is_ancestor(const batch_t *maybe, const batch_t *b) { @@ -103,20 +112,18 @@ static int batch_is_ancestor(const batch_t *maybe, const batch_t *b) return 0; } -/* The batch open the longest, ignoring protect and everything it is nested - * inside. Evicting a batch cascades to its children, so an ancestor of the - * batch we are about to open must be excluded, or the new record would be - * left with a parent pointer into freed memory. - */ +/* Find the batch open the longest based on sequence */ static batch_t *batch_oldest(const batch_t *protect) { batch_t *b, *oldest = NULL; for (b = batchlist; b; b = b->next) { - if (batch_is_ancestor(b, protect)) + if (batch_is_ancestor(b, protect)) { continue; - if (!oldest || b->started < oldest->started) + } + if (!oldest || batch_seq_older(b->seq, oldest->seq)) { oldest = b; + } } return oldest; } @@ -153,6 +160,7 @@ static batch_t *batch_start(const char *reftag, const char *type, if (args) strlcpy(b->args, args, sizeof b->args); b->parent = parent; + b->seq = batchseq++; b->started = now; b->next = batchlist; batchlist = b; diff --git a/src/mod/server.mod/server.c b/src/mod/server.mod/server.c index 0ec991d1a..2166afa60 100644 --- a/src/mod/server.mod/server.c +++ b/src/mod/server.mod/server.c @@ -135,9 +135,9 @@ static int batch = 0; static char cap_request[CAPMAX - 9]; -#include "batch.c" #include "isupport.c" #include "tclisupport.c" +#include "batch.c" #include "servmsg.c" #include "sasl.c" diff --git a/src/mod/server.mod/server.h b/src/mod/server.mod/server.h index 4a5c470b6..70f2515b2 100644 --- a/src/mod/server.mod/server.h +++ b/src/mod/server.mod/server.h @@ -158,6 +158,7 @@ typedef struct batch_list { char type[BATCHTYPEMAX + 1]; /* Batch type */ char args[BATCHARGSMAX + 1]; /* Raw remainder of the BATCH + line */ struct batch_list *parent; /* Enclosing batch, or NULL */ + unsigned long seq; /* Insertion order, for eviction */ time_t started; struct batch_list *next; } batch_t; From 8d429d0ea7b6541c6fa9f3d7ad4f8a138f8d4295 Mon Sep 17 00:00:00 2001 From: Geo Date: Sat, 12 Sep 2026 19:48:25 -0400 Subject: [PATCH 03/16] Add batch bind --- src/mod/server.mod/batch.c | 82 ++++++++++++++++++++++++------------- src/mod/server.mod/server.c | 19 ++++++++- src/mod/server.mod/server.h | 2 + 3 files changed, 73 insertions(+), 30 deletions(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index 80205e7f8..46b52a3f8 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -28,7 +28,19 @@ static batch_t *batchlist = NULL; /* List of batches the server has opened static batch_t *current_batch = NULL; /* The batch that the line currently being dispatched belongs */ /* to, or NULL if that line carried no batch tag. */ -static void batch_end(batch_t *b); +static void batch_end(batch_t *b, const char *event); + +static void check_tcl_batch(batch_t *b, const char *event) +{ + Tcl_SetVar(interp, "_batch1", b->reftag, 0); + Tcl_SetVar(interp, "_batch2", b->type, 0); + Tcl_SetVar(interp, "_batch3", b->args, 0); + Tcl_SetVar(interp, "_batch4", (char *) event, 0); + Tcl_SetVar(interp, "_batch5", b->parent ? b->parent->reftag : "", 0); + check_tcl_bind(H_batch, b->type, 0, + " $_batch1 $_batch2 $_batch3 $_batch4 $_batch5", + MATCH_MASK | BIND_STACKABLE); +} static int batch_valid_reftag(const char *reftag) { @@ -64,21 +76,23 @@ static batch_t *batch_get_current(void) /* Free every open batch, good for disconnections/capability removal */ static void batch_free_all(void) { - batch_t *b, *next; - - for (b = batchlist; b; b = next) { + batch_t *b, *next, *expired = batchlist; + batchlist = NULL; + current_batch = NULL; + batchcount = 0; + for (b = expired; b; b = b->next) { + check_tcl_batch(b, "discard"); + } + for (b = expired; b; b = next) { next = b->next; putlog(LOG_DEBUG, "*", "BATCH: discarding unterminated batch %s (type %s)", b->reftag, b->type); nfree(b); } - batchlist = NULL; - current_batch = NULL; - batchcount = 0; } /* Unlink and free a single batch record, without touching its children */ -static void batch_free_one(batch_t *b) +static void batch_unlink(batch_t *b) { batch_t **prev; @@ -88,7 +102,6 @@ static void batch_free_one(batch_t *b) if (*prev == b) { *prev = b->next; batchcount--; - nfree(b); return; } } @@ -151,7 +164,7 @@ static batch_t *batch_start(const char *reftag, const char *type, putlog(LOG_DEBUG, "*", "BATCH: at the %d open batch limit, discarding " "oldest batch %s (type %s) to make room for %s", BATCHMAX, old->reftag, old->type, reftag); - batch_end(old); + batch_end(old, "discard"); } b = nmalloc(sizeof *b); memset(b, 0, sizeof *b); @@ -168,18 +181,7 @@ static batch_t *batch_start(const char *reftag, const char *type, return b; } -/* Close a batch and free it, along with anything nested inside it. - * - * A nested batch cannot outlive its parent: its end line would have to carry - * a tag naming the parent, and the spec forbids referring to a batch after it - * has ended, so once the parent closes no legal line can ever close the - * child. Those records are unreachable, so they go now rather than sitting in - * the list until we disconnect. - * - * The scan restarts after each removal because freeing a child also frees its - * own descendants, which can invalidate a saved next pointer. - */ -static void batch_end(batch_t *b) +static void batch_detach(batch_t *b, batch_t **head) { batch_t *cur; int found; @@ -188,17 +190,39 @@ static void batch_end(batch_t *b) found = 0; for (cur = batchlist; cur; cur = cur->next) { if (cur->parent == b) { - putlog(LOG_DEBUG, "*", "BATCH: discarding nested batch %s, its parent " - "%s closed first", cur->reftag, b->reftag); - batch_end(cur); + batch_detach(cur, head); found = 1; break; } } } while (found); - batch_free_one(b); + batch_unlink(b); + b->next = *head; + *head = b; +} + +/* Close a batch and free it, along with anything nested inside it. */ +static void batch_end(batch_t *b, const char *event) +{ + batch_t *doomed = NULL, *cur, *next; + + batch_detach(b, &doomed); + for (cur = doomed; cur; cur = cur->next) { + if (cur == b) { + check_tcl_batch(cur, event); + } else { + putlog(LOG_DEBUG, "*", "BATCH: discarding nested batch %s, its parent " + "%s closed first", cur->reftag, cur->parent->reftag); + check_tcl_batch(cur, "discard"); + } + } + for (cur = doomed; cur; cur = next) { + next = cur->next; + nfree(cur); + } } + /* Resolve the batch tag on an incoming line to an open batch record. Returns * NULL if the line carried no batch tag, or if it named a batch we have no * record of. @@ -263,10 +287,12 @@ static int gotbatch(char *from, char *msg) return 0; } b = batch_start(reftag, type, msg, current_batch); - if (b) + if (b) { putlog(LOG_DEBUG, "*", "BATCH: opened %s (type %s)%s%s", b->reftag, b->type, b->parent ? ", nested in " : "", b->parent ? b->parent->reftag : ""); + check_tcl_batch(b, "start"); + } } else if (prefix == '-') { b = batch_find(reftag); if (!b) { @@ -282,7 +308,7 @@ static int gotbatch(char *from, char *msg) putlog(LOG_DEBUG, "*", "BATCH: %s closed batch %s from a different batch " "context than it was opened in", from, reftag); putlog(LOG_DEBUG, "*", "BATCH: closed %s (type %s)", b->reftag, b->type); - batch_end(b); + batch_end(b, "end"); } else { putlog(LOG_DEBUG, "*", "BATCH: %s sent a BATCH with an unrecognized prefix " "'%c'", from, prefix); diff --git a/src/mod/server.mod/server.c b/src/mod/server.mod/server.c index 2166afa60..8d676fcc9 100644 --- a/src/mod/server.mod/server.c +++ b/src/mod/server.mod/server.c @@ -108,7 +108,7 @@ static struct monitor_list *monitor = NULL; static p_tcl_bind_list H_wall, H_raw, H_notc, H_msgm, H_msg, H_flud, H_ctcr, - H_ctcp, H_out, H_rawt, H_monitor; + H_ctcp, H_out, H_rawt, H_monitor, H_batch; static void empty_msgq(void); static void next_server(int *, char *, unsigned int *, char *); @@ -1429,6 +1429,18 @@ static int server_out STDVAR return TCL_OK; } +static int batch_5char STDVAR +{ + Function F = (Function) cd; + + BADARGS(6, 6, " reference type params event parent"); + + CHECKVALIDITY(batch_5char); + F(argv[1], argv[2], argv[3], argv[4], argv[5]); + return TCL_OK; +} + + static int monitor_2char STDVAR { Function F = (Function) cd; @@ -2254,6 +2266,7 @@ static char *server_close() del_bind_table(H_ctcr); del_bind_table(H_ctcp); del_bind_table(H_out); + del_bind_table(H_batch); del_bind_table(H_monitor); rem_tcl_coups(my_tcl_coups); rem_tcl_strings(my_tcl_strings); @@ -2368,7 +2381,8 @@ static Function server_table[] = { /* 52 - 55 */ (Function) & H_monitor, (Function) isupport_get_prefixchars, - (Function) batch_get_current + (Function) batch_get_current, + (Function) & H_batch }; char *server_start(Function *global_funcs) @@ -2479,6 +2493,7 @@ char *server_start(Function *global_funcs) H_ctcp = add_bind_table("ctcp", HT_STACKABLE, server_6char); H_out = add_bind_table("out", HT_STACKABLE, server_out); H_monitor = add_bind_table("monitor", HT_STACKABLE, monitor_2char); + H_batch = add_bind_table("batch", HT_STACKABLE, batch_5char); isupport_init(); add_builtins(H_raw, my_raw_binds); add_builtins(H_rawt, my_rawt_binds); diff --git a/src/mod/server.mod/server.h b/src/mod/server.mod/server.h index 70f2515b2..2a0e357a4 100644 --- a/src/mod/server.mod/server.h +++ b/src/mod/server.mod/server.h @@ -111,6 +111,8 @@ #define H_monitor (*(p_tcl_bind_list *)(server_funcs[52])) #define isupport_get_prefixchars ((const char *(*)(void))server_funcs[53]) #define batch_get_current ((batch_t *(*)(void))(server_funcs[54])) +#define H_batch (*(p_tcl_bind_list *)(server_funcs[55])) +/* 56 - 59 */ #endif /* MAKING_SERVER */ From b3b65a7bf93f185d718f3e899bd4e5f6eed38f0b Mon Sep 17 00:00:00 2001 From: Geo Date: Sat, 12 Sep 2026 20:14:37 -0400 Subject: [PATCH 04/16] Add netsplit/join types --- src/mod/irc.mod/chan.c | 40 +++++++++++++++++++++++-------------- src/mod/irc.mod/irc.c | 6 +++--- src/mod/server.mod/server.c | 2 +- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/mod/irc.mod/chan.c b/src/mod/irc.mod/chan.c index 1f9558a6a..6d666ba71 100644 --- a/src/mod/irc.mod/chan.c +++ b/src/mod/irc.mod/chan.c @@ -2588,6 +2588,7 @@ static int gotquit(char *from, char *msg) memberlist *m; struct chanset_t *chan, *oldchan = NULL; struct userrec *u; + batch_t *b; strlcpy(from2, from, sizeof from2); nick = splitnick(&from); @@ -2595,22 +2596,31 @@ static int gotquit(char *from, char *msg) /* Fred1: Instead of expensive wild_match on signoff, quicker method. * Determine if signoff string matches "%.% %.%", and only one * space. + * + * 30 years later... If the server sent this QUIT inside an IRCv3 netsplit + * batch, it has told us outright that this is a split and there is nothing + * to guess at. */ - p = strchr(msg, ' '); - if (p && (p == strrchr(msg, ' '))) { - char *z1, *z2; - - *p = 0; - z1 = strchr(p + 1, '.'); - z2 = strchr(msg, '.'); - if (z1 && z2 && (*(z1 + 1) != 0) && (z1 - 1 != p) && - (z2 + 1 != p) && (z2 != msg)) { - /* Server split, or else it looked like it anyway (no harm in - * assuming) - */ - split = 1; - } else - *p = ' '; + b = batch_get_current(); + if (b && !strcmp(b->type, "netsplit")) { + split = 1; + } else { + p = strchr(msg, ' '); + if (p && (p == strrchr(msg, ' '))) { + char *z1, *z2; + + *p = 0; + z1 = strchr(p + 1, '.'); + z2 = strchr(msg, '.'); + if (z1 && z2 && (*(z1 + 1) != 0) && (z1 - 1 != p) && + (z2 + 1 != p) && (z2 != msg)) { + /* Server split, or else it looked like it anyway (no harm in + * assuming) + */ + split = 1; + } else + *p = ' '; + } } for (chan = chanset; chan; chan = chan->next) { oldchan = chan; diff --git a/src/mod/irc.mod/irc.c b/src/mod/irc.mod/irc.c index 204b0ed54..f388b1d70 100644 --- a/src/mod/irc.mod/irc.c +++ b/src/mod/irc.mod/irc.c @@ -1426,14 +1426,14 @@ char *irc_start(Function *global_funcs) global = global_funcs; - module_register(MODULE_NAME, irc_table, 1, 5); + module_register(MODULE_NAME, irc_table, 1, 6); if (!module_depend(MODULE_NAME, "eggdrop", 108, 0)) { module_undepend(MODULE_NAME); return "This module requires Eggdrop 1.8.0 or later."; } - if (!(server_funcs = module_depend(MODULE_NAME, "server", 1, 5))) { + if (!(server_funcs = module_depend(MODULE_NAME, "server", 1, 6))) { module_undepend(MODULE_NAME); - return "This module requires server module 1.5 or later."; + return "This module requires server module 1.6 or later."; } if (!(channels_funcs = module_depend(MODULE_NAME, "channels", 1, 1))) { module_undepend(MODULE_NAME); diff --git a/src/mod/server.mod/server.c b/src/mod/server.mod/server.c index 8d676fcc9..12124c4f5 100644 --- a/src/mod/server.mod/server.c +++ b/src/mod/server.mod/server.c @@ -2450,7 +2450,7 @@ char *server_start(Function *global_funcs) #endif server_table[4] = (Function) botname; - module_register(MODULE_NAME, server_table, 1, 5); + module_register(MODULE_NAME, server_table, 1, 6); if (!module_depend(MODULE_NAME, "eggdrop", 108, 0)) { module_undepend(MODULE_NAME); return "This module requires Eggdrop 1.8.0 or later."; From 1c834bafb3c056bde565a318e53962ffad3e1b2e Mon Sep 17 00:00:00 2001 From: Geo Date: Sat, 12 Sep 2026 20:22:02 -0400 Subject: [PATCH 05/16] cleanup --- src/mod/server.mod/batch.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index 46b52a3f8..895c78379 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -262,7 +262,7 @@ static int gotbatch(char *from, char *msg) char prefix; batch_t *b; - // Check for malformed states. Probably need to add more handling later + /* Check for malformed states. Probably need to add more handling later */ reftag = newsplit(&msg); if (!*reftag) { putlog(LOG_DEBUG, "*", "BATCH: %s sent a BATCH with no reference tag", from); @@ -286,6 +286,9 @@ static int gotbatch(char *from, char *msg) reftag); return 0; } + if (*msg == ':') { + msg++; + } b = batch_start(reftag, type, msg, current_batch); if (b) { putlog(LOG_DEBUG, "*", "BATCH: opened %s (type %s)%s%s", b->reftag, From 708499c9b3ad408f371a8ae11ed9213b6c865d3c Mon Sep 17 00:00:00 2001 From: Geo Date: Sun, 13 Sep 2026 09:27:54 -0400 Subject: [PATCH 06/16] fix capability delete ordering --- src/mod/server.mod/batch.c | 2 ++ src/mod/server.mod/servmsg.c | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index 895c78379..d776d17fb 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -22,6 +22,8 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +#include "server.h" + static int batchcount = 0; static unsigned long batchseq = 0; /* Tracks 'oldest' non-closed session for expiration */ static batch_t *batchlist = NULL; /* List of batches the server has opened but not yet closed. */ diff --git a/src/mod/server.mod/servmsg.c b/src/mod/server.mod/servmsg.c index e524cb7b0..4d43ae5db 100644 --- a/src/mod/server.mod/servmsg.c +++ b/src/mod/server.mod/servmsg.c @@ -1449,6 +1449,10 @@ static void free_capability(struct capability *z) { static int del_capability(char *name) { struct capability *curr, *prev; + /* If batch, remove any remaining/hanging batch sessions */ + if (!strcasecmp(name, "batch")) { + batch_free_all(); + } for (prev = NULL, curr = cap; curr; curr = prev ? prev->next : cap) { if (!strcasecmp(name, curr->name)) { if (prev) { @@ -1462,11 +1466,6 @@ static int del_capability(char *name) { prev = curr; } } - /* Remove any remaining/hanging batch commands */ - if (!strcasecmp(name, "batch")) { - batch_free_all(); - } - putlog(LOG_SERV, "*", "CAP: %s not found, can't remove", name); return -1; } From 472044f7c4f99e7e733556464cba3da8175c7733 Mon Sep 17 00:00:00 2001 From: Geo Date: Sun, 13 Sep 2026 09:40:10 -0400 Subject: [PATCH 07/16] Adjust batch bind arg ordering --- src/mod/server.mod/batch.c | 6 +++--- src/mod/server.mod/server.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index d776d17fb..217fd6890 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -36,9 +36,9 @@ static void check_tcl_batch(batch_t *b, const char *event) { Tcl_SetVar(interp, "_batch1", b->reftag, 0); Tcl_SetVar(interp, "_batch2", b->type, 0); - Tcl_SetVar(interp, "_batch3", b->args, 0); - Tcl_SetVar(interp, "_batch4", (char *) event, 0); - Tcl_SetVar(interp, "_batch5", b->parent ? b->parent->reftag : "", 0); + Tcl_SetVar(interp, "_batch3", (char *) event, 0); + Tcl_SetVar(interp, "_batch4", b->parent ? b->parent->reftag : "", 0); + Tcl_SetVar(interp, "_batch5", b->args, 0); check_tcl_bind(H_batch, b->type, 0, " $_batch1 $_batch2 $_batch3 $_batch4 $_batch5", MATCH_MASK | BIND_STACKABLE); diff --git a/src/mod/server.mod/server.c b/src/mod/server.mod/server.c index 12124c4f5..79f2b0172 100644 --- a/src/mod/server.mod/server.c +++ b/src/mod/server.mod/server.c @@ -1433,7 +1433,7 @@ static int batch_5char STDVAR { Function F = (Function) cd; - BADARGS(6, 6, " reference type params event parent"); + BADARGS(6, 6, " reference type event parent args"); CHECKVALIDITY(batch_5char); F(argv[1], argv[2], argv[3], argv[4], argv[5]); From 6cc7c06e6f1977105dbe92e12e52395e47512875 Mon Sep 17 00:00:00 2001 From: Geo Date: Sun, 13 Sep 2026 11:28:01 -0400 Subject: [PATCH 08/16] Incorporate tags into batch bind --- src/mod/server.mod/batch.c | 18 ++++++++++++------ src/mod/server.mod/server.h | 2 ++ src/mod/server.mod/servmsg.c | 4 ++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index 217fd6890..47825254b 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -25,10 +25,11 @@ #include "server.h" static int batchcount = 0; -static unsigned long batchseq = 0; /* Tracks 'oldest' non-closed session for expiration */ -static batch_t *batchlist = NULL; /* List of batches the server has opened but not yet closed. */ -static batch_t *current_batch = NULL; /* The batch that the line currently being dispatched belongs */ - /* to, or NULL if that line carried no batch tag. */ +static unsigned long batchseq = 0; /* Tracks 'oldest' non-closed session for expiration */ +static batch_t *batchlist = NULL; /* List of batches the server has opened but not yet closed. */ +static batch_t *current_batch = NULL; /* The batch that the line currently being dispatched belongs */ + /* to, or NULL if that line carried no batch tag. */ +static Tcl_Obj *current_tagdict = NULL; /* Message tags on the line currently being dispatched, or NULL */ static void batch_end(batch_t *b, const char *event); @@ -39,8 +40,9 @@ static void check_tcl_batch(batch_t *b, const char *event) Tcl_SetVar(interp, "_batch3", (char *) event, 0); Tcl_SetVar(interp, "_batch4", b->parent ? b->parent->reftag : "", 0); Tcl_SetVar(interp, "_batch5", b->args, 0); + Tcl_SetVar(interp, "_batch6", b->tags, 0); check_tcl_bind(H_batch, b->type, 0, - " $_batch1 $_batch2 $_batch3 $_batch4 $_batch5", + " $_batch1 $_batch2 $_batch3 $_batch4 $_batch5 $_batch6", MATCH_MASK | BIND_STACKABLE); } @@ -172,8 +174,12 @@ static batch_t *batch_start(const char *reftag, const char *type, memset(b, 0, sizeof *b); strlcpy(b->reftag, reftag, sizeof b->reftag); strlcpy(b->type, type, sizeof b->type); - if (args) + if (args) { strlcpy(b->args, args, sizeof b->args); + } + if (current_tagdict) { + strlcpy(b->tags, Tcl_GetString(current_tagdict), sizeof b->tags); + } b->parent = parent; b->seq = batchseq++; b->started = now; diff --git a/src/mod/server.mod/server.h b/src/mod/server.mod/server.h index 2a0e357a4..80ed68b95 100644 --- a/src/mod/server.mod/server.h +++ b/src/mod/server.mod/server.h @@ -36,6 +36,7 @@ #define BATCHTYPEMAX 128 /* Max length of a batch type */ #define BATCHARGSMAX 512 /* Max length of stored batch arguments */ #define BATCHMAX 32 /* Max concurrently open batches */ +#define BATCHTAGSMAX 512 /* Max length of stored BATCH + message tags */ @@ -159,6 +160,7 @@ typedef struct batch_list { char reftag[BATCHREFMAX + 1]; /* Case-sensitive identifier */ char type[BATCHTYPEMAX + 1]; /* Batch type */ char args[BATCHARGSMAX + 1]; /* Raw remainder of the BATCH + line */ + char tags[BATCHTAGSMAX + 1]; /* Message tags on the BATCH + line */ struct batch_list *parent; /* Enclosing batch, or NULL */ unsigned long seq; /* Insertion order, for eviction */ time_t started; diff --git a/src/mod/server.mod/servmsg.c b/src/mod/server.mod/servmsg.c index 4d43ae5db..aed240a3f 100644 --- a/src/mod/server.mod/servmsg.c +++ b/src/mod/server.mod/servmsg.c @@ -1229,6 +1229,7 @@ static void server_activity(int idx, char *tagmsg, int len) char rawmsg[RECVLINEMAX+7]; int ret; batch_t *saved_batch; + Tcl_Obj *saved_tagdict; Tcl_Obj *tagdict = Tcl_NewDictObj(); Tcl_IncrRefCount(tagdict); @@ -1268,7 +1269,9 @@ static void server_activity(int idx, char *tagmsg, int len) /* Make the batch context of this line visible to handlers duration */ saved_batch = current_batch; + saved_tagdict = current_tagdict; current_batch = batch_from_tagdict(tagdict); + current_tagdict = tagdict; if (raw_log && ((strcmp(code, "PRIVMSG") && strcmp(code, "NOTICE")) || !match_ignore(from))) { @@ -1283,6 +1286,7 @@ static void server_activity(int idx, char *tagmsg, int len) check_tcl_raw(from, code, msgptr); } current_batch = saved_batch; + current_tagdict = saved_tagdict; Tcl_DecrRefCount(tagdict); } From 7af83e9a08c3012bc1e83ebc5b352ec4ba5e9902 Mon Sep 17 00:00:00 2001 From: Geo Date: Sun, 13 Sep 2026 11:55:02 -0400 Subject: [PATCH 09/16] Add docs/config settings --- doc/IRCv3 | 2 ++ doc/tcl-commands.doc | 11 +++++++++++ eggdrop.conf | 13 +++++++++---- src/mod/server.mod/servmsg.c | 4 ++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/doc/IRCv3 b/doc/IRCv3 index 8accc0251..e606b5373 100644 --- a/doc/IRCv3 +++ b/doc/IRCv3 @@ -42,12 +42,14 @@ The following capabilities are supported by Eggdrop: - account-notify - account-tag - away-notify + - batch - BOT 005 mode - cap-notify - chghost - echo-message - extended-join - invite-notify + - labeled-response - message-tags - Monitor - server-time diff --git a/doc/tcl-commands.doc b/doc/tcl-commands.doc index 66cf75320..2dfe0cce7 100644 --- a/doc/tcl-commands.doc +++ b/doc/tcl-commands.doc @@ -4050,6 +4050,17 @@ the Tcl proc, and an explanation. the setting that was changed, and the value it was set to (0/1 for -/+, string, or X:Y formatted value). +(58) BATCH (stackable) + +bind batch + +procname + +Description: triggered when a server opens, closes, or Eggdrop discards an IRCv3 batch. flags are ignored, type is the batch type and can be used with wildcards. For the proc, reftag is the reference tag identifying the batch and is case-sensitive, type is the batch type such as 'netsplit' or 'chathistory'. Event can be 'start' (when the server opens the batch), 'end' (when the server closes it), or 'discard' (Eggdrop drops when its parent batch closes first or ages out after not being closed by the server). parent is the reference tag of the enclosing batch if this batch is nested inside another, otherwise it is empty. args is the remainder of the BATCH line following the type, and tags is a Tcl dict of the message tags present on the line that opened the batch (or empty if there were none). + +Module: server + + Return Values Several bindings pay attention to the value you return from the diff --git a/eggdrop.conf b/eggdrop.conf index 793af64be..2d8346f27 100755 --- a/eggdrop.conf +++ b/eggdrop.conf @@ -1086,10 +1086,9 @@ server add ssl.example.net +7000 # # The capabilities currently known to be supported by Eggdrop are as follows: # -# account-notify, account-tag, away-notify, chghost, echo-message, -# extended-join, invite-notify, message-tags, server-time, sasl, setname, -# +typing -# +# account-notify, account-tag, away-notify, batch, chghost, echo-message, +# extended-join, invite-notify, labeled-response, message-tags, server-time, +# sasl, setname, +typing # SASL is a method that allows Eggdrop to authenticate with a NickServ service # as part of the connection process to a server, eliminating the need to later @@ -1145,12 +1144,18 @@ server add ssl.example.net +7000 # To request the account-notify feature via CAP, set this to 1 set account-notify 1 +# To request the batch feature vi CAP, set this to 1 +#set batch 0 + # To request the extended-join feature via CAP, set this to 1 set extended-join 1 # To request the invite-notify feature via CAP, set this to 1 #set invite-notify 0 +# To request the labeled-response feature via CAP, set this to 1 +#set labeled-response 0 + # To request the message-tags feature via CAP, set this to 1 #set message-tags 0 diff --git a/src/mod/server.mod/servmsg.c b/src/mod/server.mod/servmsg.c index aed240a3f..50619a6ce 100644 --- a/src/mod/server.mod/servmsg.c +++ b/src/mod/server.mod/servmsg.c @@ -1600,6 +1600,10 @@ static int gotcap(char *from, char *msg) { } else if (!strcmp(current->name, "batch")) { if ((batch) && (!current->enabled)) add_req(current->name); + } else if (!strcmp(current->name, "labeled-response")) { + if (labeled_response && !current->enabled) { + add_req(current->name); + } } /* Add any custom capes the user listed */ strlcpy(cape, cap_request, sizeof cape); From f206f6a62be3710c357bb2194a04502f15706808 Mon Sep 17 00:00:00 2001 From: Geo Date: Sun, 13 Sep 2026 11:58:17 -0400 Subject: [PATCH 10/16] oops --- src/mod/server.mod/batch.c | 4 ++-- src/mod/server.mod/server.c | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index 47825254b..3741102e8 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -34,7 +34,7 @@ static Tcl_Obj *current_tagdict = NULL; /* Message tags on the line currently be static void batch_end(batch_t *b, const char *event); static void check_tcl_batch(batch_t *b, const char *event) -{ +{ Tcl_SetVar(interp, "_batch1", b->reftag, 0); Tcl_SetVar(interp, "_batch2", b->type, 0); Tcl_SetVar(interp, "_batch3", (char *) event, 0); @@ -44,7 +44,7 @@ static void check_tcl_batch(batch_t *b, const char *event) check_tcl_bind(H_batch, b->type, 0, " $_batch1 $_batch2 $_batch3 $_batch4 $_batch5 $_batch6", MATCH_MASK | BIND_STACKABLE); -} +} static int batch_valid_reftag(const char *reftag) { diff --git a/src/mod/server.mod/server.c b/src/mod/server.mod/server.c index 79f2b0172..dce8fd33a 100644 --- a/src/mod/server.mod/server.c +++ b/src/mod/server.mod/server.c @@ -132,6 +132,7 @@ static int away_notify = 0; static int invite_notify = 0; static int message_tags = 0; static int batch = 0; +static int labeled_response = 0; static char cap_request[CAPMAX - 9]; From 36435c01b3b04f4b977ed7bcfdaf946c5d8267ed Mon Sep 17 00:00:00 2001 From: Geo Date: Sun, 13 Sep 2026 12:07:09 -0400 Subject: [PATCH 11/16] redo detaching --- src/mod/server.mod/batch.c | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index 3741102e8..d01e189cd 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -189,21 +189,29 @@ static batch_t *batch_start(const char *reftag, const char *type, return b; } -static void batch_detach(batch_t *b, batch_t **head) +/* Find first open batch nested inside b, or NULL if it has no children */ +static batch_t *batch_first_child(const batch_t *b) { batch_t *cur; - int found; - do { - found = 0; - for (cur = batchlist; cur; cur = cur->next) { - if (cur->parent == b) { - batch_detach(cur, head); - found = 1; - break; - } + for (cur = batchlist; cur; cur = cur->next) { + if (cur->parent == b) { + return cur; } - } while (found); + } + return NULL; +} + +/* Move b, and everything nested inside it, off the open list and onto head. + * The caller will own a self-contained list whose parent pointers are all still valid. + */ +static void batch_detach(batch_t *b, batch_t **head) +{ + batch_t *cur; + + while ((cur = batch_first_child(b)) != NULL) { + batch_detach(cur, head); + } batch_unlink(b); b->next = *head; *head = b; From 3536f99f84124c0db0c274621bb68d38cbda71df Mon Sep 17 00:00:00 2001 From: Geo Date: Sat, 19 Sep 2026 21:33:07 -0400 Subject: [PATCH 12/16] Incorporate ability to skip bind processing of batch replay --- src/mod/server.mod/batch.c | 28 ++++++++++++++++++++++++---- src/mod/server.mod/server.h | 13 +++++++------ src/mod/server.mod/servmsg.c | 2 +- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index d01e189cd..2467042eb 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -33,17 +33,27 @@ static Tcl_Obj *current_tagdict = NULL; /* Message tags on the line currently be static void batch_end(batch_t *b, const char *event); -static void check_tcl_batch(batch_t *b, const char *event) +static int check_tcl_batch(batch_t *b, const char *event) { + int x; Tcl_SetVar(interp, "_batch1", b->reftag, 0); Tcl_SetVar(interp, "_batch2", b->type, 0); Tcl_SetVar(interp, "_batch3", (char *) event, 0); Tcl_SetVar(interp, "_batch4", b->parent ? b->parent->reftag : "", 0); Tcl_SetVar(interp, "_batch5", b->args, 0); Tcl_SetVar(interp, "_batch6", b->tags, 0); - check_tcl_bind(H_batch, b->type, 0, + x = check_tcl_bind(H_batch, b->type, 0, " $_batch1 $_batch2 $_batch3 $_batch4 $_batch5 $_batch6", - MATCH_MASK | BIND_STACKABLE); + MATCH_MASK | BIND_STACKABLE | BIND_STACKRET); + return (x == BIND_EXEC_LOG); +} + +/* Should we not process this batch session through other Eggdrop binds? + * Useful, for example, to not trigger pub binds when processing chathistory + */ +static int batch_suppress(const char *code) +{ + return current_batch && current_batch->suppress && strcasecmp(code, "BATCH"); } static int batch_valid_reftag(const char *reftag) @@ -181,6 +191,9 @@ static batch_t *batch_start(const char *reftag, const char *type, strlcpy(b->tags, Tcl_GetString(current_tagdict), sizeof b->tags); } b->parent = parent; + if (parent) { + b->suppress = parent->suppress; + } b->seq = batchseq++; b->started = now; b->next = batchlist; @@ -310,7 +323,14 @@ static int gotbatch(char *from, char *msg) putlog(LOG_DEBUG, "*", "BATCH: opened %s (type %s)%s%s", b->reftag, b->type, b->parent ? ", nested in " : "", b->parent ? b->parent->reftag : ""); - check_tcl_batch(b, "start"); + /* Suppress session if parent session is suppressed */ + if (check_tcl_batch(b, "start")) { + b->suppress = 1; + } + if (b->suppress) { + putlog(LOG_DEBUG, "*", "BATCH: %s is suppressed, lines will not trigger " + "binds", b->reftag); + } } } else if (prefix == '-') { b = batch_find(reftag); diff --git a/src/mod/server.mod/server.h b/src/mod/server.mod/server.h index 80ed68b95..ffd68d83a 100644 --- a/src/mod/server.mod/server.h +++ b/src/mod/server.mod/server.h @@ -157,12 +157,13 @@ typedef struct monitor_list { * are permitted to interleave. */ typedef struct batch_list { - char reftag[BATCHREFMAX + 1]; /* Case-sensitive identifier */ - char type[BATCHTYPEMAX + 1]; /* Batch type */ - char args[BATCHARGSMAX + 1]; /* Raw remainder of the BATCH + line */ - char tags[BATCHTAGSMAX + 1]; /* Message tags on the BATCH + line */ - struct batch_list *parent; /* Enclosing batch, or NULL */ - unsigned long seq; /* Insertion order, for eviction */ + char reftag[BATCHREFMAX + 1]; /* Case-sensitive identifier */ + char type[BATCHTYPEMAX + 1]; /* Batch type */ + char args[BATCHARGSMAX + 1]; /* Raw remainder of the BATCH + line */ + char tags[BATCHTAGSMAX + 1]; /* Message tags on the BATCH + line */ + struct batch_list *parent; /* Enclosing batch, or NULL */ + int suppress; /* Don't process binds for this session */ + unsigned long seq; /* Insertion order, for eviction */ time_t started; struct batch_list *next; } batch_t; diff --git a/src/mod/server.mod/servmsg.c b/src/mod/server.mod/servmsg.c index 50619a6ce..0b9353f12 100644 --- a/src/mod/server.mod/servmsg.c +++ b/src/mod/server.mod/servmsg.c @@ -1282,7 +1282,7 @@ static void server_activity(int idx, char *tagmsg, int len) /* Tcl_GetString() must not be modified, so we have to copy because string C API is not const char* */ strlcpy(rawmsg, Tcl_GetString(tagdict), sizeof rawmsg); ret = check_tcl_rawt(from, code, msgptr, rawmsg); - if (!ret) { + if (!ret && !batch_suppress(code)) { check_tcl_raw(from, code, msgptr); } current_batch = saved_batch; From 4512300412d78616cf6bcb3429aa8fa2c0b1eef8 Mon Sep 17 00:00:00 2001 From: Geo Date: Sat, 19 Sep 2026 22:06:18 -0400 Subject: [PATCH 13/16] Don't search every line for batch --- src/mod/server.mod/batch.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mod/server.mod/batch.c b/src/mod/server.mod/batch.c index 2467042eb..bf6bdd08f 100644 --- a/src/mod/server.mod/batch.c +++ b/src/mod/server.mod/batch.c @@ -264,6 +264,9 @@ static batch_t *batch_from_tagdict(Tcl_Obj *tagdict) if (!tagdict) return NULL; + /* Don't bother if the table is empty */ + if (!batchlist) + return NULL; key = Tcl_NewStringObj("batch", -1); Tcl_IncrRefCount(key); if ((Tcl_DictObjGet(interp, tagdict, key, &value) != TCL_OK) || !value) { From d091d5589f6fffc0850c90c0e4fd000ef3f37d23 Mon Sep 17 00:00:00 2001 From: Geo Date: Sat, 19 Sep 2026 22:14:23 -0400 Subject: [PATCH 14/16] Add to Makefile --- src/mod/server.mod/Makefile | 2 +- src/mod/server.mod/servmsg.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mod/server.mod/Makefile b/src/mod/server.mod/Makefile index 235641741..a7f93db17 100644 --- a/src/mod/server.mod/Makefile +++ b/src/mod/server.mod/Makefile @@ -41,4 +41,4 @@ distclean: clean .././server.mod/tclisupport.c .././server.mod/servmsg.c \ .././server.mod/../irc.mod/irc.h \ .././server.mod/../channels.mod/channels.h .././server.mod/sasl.c \ - .././server.mod/cmdsserv.c .././server.mod/tclserv.c + .././server.mod/cmdsserv.c .././server.mod/tclserv.c .././server.mod/batch.c diff --git a/src/mod/server.mod/servmsg.c b/src/mod/server.mod/servmsg.c index 0b9353f12..2d3baf640 100644 --- a/src/mod/server.mod/servmsg.c +++ b/src/mod/server.mod/servmsg.c @@ -1270,7 +1270,7 @@ static void server_activity(int idx, char *tagmsg, int len) /* Make the batch context of this line visible to handlers duration */ saved_batch = current_batch; saved_tagdict = current_tagdict; - current_batch = batch_from_tagdict(tagdict); + current_batch = batch_from_tagdict(tagdict); /* This is the batch record for the batch ref in msgtag */ current_tagdict = tagdict; if (raw_log && ((strcmp(code, "PRIVMSG") && strcmp(code, "NOTICE")) || From 86a9ad57d1549bb73578149b39c390219837915b Mon Sep 17 00:00:00 2001 From: Geo Date: Tue, 22 Sep 2026 18:12:35 -0400 Subject: [PATCH 15/16] Add getbatch command --- doc/sphinx_source/using/tcl-commands.rst | 7 +++++ src/mod/server.mod/tclserv.c | 39 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/doc/sphinx_source/using/tcl-commands.rst b/doc/sphinx_source/using/tcl-commands.rst index 5ae8be558..84779e31a 100644 --- a/doc/sphinx_source/using/tcl-commands.rst +++ b/doc/sphinx_source/using/tcl-commands.rst @@ -1106,6 +1106,13 @@ monitor [nickname] Module: irc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +getbatch +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Description: provides information about a current IRCv3 batch session. + + Returns: a dict of the information Eggdrop is tracking for an open batch session, containing the keys ```reftag type args parent suppress started seq```, where reftag is the case-sensitive batch session reference tag, type is the batch type, tags is a nested dict of the message tags that were associated with the BATCH command, parent is the reference tag for a parent batch session, suppress is 1 if a batch bind is suppressing further bind processing of this batch session and 0 if not, started is a timestamp of when the batch was opened, seq is an internally-tracked sequence number used for tracking batch message ordering, and args is everything after the ```BATCH +``` portion of the command. + ^^^^^^^^^^^^^^^ accounttracking ^^^^^^^^^^^^^^^ diff --git a/src/mod/server.mod/tclserv.c b/src/mod/server.mod/tclserv.c index a50c6caf3..0355a0e04 100644 --- a/src/mod/server.mod/tclserv.c +++ b/src/mod/server.mod/tclserv.c @@ -428,6 +428,44 @@ static int tcl_monitor STDVAR } } +/* Return what Eggdrop is tracking for an open batch as a Tcl dict, or an + * empty string if no batch is open under that reference tag. Reference tags + * are case-sensitive, so the lookup is too. + */ +static int tcl_getbatch STDVAR { + batch_t *b; + Tcl_Obj *batchinfo; + + BADARGS(2, 2, " batch-reference-tag"); + + b = batch_find(argv[1]); + if (!b) { + Tcl_AppendResult(irp, "", NULL); + return TCL_OK; + } + batchinfo = Tcl_NewDictObj(); + Tcl_DictObjPut(irp, batchinfo, Tcl_NewStringObj("reftag", -1), + Tcl_NewStringObj(b->reftag, -1)); + Tcl_DictObjPut(irp, batchinfo, Tcl_NewStringObj("type", -1), + Tcl_NewStringObj(b->type, -1)); + Tcl_DictObjPut(irp, batchinfo, Tcl_NewStringObj("args", -1), + Tcl_NewStringObj(b->args, -1)); + Tcl_DictObjPut(irp, batchinfo, Tcl_NewStringObj("tags", -1), + Tcl_NewStringObj(b->tags, -1)); + /* The parent is a pointer internally, scripts get its reference tag */ + Tcl_DictObjPut(irp, batchinfo, Tcl_NewStringObj("parent", -1), + Tcl_NewStringObj(b->parent ? b->parent->reftag : "", -1)); + Tcl_DictObjPut(irp, batchinfo, Tcl_NewStringObj("suppress", -1), + Tcl_NewIntObj(b->suppress)); + Tcl_DictObjPut(irp, batchinfo, Tcl_NewStringObj("started", -1), + Tcl_NewWideIntObj((Tcl_WideInt) b->started)); + Tcl_DictObjPut(irp, batchinfo, Tcl_NewStringObj("seq", -1), + Tcl_NewWideIntObj((Tcl_WideInt) b->seq)); + Tcl_SetObjResult(irp, batchinfo); + return TCL_OK; +} + + static int tcl_jump STDVAR { BADARGS(1, 4, " ?server? ?port? ?pass?"); @@ -663,5 +701,6 @@ static tcl_cmds my_tcl_cmds[] = { {"getaccount", tcl_getaccount}, {"isidentified", tcl_isidentified}, {"monitor", tcl_monitor}, + {"getbatch", tcl_getbatch}, {NULL, NULL} }; From 539d01883001a5a62917538c7ec1bf0b0c35b6f8 Mon Sep 17 00:00:00 2001 From: Geo Date: Tue, 22 Sep 2026 18:42:45 -0400 Subject: [PATCH 16/16] Fix documentation --- doc/sphinx_source/using/tcl-commands.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx_source/using/tcl-commands.rst b/doc/sphinx_source/using/tcl-commands.rst index 84779e31a..7be01d882 100644 --- a/doc/sphinx_source/using/tcl-commands.rst +++ b/doc/sphinx_source/using/tcl-commands.rst @@ -1111,7 +1111,7 @@ getbatch ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Description: provides information about a current IRCv3 batch session. - Returns: a dict of the information Eggdrop is tracking for an open batch session, containing the keys ```reftag type args parent suppress started seq```, where reftag is the case-sensitive batch session reference tag, type is the batch type, tags is a nested dict of the message tags that were associated with the BATCH command, parent is the reference tag for a parent batch session, suppress is 1 if a batch bind is suppressing further bind processing of this batch session and 0 if not, started is a timestamp of when the batch was opened, seq is an internally-tracked sequence number used for tracking batch message ordering, and args is everything after the ```BATCH +``` portion of the command. + Returns: a dict of the information Eggdrop is tracking for an open batch session, containing the keys ```reftag type tags args parent suppress started seq```, where reftag is the case-sensitive batch session reference tag, type is the batch type, tags is a nested dict of the message tags that were associated with the BATCH command, parent is the reference tag for a parent batch session, suppress is 1 if a batch bind is suppressing further bind processing of this batch session and 0 if not, started is a timestamp of when the batch was opened, seq is an internally-tracked sequence number used for tracking batch session ordering, and args is everything after the ```type``` portion of the command. ^^^^^^^^^^^^^^^ accounttracking