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/sphinx_source/using/tcl-commands.rst b/doc/sphinx_source/using/tcl-commands.rst index 5ae8be558..7be01d882 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 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 ^^^^^^^^^^^^^^^ 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/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/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/batch.c b/src/mod/server.mod/batch.c new file mode 100644 index 000000000..bf6bdd08f --- /dev/null +++ b/src/mod/server.mod/batch.c @@ -0,0 +1,359 @@ +/* + * 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. + */ + +#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 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); + +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); + x = check_tcl_bind(H_batch, b->type, 0, + " $_batch1 $_batch2 $_batch3 $_batch4 $_batch5 $_batch6", + 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) +{ + 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, *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); + } +} + +/* Unlink and free a single batch record, without touching its children */ +static void batch_unlink(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--; + return; + } + } +} + +/* 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) +{ + for (; b; b = b->parent) { + if (b == maybe) + return 1; + } + return 0; +} + +/* 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)) { + continue; + } + if (!oldest || batch_seq_older(b->seq, oldest->seq)) { + 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, "discard"); + } + 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); + } + if (current_tagdict) { + 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; + batchlist = b; + batchcount++; + return b; +} + +/* 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; + + for (cur = batchlist; cur; cur = cur->next) { + if (cur->parent == b) { + return cur; + } + } + 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; +} + +/* 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. + */ +static batch_t *batch_from_tagdict(Tcl_Obj *tagdict) +{ + Tcl_Obj *key, *value = NULL; + batch_t *b; + char *reftag; + + 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) { + 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; + } + 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, + b->type, b->parent ? ", nested in " : "", + b->parent ? b->parent->reftag : ""); + /* 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); + 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, "end"); + } 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..dce8fd33a 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 *); @@ -131,11 +131,14 @@ 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 int labeled_response = 0; static char cap_request[CAPMAX - 9]; #include "isupport.c" #include "tclisupport.c" +#include "batch.c" #include "servmsg.c" #include "sasl.c" @@ -1427,6 +1430,18 @@ static int server_out STDVAR return TCL_OK; } +static int batch_5char STDVAR +{ + Function F = (Function) cd; + + BADARGS(6, 6, " reference type event parent args"); + + 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; @@ -1768,6 +1783,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}, @@ -2251,6 +2267,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); @@ -2364,7 +2381,9 @@ static Function server_table[] = { (Function) encode_msgtags, /* 52 - 55 */ (Function) & H_monitor, - (Function) isupport_get_prefixchars + (Function) isupport_get_prefixchars, + (Function) batch_get_current, + (Function) & H_batch }; char *server_start(Function *global_funcs) @@ -2432,7 +2451,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."; @@ -2475,6 +2494,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 909fe2116..ffd68d83a 100644 --- a/src/mod/server.mod/server.h +++ b/src/mod/server.mod/server.h @@ -31,6 +31,14 @@ #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 BATCHTAGSMAX 512 /* Max length of stored BATCH + message tags */ + + #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 +111,9 @@ /* 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])) +#define H_batch (*(p_tcl_bind_list *)(server_funcs[55])) +/* 56 - 59 */ #endif /* MAKING_SERVER */ @@ -141,6 +152,24 @@ 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 */ + 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; + + + /* 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..2d3baf640 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,8 @@ 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 *saved_tagdict; Tcl_Obj *tagdict = Tcl_NewDictObj(); Tcl_IncrRefCount(tagdict); @@ -1263,6 +1266,13 @@ 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; + saved_tagdict = current_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")) || !match_ignore(from))) { putlog(LOG_RAW, "*", "[@] %s", rawmsg); @@ -1272,9 +1282,11 @@ 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; + current_tagdict = saved_tagdict; Tcl_DecrRefCount(tagdict); } @@ -1441,6 +1453,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) { @@ -1581,6 +1597,13 @@ 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); + } 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); @@ -1905,6 +1928,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} }; 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} };