1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-06 15:13:12 +02:00
Files
unrealircd/src/modules/multiline.c
T
2026-07-02 18:10:49 +02:00

2016 lines
61 KiB
C

/*
* IRC - Internet Relay Chat, src/modules/multiline.c
* (C) 2026 Syzop & The UnrealIRCd Team
*
* See file AUTHORS in IRC package for additional names of
* the programmers.
*
* 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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER
= {
"multiline",
"1.0",
"IRCv3 draft/multiline",
"UnrealIRCd Team",
"unrealircd-6",
};
/* ===================== CONFIGURATION ===================== */
struct {
int batch_timeout;
} cfg;
/* ===================== DATA STRUCTURES ===================== */
/** State for a locally-initiated multiline batch (one per local client) */
typedef struct MultilineBatch MultilineBatch;
struct MultilineBatch {
char batch_id[MAXBATCHREFLEN+1]; /**< Client-chosen batch reference tag */
char *target; /**< Target channel or nick */
SendType sendtype; /**< SEND_TYPE_PRIVMSG or SEND_TYPE_NOTICE */
int sendtype_set; /**< Has sendtype been determined (from first line)? */
char member_modes[2]; /**< Member mode filter from STATUSMSG prefix (e.g. "o"), or empty string */
MessageTag *client_mtags; /**< Tags from the opening BATCH command */
int line_count;
int received_bytes; /**< Total user-sent content bytes (for max-bytes policy) */
time_t start_time;
int failed; /**< Batch marked as failed — consume remaining lines, send error at BATCH close */
char *fail_message; /**< FAIL response to send at BATCH close (if failed) */
char label[256]; /**< Saved label for echo-message + labeled-response interaction */
/* Buffered lines */
MLine *lines;
MLine *lines_tail;
};
/** State for an S2S multiline batch being received from a remote server */
typedef struct S2SMultilineBatch S2SMultilineBatch;
struct S2SMultilineBatch {
S2SMultilineBatch *prev, *next;
Client *sender; /**< Remote user who initiated */
Client *direction; /**< Server link it came from */
char batch_id[BATCHLEN+1];
char *target;
int is_channel; /**< 1 if target is a channel, 0 if user */
SendType sendtype;
int sendtype_set;
char member_modes[2]; /**< Member mode filter from STATUSMSG prefix */
MessageTag *first_mtags; /**< Message tags from the opening BATCH (for relay) */
int line_count;
int received_bytes;
time_t start_time;
MLine *lines;
MLine *lines_tail;
};
/* ===================== FORWARD DECLARATIONS ===================== */
/* Module callbacks */
int multiline_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int multiline_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
/* Command overrides */
CMD_OVERRIDE_FUNC(multiline_override_batch);
CMD_OVERRIDE_FUNC(multiline_override_msg);
/* Message tag handler */
int multiline_concat_mtag_is_ok(Client *client, const char *name, const char *value);
/* Capability parameter callback */
const char *multiline_capability_parameter(Client *client);
/* ModData free callback */
void multiline_mdata_free(ModData *m);
/* Hooks */
int multiline_close_connection(Client *client);
int multiline_remote_quit(Client *client, MessageTag *mtags, const char *comment);
int multiline_server_quit(Client *client, MessageTag *mtags);
int multiline_known_user_cache_change(Client *client);
/* Timer */
EVENT(multiline_timeout_check);
/* Core functions */
static void multiline_deliver(Client *client, MultilineBatch *batch);
static void multiline_deliver_channel(Client *client, MultilineBatch *batch, Channel *channel);
static void multiline_deliver_user(Client *client, MultilineBatch *batch, Client *target);
static void multiline_send_batch_to_client(Client *to, Client *from, MultilineBatch *batch, MessageTag *base_mtags, const char *targetstr, const char *cmd);
static void multiline_send_fallback_to_client(Client *to, Client *from, MultilineBatch *batch, MessageTag *base_mtags, const char *targetstr, const char *cmd);
static void multiline_echo_to_sender(Client *client, MultilineBatch *batch, MessageTag *mtags, const char *targetstr, const char *cmd);
static void multiline_send_s2s_to_direction(Client *direction, Client *from, MultilineBatch *batch, MessageTag *base_mtags, const char *cmd, const char *targetstr, const char *ref);
static void multiline_send_s2s_channel(Client *from, MultilineBatch *batch, Channel *channel, MessageTag *base_mtags, const char *cmd, Client *skip_direction, const char *targetstr);
static void multiline_send_s2s_user(Client *from, MultilineBatch *batch, Client *target, MessageTag *base_mtags, const char *cmd);
static void multiline_deliver_to_local_members(Channel *channel, Client *from, MultilineBatch *batch, MessageTag *mtags, const char *targetstr, const char *cmd, const char *filter_modes, Client *skip, int sendflags);
static void multiline_run_chanmsg_hooks(Client *sender, Channel *channel, int sendflags, const char *member_modes, const char *targetstr, MessageTag *mtags, MLine *lines, SendType sendtype);
static void multiline_run_usermsg_hooks(Client *sender, Client *target, MessageTag *mtags, MLine *lines, SendType sendtype);
static void multiline_fail_batch(Client *client, MultilineBatch *batch, const char *fail_msg);
static void multiline_abort_batch(Client *client, MultilineBatch *batch);
static void multiline_append_line(MLine **lines, MLine **lines_tail, int *line_count, const char *text, int is_concat);
static void multiline_free_lines(MLine *lines);
static void multiline_free_batch(MultilineBatch *batch);
static char *multiline_concat_text(MultilineBatch *batch);
/* S2S receiving */
static void multiline_handle_s2s_batch_open(Client *client, MessageTag *recv_mtags, int parc, const char *parv[], int is_channel);
static void multiline_handle_s2s_batch_close(Client *client, const char *batch_id);
static void multiline_handle_s2s_msg(Client *client, MessageTag *recv_mtags, int parc, const char *parv[], SendType sendtype);
static S2SMultilineBatch *multiline_find_s2s_batch(Client *sender, const char *batch_id);
static void multiline_free_s2s_batch(S2SMultilineBatch *s2s);
static void multiline_deliver_s2s_batch(S2SMultilineBatch *s2s);
static void multiline_free_s2s_batches_all(ModData *m);
/* ===================== VARIABLES ===================== */
static long CAP_MULTILINE = 0L;
static long CAP_BATCH = 0L;
static long CAP_NOTIFY = 0L;
#define HasMultiline(c) (HasCapabilityFast(c, CAP_MULTILINE) && HasCapabilityFast(c, CAP_BATCH))
static ModDataInfo *multiline_md = NULL;
static S2SMultilineBatch *s2s_batches = NULL;
/* ===================== MODULE LIFECYCLE ===================== */
MOD_TEST()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, multiline_config_test);
return MOD_SUCCESS;
}
MOD_INIT()
{
ClientCapabilityInfo cap;
ClientCapability *c;
MessageTagHandlerInfo mtag;
ModDataInfo mreq;
MARK_AS_OFFICIAL_MODULE(modinfo);
LoadPersistentPointer(modinfo, s2s_batches, multiline_free_s2s_batches_all);
/* Register draft/multiline capability */
memset(&cap, 0, sizeof(cap));
cap.name = "draft/multiline";
cap.parameter = multiline_capability_parameter;
c = ClientCapabilityAdd(modinfo->handle, &cap, &CAP_MULTILINE);
/* Register draft/multiline-concat message tag */
memset(&mtag, 0, sizeof(mtag));
mtag.name = "draft/multiline-concat";
mtag.is_ok = multiline_concat_mtag_is_ok;
mtag.clicap_handler = c;
MessageTagHandlerAdd(modinfo->handle, &mtag);
/* Register ModData for per-client multiline state */
memset(&mreq, 0, sizeof(mreq));
mreq.name = "multiline";
mreq.type = MODDATATYPE_LOCAL_CLIENT;
mreq.free = multiline_mdata_free;
mreq.serialize = NULL;
mreq.unserialize = NULL;
mreq.sync = 0;
multiline_md = ModDataAdd(modinfo->handle, mreq);
/* Hooks */
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, multiline_config_run);
HookAdd(modinfo->handle, HOOKTYPE_CLOSE_CONNECTION, 0, multiline_close_connection);
HookAdd(modinfo->handle, HOOKTYPE_REMOTE_QUIT, 0, multiline_remote_quit);
HookAdd(modinfo->handle, HOOKTYPE_SERVER_QUIT, 0, multiline_server_quit);
HookAdd(modinfo->handle, HOOKTYPE_KNOWN_USER_CACHE_CHANGE, 1000000, multiline_known_user_cache_change); /* (prio: near-last) */
/* Timer for batch timeout */
EventAdd(modinfo->handle, "multiline_timeout", multiline_timeout_check, NULL, 5000, 0);
/* Set default batch timeout (multiline limits are in FloodSettings via conf.c) */
cfg.batch_timeout = 15;
return MOD_SUCCESS;
}
MOD_LOAD()
{
CAP_BATCH = ClientCapabilityBit("batch");
CAP_NOTIFY = ClientCapabilityBit("cap-notify");
/* Command overrides */
CommandOverrideAdd(modinfo->handle, "BATCH", 0, multiline_override_batch);
CommandOverrideAdd(modinfo->handle, "PRIVMSG", 0, multiline_override_msg);
CommandOverrideAdd(modinfo->handle, "NOTICE", 0, multiline_override_msg);
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
SavePersistentPointer(modinfo, s2s_batches);
return MOD_SUCCESS;
}
/* ===================== CAPABILITY ===================== */
const char *multiline_capability_parameter(Client *client)
{
static char buf[128];
FloodSettings *f = get_floodsettings_for_user(client, FLD_MULTILINE);
snprintf(buf, sizeof(buf), "max-bytes=%d,max-lines=%d",
(int)f->period[FLD_MULTILINE],
(int)f->limit[FLD_MULTILINE]);
return buf;
}
/** Called when a user's known-user status changes.
* Sends an updated CAP NEW with the new multiline parameters
* so the client knows its updated max-lines/max-bytes limits.
*/
int multiline_known_user_cache_change(Client *client)
{
if (!MyConnect(client))
return HOOK_CONTINUE;
if (!HasCapabilityFast(client, CAP_NOTIFY))
return HOOK_CONTINUE;
if (client->local->cap_protocol >= 302)
{
const char *args = multiline_capability_parameter(client);
sendto_one(client, NULL, ":%s CAP %s NEW :draft/multiline=%s",
me.name, (*client->name ? client->name : "*"), args);
} else {
sendto_one(client, NULL, ":%s CAP %s NEW :draft/multiline",
me.name, (*client->name ? client->name : "*"));
}
return HOOK_CONTINUE;
}
/* ===================== MESSAGE TAG ===================== */
int multiline_concat_mtag_is_ok(Client *client, const char *name, const char *value)
{
/* Tag must have no value per spec */
if (!BadPtr(value))
return 0;
if (IsServer(client))
return 1;
/* Allow from local users who have an active multiline batch */
if (MyUser(client))
{
MultilineBatch *batch = moddata_local_client(client, multiline_md).ptr;
if (batch)
return 1;
}
return 0;
}
/* ===================== CONFIGURATION ===================== */
int multiline_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep;
if (type != CONFIG_SET)
return 0;
if (!ce || strcmp(ce->name, "multiline"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!cep->value)
{
config_error("%s:%i: blank set::multiline::%s without value",
cep->file->filename, cep->line_number, cep->name);
errors++;
continue;
}
if (!strcmp(cep->name, "batch-timeout"))
{
int v = atoi(cep->value);
if (v < 5 || v > 120)
{
config_error("%s:%i: set::multiline::batch-timeout must be between 5 and 120 seconds",
cep->file->filename, cep->line_number);
errors++;
}
} else
{
config_error("%s:%i: unknown directive set::multiline::%s",
cep->file->filename, cep->line_number, cep->name);
errors++;
}
}
*errs = errors;
return errors ? -1 : 1;
}
int multiline_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep;
if (type != CONFIG_SET)
return 0;
if (!ce || strcmp(ce->name, "multiline"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "batch-timeout"))
cfg.batch_timeout = atoi(cep->value);
}
return 1;
}
/* ===================== MODDATA ===================== */
void multiline_mdata_free(ModData *m)
{
MultilineBatch *batch = m->ptr;
if (batch)
{
multiline_free_batch(batch);
m->ptr = NULL;
}
}
/* ===================== HELPER FUNCTIONS ===================== */
/** Find channel for a target that may have a STATUSMSG prefix (e.g. "@#channel") */
static inline Channel *multiline_find_channel(const char *target)
{
const char *p = strchr(target, '#');
return p ? find_channel(p) : NULL;
}
/** Duplicate a message tag list, excluding a specific tag name.
* @param mtags Source tag list
* @param exclude Tag name to exclude
* @returns New tag list (caller must free with free_message_tags())
*/
static MessageTag *duplicate_mtags_excluding(MessageTag *mtags, const char *exclude)
{
MessageTag *out = NULL, *m, *dup;
for (m = mtags; m; m = m->next)
{
if (!strcmp(m->name, exclude))
continue;
dup = duplicate_mtag(m);
AddListItem(dup, out);
}
return out;
}
/** Run HOOKTYPE_CHANMSG per-line for a multiline batch.
* First line carries mtags (incl msgid), subsequent lines don't.
*/
static void multiline_run_chanmsg_hooks(Client *sender, Channel *channel,
int sendflags, const char *member_modes, const char *targetstr,
MessageTag *mtags, MLine *lines, SendType sendtype)
{
MLine *line;
echo_message_inhibit = 1;
history_inhibit = 1;
for (line = lines; line; line = line->next)
{
RunHook(HOOKTYPE_CHANMSG, sender, channel, sendflags,
member_modes, targetstr,
(line == lines) ? mtags : NULL,
line->text, sendtype);
}
history_inhibit = 0;
echo_message_inhibit = 0;
/* Fire multiline hook for atomic history storage etc. */
RunHook(HOOKTYPE_CHANMSG_MULTILINE, sender, channel, sendflags,
member_modes, targetstr, mtags, lines, sendtype);
}
/** Run HOOKTYPE_USERMSG per-line for a multiline batch.
* First line carries mtags (incl msgid), subsequent lines don't.
*/
static void multiline_run_usermsg_hooks(Client *sender, Client *target,
MessageTag *mtags, MLine *lines, SendType sendtype)
{
MLine *line;
echo_message_inhibit = 1;
history_inhibit = 1;
for (line = lines; line; line = line->next)
{
RunHook(HOOKTYPE_USERMSG, sender, target,
(line == lines) ? mtags : NULL,
line->text, sendtype);
}
history_inhibit = 0;
echo_message_inhibit = 0;
}
/** Free a linked list of multiline lines */
static void multiline_free_lines(MLine *lines)
{
MLine *l, *l_next;
for (l = lines; l; l = l_next)
{
l_next = l->next;
safe_free(l->text);
safe_free(l);
}
}
/** Free a local multiline batch (but not the ModData slot itself) */
static void multiline_free_batch(MultilineBatch *batch)
{
if (!batch)
return;
safe_free(batch->target);
safe_free(batch->fail_message);
free_message_tags(batch->client_mtags);
multiline_free_lines(batch->lines);
safe_free(batch);
}
/** Append a line to a multiline line list */
static void multiline_append_line(MLine **lines, MLine **lines_tail,
int *line_count, const char *text, int is_concat)
{
MLine *line = safe_alloc(sizeof(MLine));
safe_strdup(line->text, text ? text : "");
line->concat = is_concat;
line->next = NULL;
if (*lines_tail)
(*lines_tail)->next = line;
else
*lines = line;
*lines_tail = line;
(*line_count)++;
}
/** Calculate the multiline batch fake lag in milliseconds.
* Uses the same per-line formula as parse.c with a floor of
* 1 extra line and a hard cap of 15 seconds.
* FIXME: reconsider exact algo
*/
static long calculate_multiline_fakelag(Client *client, MultilineBatch *batch)
{
FloodSettings *settings;
int lag_penalty, lag_penalty_bytes;
long lag_msec;
MLine *l;
if (!batch->lines)
return 0;
settings = get_floodsettings_for_user(client, FLD_LAG_PENALTY);
lag_penalty = settings->period[FLD_LAG_PENALTY];
lag_penalty_bytes = settings->limit[FLD_LAG_PENALTY];
if (lag_penalty_bytes < 1)
lag_penalty_bytes = 1;
lag_msec = lag_penalty; /* floor: 1 extra line */
for (l = batch->lines; l; l = l->next)
lag_msec += (1 + ((int)strlen(l->text) / lag_penalty_bytes)) * lag_penalty;
if (lag_msec > 15000)
lag_msec = 15000;
return lag_msec;
}
/** Mark a local multiline batch as failed.
* The batch stays open so remaining lines are silently consumed.
* The error is sent when the client sends BATCH -ref.
* @param fail_msg The FAIL response line (after ":<server> "), e.g.
* "FAIL BATCH MULTILINE_MAX_LINES 5 :Too many lines in batch"
*/
static void multiline_fail_batch(Client *client, MultilineBatch *batch, const char *fail_msg)
{
if (!batch)
return;
if (batch->failed)
return; /* Already marked, keep first error */
batch->failed = 1;
safe_strdup(batch->fail_message, fail_msg);
}
/** Abort a local multiline batch: apply half fake lag penalty and free state.
* Called at BATCH close for failed batches, or for fatal errors where
* the batch state must be freed immediately.
*/
static void multiline_abort_batch(Client *client, MultilineBatch *batch)
{
if (!batch)
return;
/* Apply half the batch fake lag penalty for aborted batches */
add_fake_lag(client, 2000 + calculate_multiline_fakelag(client, batch) / 2);
multiline_free_batch(batch);
moddata_local_client(client, multiline_md).ptr = NULL;
}
/** Calculate the number of bytes a line adds to a multiline batch.
* Accounts for the \n separator between non-concat lines.
* If you wonder why this is a separate and surprisingly small function,
* it is because already screwing up twice, where the local-case
* diverted from the remote-case, so hence this helper :D.
*/
static inline int multiline_calc_add_bytes(int text_len, int line_count, int is_concat)
{
return text_len + (((line_count > 0) && !is_concat) ? 1 : 0);
}
/** Concatenate all batch lines into a single string (for spamfilter).
* Concat lines are joined without separator, normal lines with \n.
* @returns A dynamically allocated string. Caller must free with safe_free().
*/
static char *multiline_concat_text(MultilineBatch *batch)
{
MLine *l;
int bufsize = 1; /* 1 for \0 */
char *buf;
int pos = 0;
/* Calculate buffer size from actual line data, since text
* may have been transformed at delivery time (+S, +G, etc.).
*/
for (l = batch->lines; l; l = l->next)
{
if (l != batch->lines && !l->concat)
bufsize++; /* \n separator */
if (l->text)
bufsize += strlen(l->text);
}
buf = safe_alloc(bufsize);
/* Now build the big string*/
for (l = batch->lines; l; l = l->next)
{
if (l != batch->lines && !l->concat)
{
if (pos + 1 >= bufsize)
break;
buf[pos++] = '\n';
}
if (l->text)
{
int len = strlen(l->text);
if (pos + len >= bufsize)
break;
strlcpy(buf + pos, l->text, bufsize - pos);
pos += len;
}
}
buf[pos] = '\0';
return buf;
}
/** Check if a batch has at least one non-blank line */
static int multiline_batch_has_content(MultilineBatch *batch)
{
MLine *l;
for (l = batch->lines; l; l = l->next)
if (l->text && l->text[0])
return 1;
return 0;
}
/* ===================== BATCH COMMAND OVERRIDE ===================== */
CMD_OVERRIDE_FUNC(multiline_override_batch)
{
const char *ref;
MultilineBatch *batch;
/* Handle S2S multiline batches.
* Both channel and user cases use the same wire format:
* BATCH <target> +ref draft/multiline [extra]
* BATCH <target> -ref
* where <target> is a channel name or user UID.
* We distinguish channel vs user by checking if parv[1] contains '#'.
*/
if (IsServer(client) || !MyConnect(client))
{
if (parc >= 3 && (parv[2][0] == '+' || parv[2][0] == '-'))
{
int is_channel = (strchr(parv[1], '#') != NULL);
if (parv[2][0] == '+' && parc >= 4 && !strcmp(parv[3], "draft/multiline"))
{
multiline_handle_s2s_batch_open(client, recv_mtags, parc, parv, is_channel);
return;
}
if (parv[2][0] == '-')
{
S2SMultilineBatch *s2s = multiline_find_s2s_batch(client, parv[2] + 1);
if (s2s)
{
multiline_handle_s2s_batch_close(client, parv[2] + 1);
return;
}
}
}
/* Not a multiline batch - fall through to batch.c */
CALL_NEXT_COMMAND_OVERRIDE();
return;
}
/* Not a local user - fall through */
if (!MyUser(client))
{
CALL_NEXT_COMMAND_OVERRIDE();
return;
}
if (parc < 2 || BadPtr(parv[1]))
{
CALL_NEXT_COMMAND_OVERRIDE();
return;
}
ref = parv[1];
/* Opening batch: BATCH +ref draft/multiline <target> */
if (ref[0] == '+')
{
const char *batch_type;
const char *target;
ref++; /* skip '+' */
if (!valid_batch_reference_tag(ref))
{
sendto_one(client, NULL, ":%s FAIL BATCH INVALID_REFTAG %s :Invalid batch reference tag", me.name, ref);
return;
}
if (parc < 4 || BadPtr(parv[2]) || BadPtr(parv[3]))
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_INVALID :Insufficient parameters", me.name);
return;
}
batch_type = parv[2];
target = parv[3];
/* Only handle draft/multiline batch type */
if (strcmp(batch_type, "draft/multiline"))
{
/* Not a multiline batch - fall through */
CALL_NEXT_COMMAND_OVERRIDE();
return;
}
/* Check capabilities */
if (!HasMultiline(client))
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_INVALID :You must negotiate the draft/multiline and batch capabilities", me.name);
return;
}
/* Check for existing open batch */
if (moddata_local_client(client, multiline_md).ptr)
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_INVALID :You already have an open multiline batch", me.name);
return;
}
/* Allocate and initialize batch state */
batch = safe_alloc(sizeof(MultilineBatch));
strlcpy(batch->batch_id, ref, sizeof(batch->batch_id));
/* Parse STATUSMSG prefix from target, modeled on message.c cmd_message() */
{
const char *p2 = strchr(target, '#');
if (p2 && (p2 - target > 0))
{
char prefix_tmp[32];
char prefix;
Channel *channel;
strlncpy(prefix_tmp, target, sizeof(prefix_tmp), p2 - target);
prefix = lowest_ranking_prefix(prefix_tmp);
if (!prefix)
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_INVALID :Invalid prefix", me.name);
safe_free(batch);
return;
}
channel = find_channel(p2);
if (!channel)
{
sendnumeric(client, ERR_NOSUCHNICK, p2);
safe_free(batch);
return;
}
/* STATUSMSG access is checked at delivery time
* (in multiline_deliver_channel) to avoid TOCTOU.
*/
/* Store normalized prefixed target (e.g., "@#channel") */
{
char pfixchan[CHANNELLEN + 4];
snprintf(pfixchan, sizeof(pfixchan), "%c%s", prefix, channel->name);
safe_strdup(batch->target, pfixchan);
}
/* Store member mode */
batch->member_modes[0] = prefix_to_mode(prefix);
batch->member_modes[1] = '\0';
} else {
safe_strdup(batch->target, target);
batch->member_modes[0] = '\0';
}
}
batch->sendtype_set = 0;
batch->line_count = 0;
batch->received_bytes = 0;
batch->start_time = TStime();
batch->lines = NULL;
batch->lines_tail = NULL;
/* Save client tags from the BATCH command (spec: other client tags go here) */
batch->client_mtags = duplicate_mtags_excluding(recv_mtags, "batch");
/* For echo-message + labeled-response interaction: save the label
* and clear the LR context so lr_post_command won't send a
* premature ACK. The label will be placed on the echo batch's
* BATCH open line at delivery time instead.
* Spec: "Servers MUST only include the label tag on the opening
* BATCH command when replying to a client using echo-message."
*/
batch->label[0] = '\0';
if (HasCapability(client, "echo-message") &&
HasCapability(client, "labeled-response"))
{
MessageTag *mt;
for (mt = recv_mtags; mt; mt = mt->next)
{
if (!strcmp(mt->name, "label") && mt->value)
{
strlcpy(batch->label, mt->value, sizeof(batch->label));
labeled_response_set_context(NULL);
break;
}
}
}
moddata_local_client(client, multiline_md).ptr = batch;
return; /* Consumed, don't fall through */
}
/* Closing batch: BATCH -ref */
if (ref[0] == '-')
{
ref++; /* skip '-' */
batch = moddata_local_client(client, multiline_md).ptr;
if (!batch || strcmp(batch->batch_id, ref))
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_INVALID :No matching open batch", me.name);
return;
}
/* If the batch was marked as failed during line processing,
* send the deferred error now and clean up.
*/
if (batch->failed)
{
sendto_one(client, NULL, ":%s %s", me.name, batch->fail_message);
multiline_abort_batch(client, batch);
return;
}
/* Validate: batch must have at least one line */
if (batch->line_count == 0)
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_INVALID :Empty batch", me.name);
multiline_abort_batch(client, batch);
return;
}
/* Validate: batch must not be entirely blank lines */
if (!multiline_batch_has_content(batch))
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_INVALID :Batch consists entirely of blank lines", me.name);
multiline_abort_batch(client, batch);
return;
}
/* Deliver the batch */
multiline_deliver(client, batch);
if (IsDead(client))
return;
/* Apply batch fake lag */
add_fake_lag(client, calculate_multiline_fakelag(client, batch));
/* Clean up */
multiline_free_batch(batch);
moddata_local_client(client, multiline_md).ptr = NULL;
return;
}
/* Not +/- prefixed, fall through */
CALL_NEXT_COMMAND_OVERRIDE();
}
/* ===================== PRIVMSG/NOTICE OVERRIDE ===================== */
CMD_OVERRIDE_FUNC(multiline_override_msg)
{
MultilineBatch *batch;
MessageTag *m;
FloodSettings *f;
const char *batch_ref = NULL;
Channel *channel;
const char *target;
const char *text;
int is_concat = 0;
int text_len;
int add_bytes;
SendType sendtype;
/* Only intercept from local users */
if (!MyUser(client))
{
/* Check for S2S multiline batch line */
if (IsServer(client) || !MyConnect(client))
{
m = find_mtag(recv_mtags, "batch");
if (m && m->value)
{
S2SMultilineBatch *s2s = multiline_find_s2s_batch(client, m->value);
if (s2s)
{
/* Determine sendtype from command name */
sendtype = !strcmp(ovr->command->cmd, "NOTICE") ? SEND_TYPE_NOTICE : SEND_TYPE_PRIVMSG;
multiline_handle_s2s_msg(client, recv_mtags, parc, parv, sendtype);
return;
}
}
}
CALL_NEXT_COMMAND_OVERRIDE();
return;
}
/* Check for @batch= tag */
m = find_mtag(recv_mtags, "batch");
if (!m || !m->value)
{
CALL_NEXT_COMMAND_OVERRIDE();
return;
}
batch_ref = m->value;
/* Look up active batch */
batch = moddata_local_client(client, multiline_md).ptr;
if (!batch || strcmp(batch->batch_id, batch_ref))
{
/* No matching batch - process as normal message */
CALL_NEXT_COMMAND_OVERRIDE();
return;
}
/* If the batch was already marked as failed, silently consume
* remaining lines. The error will be sent at BATCH close.
*/
if (batch->failed)
{
/* Undo fakelag for discarded lines, but only up to
* max-lines total to prevent flood evasion.
*/
FloodSettings *f = get_floodsettings_for_user(client, FLD_MULTILINE);
int max_lines = f->limit[FLD_MULTILINE];
batch->failed++;
if (batch->line_count + batch->failed <= max_lines)
subtract_fake_lag(client, clictx->fake_lag_added_msec);
return;
}
/* We are inside an active multiline batch - buffer this line */
if (parc < 2 || BadPtr(parv[1]))
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_INVALID :Insufficient parameters", me.name);
multiline_abort_batch(client, batch);
return;
}
target = parv[1];
text = (parc >= 3 && parv[2]) ? parv[2] : "";
/* Determine SendType from command name */
sendtype = !strcmp(ovr->command->cmd, "NOTICE") ? SEND_TYPE_NOTICE : SEND_TYPE_PRIVMSG;
/* First line sets sendtype, subsequent must match (no mixing PRIVMSG/NOTICE) */
if (!batch->sendtype_set)
{
batch->sendtype = sendtype;
batch->sendtype_set = 1;
} else if (batch->sendtype != sendtype) {
char buf[512];
snprintf(buf, sizeof(buf), "FAIL BATCH MULTILINE_INVALID :Cannot mix PRIVMSG and NOTICE in a multiline batch");
multiline_fail_batch(client, batch, buf);
subtract_fake_lag(client, clictx->fake_lag_added_msec);
return;
}
/* Validate target matches batch target */
if (strcasecmp(target, batch->target))
{
char buf[512];
snprintf(buf, sizeof(buf), "FAIL BATCH MULTILINE_INVALID_TARGET %s %s :Target mismatch",
batch->target, target);
multiline_fail_batch(client, batch, buf);
subtract_fake_lag(client, clictx->fake_lag_added_msec);
return;
}
/* Check for draft/multiline-concat tag */
if (find_mtag(recv_mtags, "draft/multiline-concat"))
is_concat = 1;
/* Validate: blank line with concat is invalid */
if (is_concat && (!text || !text[0]))
{
multiline_fail_batch(client, batch, "FAIL BATCH MULTILINE_INVALID :Blank line with concat tag");
subtract_fake_lag(client, clictx->fake_lag_added_msec);
return;
}
/* Reject CTCP in multiline batches (including ACTION).
* A batch mixing CTCP and non-CTCP lines has no coherent
* semantics, and even a pure-CTCP batch would behave
* differently for multiline-capable vs fallback recipients.
* Fallback delivery sends each line as an individual PRIVMSG,
* turning one batch into N separate CTCP requests — a flood
* amplification vector triggering auto-replies from each client.
*/
if (text && *text == '\001')
{
multiline_fail_batch(client, batch, "FAIL BATCH MULTILINE_INVALID :CTCP not permitted in multiline batches");
subtract_fake_lag(client, clictx->fake_lag_added_msec);
return;
}
/* Check max-lines and max-bytes (per-group limits) */
if ((f = get_floodsettings_for_user(client, FLD_MULTILINE)))
{
int max_lines = f->limit[FLD_MULTILINE];
int max_bytes = (int)f->period[FLD_MULTILINE];
/* If target is a channel, also respect +f/+F 'm' and 't' limits */
channel = multiline_find_channel(batch->target);
if (channel)
max_lines = MIN(max_lines, get_floodprot_channel_max_lines(channel));
if (batch->line_count >= max_lines)
{
char buf[512];
snprintf(buf, sizeof(buf), "FAIL BATCH MULTILINE_MAX_LINES %d :Too many lines in batch", max_lines);
multiline_fail_batch(client, batch, buf);
subtract_fake_lag(client, clictx->fake_lag_added_msec);
return;
}
/* Check max-bytes (text length + 1 for newline separator, except for concat and first line) */
text_len = text ? strlen(text) : 0;
add_bytes = multiline_calc_add_bytes(text_len, batch->line_count, is_concat);
if (batch->received_bytes + add_bytes > max_bytes)
{
char buf[512];
snprintf(buf, sizeof(buf), "FAIL BATCH MULTILINE_MAX_BYTES %d :Too many bytes in batch", max_bytes);
multiline_fail_batch(client, batch, buf);
subtract_fake_lag(client, clictx->fake_lag_added_msec);
return;
}
batch->received_bytes += add_bytes;
}
/* Permission checks (can_send_to_channel / can_send_to_user) and text
* transformations (+S, +G, etc.) are intentionally NOT done here during
* buffering. They are deferred to delivery time (multiline_deliver_channel
* / multiline_deliver_user) to avoid TOCTOU issues: channel modes, bans,
* or user modes could change between buffering and delivery.
* Raw (untransformed) text is buffered here.
*/
/* Buffer the line */
multiline_append_line(&batch->lines, &batch->lines_tail, &batch->line_count, text, is_concat);
/* Undo the fake lag that parse_addlag() added for this buffered line,
* the batch formula at BATCH close will apply the correct lag instead.
*/
subtract_fake_lag(client, clictx->fake_lag_added_msec);
/* Line buffered - do NOT call CALL_NEXT_COMMAND_OVERRIDE() */
}
/* ===================== BATCH DELIVERY ===================== */
/** Deliver a completed multiline batch */
static void multiline_deliver(Client *client, MultilineBatch *batch)
{
Channel *channel;
Client *target;
const char *text, *errmsg;
char *concat_text;
/* Force labeled-response (like message.c does) */
labeled_response_force = 1;
/* Resolve target (channel, or NULL for user targets / vanished channels) */
channel = multiline_find_channel(batch->target);
if (channel)
{
multiline_deliver_channel(client, batch, channel);
} else if (strchr(batch->target, '#')) {
/* Target had a '#' but channel not found (parted/destroyed during batch) */
sendnumeric(client, ERR_NOSUCHNICK, batch->target);
return;
} else {
target = hash_find_nickatserver(batch->target, NULL);
if (!target)
{
if (SERVICES_NAME)
{
char *server = strchr(batch->target, '@');
if (server && strncasecmp(server + 1, SERVICES_NAME, strlen(SERVICES_NAME)) == 0)
{
sendnumeric(client, ERR_SERVICESDOWN, batch->target);
return;
}
}
sendnumeric(client, ERR_NOSUCHNICK, batch->target);
return;
}
multiline_deliver_user(client, batch, target);
}
}
/** Deliver multiline batch to a channel */
static void multiline_deliver_channel(Client *client, MultilineBatch *batch, Channel *channel)
{
char *concat_text;
const char *cmd = sendtype_to_cmd(batch->sendtype);
MessageTag *mtags = NULL;
int sendflags = SEND_ALL;
MLine *line;
char expanded_modes[64];
const char *filter_modes = NULL;
/* Run permission checks and text transformations at delivery time
* (not during buffering) to avoid TOCTOU: channel modes, bans,
* or membership could change between buffering and batch close.
*/
if (MyUser(client) && !IsULine(client))
{
for (line = batch->lines; line; line = line->next)
{
TextAnalysis ta;
ClientContext delivery_clictx;
const char *check_text;
const char *check_errmsg = NULL;
if (!line->text || !*line->text)
continue; /* blank lines are valid paragraph breaks */
memset(&ta, 0, sizeof(ta));
memset(&delivery_clictx, 0, sizeof(delivery_clictx));
delivery_clictx.textanalysis = &ta;
RunHook(HOOKTYPE_ANALYZE_TEXT, client, line->text, &ta);
check_text = line->text;
if (!can_send_to_channel(client, channel, &check_text, &check_errmsg, batch->sendtype, &delivery_clictx))
return; /* Batch rejected */
/* Apply transformed text (e.g. +S stripped colors, +G censored) */
if (check_text != line->text)
{
safe_free(line->text);
safe_strdup(line->text, check_text);
}
}
/* Re-check STATUSMSG access (user may have lost op/voice during batch) */
if (batch->member_modes[0])
{
if (!op_can_override("channel:override:message:prefix", client, channel, NULL))
{
Membership *lp = find_membership_link(client->user->channel, channel);
if (!lp || !check_channel_access_membership(lp, "vhoaq"))
{
sendnumeric(client, ERR_CHANOPRIVSNEEDED, channel->name);
return;
}
}
}
}
/* Expand member_modes to include equal-or-higher ranks for STATUSMSG filtering */
if (batch->member_modes[0])
{
channel_member_modes_generate_equal_or_greater(batch->member_modes, expanded_modes, sizeof(expanded_modes));
filter_modes = expanded_modes;
}
/* Spamfilter on concatenated text */
if (MyUser(client))
{
int spamtype = (batch->sendtype == SEND_TYPE_NOTICE ? SPAMF_CHANNOTICE : SPAMF_CHANMSG);
concat_text = multiline_concat_text(batch);
if (match_spamfilter(client, concat_text, spamtype, cmd, channel->name, 0, NULL, NULL))
{
safe_free(concat_text);
return;
}
safe_free(concat_text);
}
/* Check channel +f 'p' (paste flood) limit */
if (MyUser(client) && floodprot_check_multiline_batch(channel, client, batch->line_count))
{
sendto_one(client, NULL, ":%s FAIL BATCH MULTILINE_PASTE_LIMIT :Too many paste events in channel (+f)", me.name);
return;
}
/* Generate outgoing message tags */
new_message(client, batch->client_mtags, &mtags);
/* Compute sendflags */
if (batch->lines && batch->lines->text && !strchr(CHANCMDPFX, batch->lines->text[0]))
sendflags |= SKIP_DEAF;
/* Hook: PRE_CHANMSG (per-line, matching single-message behavior) */
for (line = batch->lines; line; line = line->next)
RunHook(HOOKTYPE_PRE_CHANMSG, client, channel, &mtags, line->text, batch->sendtype);
/* Deliver to local channel members */
multiline_deliver_to_local_members(channel, client, batch, mtags,
batch->target, cmd, filter_modes, client, sendflags);
/* Echo-message for sender */
multiline_echo_to_sender(client, batch, mtags, batch->target, cmd);
/* S2S relay */
multiline_send_s2s_channel(client, batch, channel, mtags, cmd, client->direction, batch->target);
multiline_run_chanmsg_hooks(client, channel, sendflags,
batch->member_modes[0] ? batch->member_modes : NULL,
batch->target, mtags, batch->lines, batch->sendtype);
free_message_tags(mtags);
}
/** Deliver multiline batch to a user */
static void multiline_deliver_user(Client *client, MultilineBatch *batch, Client *target)
{
char *concat_text;
const char *cmd = sendtype_to_cmd(batch->sendtype);
MessageTag *mtags = NULL;
/* Run permission checks and text transformations at delivery time
* (not during buffering) to avoid TOCTOU: user modes or target
* identity could change between buffering and batch close.
*/
if (MyUser(client) && !IsULine(client))
{
MLine *line;
for (line = batch->lines; line; line = line->next)
{
TextAnalysis ta;
ClientContext delivery_clictx;
const char *check_text;
const char *check_errmsg = NULL;
if (!line->text || !*line->text)
continue;
memset(&ta, 0, sizeof(ta));
memset(&delivery_clictx, 0, sizeof(delivery_clictx));
delivery_clictx.textanalysis = &ta;
RunHook(HOOKTYPE_ANALYZE_TEXT, client, line->text, &ta);
check_text = line->text;
if (!can_send_to_user(client, target, &check_text, &check_errmsg, batch->sendtype, &delivery_clictx, 0))
return; /* Batch rejected */
if (check_text != line->text)
{
safe_free(line->text);
safe_strdup(line->text, check_text);
}
}
}
/* Spamfilter on concatenated text (not per-line, to catch
* patterns spanning multiple lines and match channel behavior).
*/
if (MyUser(client) && (batch->sendtype != SEND_TYPE_TAGMSG))
{
int spamtype = (batch->sendtype == SEND_TYPE_NOTICE ? SPAMF_USERNOTICE : SPAMF_USERMSG);
concat_text = multiline_concat_text(batch);
if (match_spamfilter(client, concat_text, spamtype, cmd, target->name, 0, NULL, NULL))
{
safe_free(concat_text);
return;
}
safe_free(concat_text);
}
/* Inform sender of away status */
if (batch->sendtype == SEND_TYPE_PRIVMSG && MyConnect(client) && target->user && target->user->away)
sendnumeric(client, RPL_AWAY, target->name, target->user->away);
/* Generate outgoing message tags */
new_message(client, batch->client_mtags, &mtags);
labeled_response_inhibit = 1;
if (MyUser(target))
{
/* Target is local */
if (HasMultiline(target))
{
multiline_send_batch_to_client(target, client, batch, mtags, target->name, cmd);
} else {
multiline_send_fallback_to_client(target, client, batch, mtags, target->name, cmd);
}
} else {
/* Target is remote - relay as S2S batch */
multiline_send_s2s_user(client, batch, target, mtags, cmd);
}
labeled_response_inhibit = 0;
/* Echo-message for sender */
multiline_echo_to_sender(client, batch, mtags, target->name, cmd);
multiline_run_usermsg_hooks(client, target, mtags, batch->lines, batch->sendtype);
free_message_tags(mtags);
}
/** Echo-message: send the multiline batch back to the sender, with label if applicable */
static void multiline_echo_to_sender(Client *client, MultilineBatch *batch,
MessageTag *mtags, const char *targetstr, const char *cmd)
{
if (!MyUser(client) || !HasCapability(client, "echo-message"))
return;
MessageTag *echo_mtags = mtags;
MessageTag *label_mtag = NULL;
/* labeled-response: include label on the echo's BATCH open */
if (batch->label[0])
{
label_mtag = safe_alloc(sizeof(MessageTag));
safe_strdup(label_mtag->name, "label");
safe_strdup(label_mtag->value, batch->label);
AddListItem(label_mtag, mtags);
echo_mtags = mtags;
}
if (HasMultiline(client))
{
multiline_send_batch_to_client(client, client, batch, echo_mtags, targetstr, cmd);
} else {
multiline_send_fallback_to_client(client, client, batch, echo_mtags, targetstr, cmd);
}
if (label_mtag)
DelListItem(label_mtag, mtags);
safe_free_message_tags(label_mtag);
}
/* ===================== SENDING HELPERS ===================== */
/** Send a multiline batch to a single local client that supports draft/multiline.
* Used for user-to-user delivery, echo-message, and S2S incoming.
* For channel broadcast, use multiline_deliver_to_local_members() instead.
*/
static void multiline_send_batch_to_client(Client *to, Client *from, MultilineBatch *batch,
MessageTag *base_mtags, const char *targetstr, const char *cmd)
{
char server_batch_id[BATCHLEN+1];
MLine *l;
MessageTag *m;
generate_batch_id(server_batch_id);
/* Send BATCH open with base_mtags (msgid, time, etc.) */
sendto_prefix_one(to, from, base_mtags,
":%s BATCH +%s draft/multiline %s",
from->name, server_batch_id, targetstr);
/* Send each line */
for (l = batch->lines; l; l = l->next)
{
/* Build line_mtags: base_mtags minus msgid, plus @batch=
* and optionally ;draft/multiline-concat
*/
MessageTag *line_mtags = duplicate_mtags_for_subsequent_lines(base_mtags);
m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, "batch");
safe_strdup(m->value, server_batch_id);
AddListItem(m, line_mtags);
if (l->concat)
{
m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, "draft/multiline-concat");
AddListItem(m, line_mtags);
}
sendto_prefix_one(to, from, line_mtags,
":%s %s %s :%s",
from->name, cmd, targetstr, l->text ? l->text : "");
free_message_tags(line_mtags);
}
/* Send BATCH close */
sendto_prefix_one(to, from, NULL, ":%s BATCH -%s", from->name, server_batch_id);
}
/** Send multiline as individual fallback lines to a single local client.
* Used for user-to-user delivery, echo-message, and S2S incoming.
* For channel broadcast, use multiline_deliver_to_local_members() instead.
*/
static void multiline_send_fallback_to_client(Client *to, Client *from, MultilineBatch *batch,
MessageTag *base_mtags, const char *targetstr, const char *cmd)
{
MLine *l;
MessageTag *rest_mtags;
int first = 1;
/* Build rest_mtags: base_mtags minus msgid.
* Spec: msgid MUST only be on the first line, but other tags
* (time, account, etc.) MAY be on subsequent lines.
*/
rest_mtags = duplicate_mtags_for_subsequent_lines(base_mtags);
/* This client has no multiline support, so just send each line as a
* normal separate message.
*/
for (l = batch->lines; l; l = l->next)
{
/* Spec: "Servers MUST NOT send blank lines to clients
* that have not negotiated the multiline capability."
*/
if (!l->text || !l->text[0])
continue;
if (first)
{
/* First non-blank line gets full mtags (msgid, time, account, etc.) */
sendto_prefix_one(to, from, base_mtags,
":%s %s %s :%s",
from->name, cmd, targetstr, l->text);
first = 0;
} else {
/* Subsequent lines get all tags except msgid */
sendto_prefix_one(to, from, rest_mtags,
":%s %s %s :%s",
from->name, cmd, targetstr, l->text);
}
}
free_message_tags(rest_mtags);
}
/** Deliver multiline batch to local channel members.
* Uses two passes (multiline-capable, then fallback) with pre-built
* message tags. This uses LineCache, which makes this function
* a bit complex but saves A LOT of CPU so is necessary.
* @param skip Client to skip (sender for local delivery, NULL for S2S)
* @param sendflags Send flags (SKIP_DEAF etc.), 0 if not applicable
*/
static void multiline_deliver_to_local_members(Channel *channel, Client *from,
MultilineBatch *batch, MessageTag *base_mtags, const char *targetstr,
const char *cmd, const char *filter_modes, Client *skip, int sendflags)
{
LocalMember *lm;
MLine *l;
MessageTag *m; // temporary for building tag lists
MessageTag *rest_mtags; // base_mtags minus first-only tags (msgid, +draft/reply)
MessageTag *mtags_line; // rest_mtags + batch=<id>, for multiline content lines
MessageTag *mtags_line_concat; // mtags_line + draft/multiline-concat
LineCache *cache_batch_open; // cache for "BATCH +id draft/multiline target"
LineCache *cache_batch_close; // cache for "BATCH -id"
LineCache **cache_lines = NULL; // one cache per multiline content line
LineCache **cache_fb = NULL; // one cache per non-blank fallback line
char server_batch_id[BATCHLEN+1]; // shared across all multiline recipients
int i;
int fallback_count = 0; // number of non-blank fallback lines
int first; // tracks first non-blank line in fallback pass
/* === Phase 1: Pre-build shared resources (once, not per-client) === */
/* Single batch ID shared across all multiline-capable recipients.
* Safe: each client connection tracks batches independently.
*/
generate_batch_id(server_batch_id);
/* rest_mtags: base_mtags minus first-only tags (msgid, +draft/reply).
* Used for subsequent lines in both multiline and fallback paths.
*/
rest_mtags = duplicate_mtags_for_subsequent_lines(base_mtags);
/* mtags_line: rest_mtags + batch=<id>, for non-concat multiline lines */
mtags_line = duplicate_mtags(rest_mtags);
m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, "batch");
safe_strdup(m->value, server_batch_id);
AddListItem(m, mtags_line);
/* mtags_line_concat: mtags_line + draft/multiline-concat */
mtags_line_concat = duplicate_mtags(mtags_line);
m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, "draft/multiline-concat");
AddListItem(m, mtags_line_concat);
/* === Phase 2: Allocate LineCaches === */
/* Multiline path: BATCH open + N content lines + BATCH close */
cache_batch_open = linecache_init();
cache_batch_close = linecache_init();
if (batch->line_count > 0)
cache_lines = safe_alloc(sizeof(LineCache *) * batch->line_count);
for (i = 0; i < batch->line_count; i++)
cache_lines[i] = linecache_init();
/* Fallback path: one cache per non-blank line */
for (l = batch->lines; l; l = l->next)
if (l->text && l->text[0])
fallback_count++;
if (fallback_count > 0)
cache_fb = safe_alloc(sizeof(LineCache *) * fallback_count);
for (i = 0; i < fallback_count; i++)
cache_fb[i] = linecache_init();
/* === Phase 3: Multiline pass === */
for (lm = channel->local_members; lm; lm = lm->next)
{
Client *acptr = lm->ptr->client;
if (acptr == skip)
continue;
if (IsDeaf(acptr) && (sendflags & SKIP_DEAF))
continue;
if (filter_modes && !check_channel_access_member(lm->ptr, filter_modes))
continue;
if (!HasMultiline(acptr))
continue;
/* BATCH open */
sendto_prefix_one_cached(cache_batch_open, 0, acptr, from, base_mtags,
":%s BATCH +%s draft/multiline %s",
from->name, server_batch_id, targetstr);
/* Content lines */
i = 0;
for (l = batch->lines; l; l = l->next, i++)
{
sendto_prefix_one_cached(cache_lines[i], 0, acptr, from,
l->concat ? mtags_line_concat : mtags_line,
":%s %s %s :%s",
from->name, cmd, targetstr, l->text ? l->text : "");
}
/* BATCH close */
sendto_prefix_one_cached(cache_batch_close, 0, acptr, from, NULL,
":%s BATCH -%s", from->name, server_batch_id);
}
/* === Phase 4: Fallback pass === */
for (lm = channel->local_members; lm; lm = lm->next)
{
Client *acptr = lm->ptr->client;
if (acptr == skip)
continue;
if (IsDeaf(acptr) && (sendflags & SKIP_DEAF))
continue;
if (filter_modes && !check_channel_access_member(lm->ptr, filter_modes))
continue;
if (HasMultiline(acptr))
continue;
first = 1;
i = 0;
for (l = batch->lines; l; l = l->next)
{
/* Spec: "Servers MUST NOT send blank lines to clients
* that have not negotiated the multiline capability."
*/
if (!l->text || !l->text[0])
continue;
sendto_prefix_one_cached(cache_fb[i], 0, acptr, from,
first ? base_mtags : rest_mtags,
":%s %s %s :%s",
from->name, cmd, targetstr, l->text);
first = 0;
i++;
}
}
/* === Phase 5: Cleanup === */
linecache_free(cache_batch_open);
linecache_free(cache_batch_close);
for (i = 0; i < batch->line_count; i++)
linecache_free(cache_lines[i]);
safe_free(cache_lines);
for (i = 0; i < fallback_count; i++)
linecache_free(cache_fb[i]);
safe_free(cache_fb);
free_message_tags(rest_mtags);
free_message_tags(mtags_line);
free_message_tags(mtags_line_concat);
}
/** Send a multiline batch (BATCH open + lines + BATCH close) to a single server direction */
static void multiline_send_s2s_to_direction(Client *direction, Client *from,
MultilineBatch *batch, MessageTag *base_mtags, const char *cmd,
const char *targetstr, const char *ref)
{
MLine *l;
int first;
sendto_one(direction, NULL,
":%s BATCH %s +%s draft/multiline",
from->id, targetstr, ref);
first = 1;
for (l = batch->lines; l; l = l->next)
{
MessageTag *line_mtags = NULL;
MessageTag *m;
if (first)
{
/* First line carries all base_mtags (msgid, time, etc.) */
line_mtags = duplicate_mtags(base_mtags);
first = 0;
} else {
/* Subsequent lines carry base_mtags minus msgid */
line_mtags = duplicate_mtags_for_subsequent_lines(base_mtags);
}
m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, "batch");
safe_strdup(m->value, ref);
AddListItem(m, line_mtags);
if (l->concat)
{
m = safe_alloc(sizeof(MessageTag));
safe_strdup(m->name, "draft/multiline-concat");
AddListItem(m, line_mtags);
}
sendto_prefix_one(direction, from, line_mtags,
":%s %s %s :%s",
from->id, cmd, targetstr, l->text ? l->text : "");
free_message_tags(line_mtags);
}
sendto_one(direction, NULL,
":%s BATCH %s -%s",
from->id, targetstr, ref);
}
/** Send multiline batch to remote servers (channel target) */
static void multiline_send_s2s_channel(Client *from, MultilineBatch *batch, Channel *channel,
MessageTag *base_mtags, const char *cmd, Client *skip_direction, const char *targetstr)
{
char ref[BATCHLEN+1];
Member *lp;
Client *acptr;
char expanded_modes[64];
const char *filter_modes = NULL;
generate_batch_id(ref);
/* Expand member_modes for STATUSMSG filtering */
if (batch->member_modes[0])
{
channel_member_modes_generate_equal_or_greater(batch->member_modes, expanded_modes, sizeof(expanded_modes));
filter_modes = expanded_modes;
}
/* Track which server directions we've already sent to */
++current_serial;
for (lp = channel->members; lp; lp = lp->next)
{
acptr = lp->client;
if (MyUser(acptr))
continue; /* Already handled locally */
if (acptr->direction == skip_direction)
continue;
/* STATUSMSG filtering: only relay to directions with matching members */
if (filter_modes && !check_channel_access_member(lp, filter_modes))
continue;
if (acptr->direction->local->serial == current_serial)
continue; /* Already sent to this direction */
acptr->direction->local->serial = current_serial;
multiline_send_s2s_to_direction(acptr->direction, from, batch, base_mtags, cmd, targetstr, ref);
}
/* Also handle broadcast-channel-messages for +H channels etc. */
if ((iConf.broadcast_channel_messages == BROADCAST_CHANNEL_MESSAGES_ALWAYS) ||
((iConf.broadcast_channel_messages == BROADCAST_CHANNEL_MESSAGES_AUTO) && has_channel_mode(channel, 'H')))
{
list_for_each_entry(acptr, &server_list, special_node)
{
if (acptr->direction == skip_direction)
continue;
if (acptr->direction->local->serial == current_serial)
continue;
acptr->direction->local->serial = current_serial;
multiline_send_s2s_to_direction(acptr->direction, from, batch, base_mtags, cmd, targetstr, ref);
}
}
}
/** Send multiline batch to remote server (user-to-user target) */
static void multiline_send_s2s_user(Client *from, MultilineBatch *batch, Client *target,
MessageTag *base_mtags, const char *cmd)
{
char ref[BATCHLEN+1];
generate_batch_id(ref);
multiline_send_s2s_to_direction(target->direction, from, batch, base_mtags, cmd, target->id, ref);
}
/* ===================== S2S RECEIVING ===================== */
/** Find an active S2S batch by sender and batch_id */
static S2SMultilineBatch *multiline_find_s2s_batch(Client *sender, const char *batch_id)
{
S2SMultilineBatch *s2s;
for (s2s = s2s_batches; s2s; s2s = s2s->next)
{
if (s2s->sender == sender && !strcmp(s2s->batch_id, batch_id))
return s2s;
}
return NULL;
}
/** Free an S2S batch and remove from the global list */
static void multiline_free_s2s_batch(S2SMultilineBatch *s2s)
{
if (!s2s)
return;
DelListItem(s2s, s2s_batches);
safe_free(s2s->target);
free_message_tags(s2s->first_mtags);
multiline_free_lines(s2s->lines);
safe_free(s2s);
}
/** Free all S2S batches (called when module is permanently removed) */
static void multiline_free_s2s_batches_all(ModData *m)
{
S2SMultilineBatch *s2s, *s2s_next;
if (!m->ptr)
return;
s2s_batches = m->ptr;
for (s2s = s2s_batches; s2s; s2s = s2s_next)
{
s2s_next = s2s->next;
multiline_free_s2s_batch(s2s);
}
s2s_batches = NULL;
m->ptr = NULL;
}
/** Handle an incoming S2S multiline batch open */
static void multiline_handle_s2s_batch_open(Client *client, MessageTag *recv_mtags,
int parc, const char *parv[], int is_channel)
{
S2SMultilineBatch *s2s;
const char *ref;
const char *target;
/* BATCH <target> +ref draft/multiline */
target = parv[1];
ref = parv[2] + 1; /* skip '+' */
/* Check for duplicate */
if (multiline_find_s2s_batch(client, ref))
return; /* Duplicate, ignore */
s2s = safe_alloc(sizeof(S2SMultilineBatch));
strlcpy(s2s->batch_id, ref, sizeof(s2s->batch_id));
safe_strdup(s2s->target, target);
s2s->is_channel = is_channel;
/* Parse STATUSMSG prefix from target (e.g., "@#channel") */
s2s->member_modes[0] = '\0';
if (is_channel)
{
const char *p2 = strchr(target, '#');
if (p2 && (p2 - target > 0))
{
char prefix_tmp[32];
char prefix;
strlncpy(prefix_tmp, target, sizeof(prefix_tmp), p2 - target);
prefix = lowest_ranking_prefix(prefix_tmp);
if (prefix)
{
s2s->member_modes[0] = prefix_to_mode(prefix);
s2s->member_modes[1] = '\0';
}
}
}
s2s->sender = client;
s2s->direction = client->direction;
s2s->sendtype_set = 0;
s2s->line_count = 0;
s2s->received_bytes = 0;
s2s->start_time = TStime();
s2s->lines = NULL;
s2s->lines_tail = NULL;
s2s->first_mtags = NULL;
/* Save message tags from the BATCH open (for relay) */
s2s->first_mtags = duplicate_mtags_excluding(recv_mtags, "batch");
AddListItem(s2s, s2s_batches);
}
/** Handle a line within an S2S multiline batch */
static void multiline_handle_s2s_msg(Client *client, MessageTag *recv_mtags,
int parc, const char *parv[], SendType sendtype)
{
MessageTag *m;
S2SMultilineBatch *s2s;
const char *text;
int is_concat = 0;
m = find_mtag(recv_mtags, "batch");
if (!m || !m->value)
return;
s2s = multiline_find_s2s_batch(client, m->value);
if (!s2s)
return;
/* Set or validate sendtype */
if (!s2s->sendtype_set)
{
s2s->sendtype = sendtype;
s2s->sendtype_set = 1;
} else if (s2s->sendtype != sendtype) {
/* Mismatch - discard batch */
multiline_free_s2s_batch(s2s);
return;
}
text = (parc >= 3 && parv[2]) ? parv[2] : "";
/* Check for concat tag */
if (find_mtag(recv_mtags, "draft/multiline-concat"))
is_concat = 1;
/* Check limits (use hardcoded max, the originating server already enforces per-user limits) */
{
int add_bytes = multiline_calc_add_bytes(strlen(text), s2s->line_count, is_concat);
if ((s2s->line_count >= MULTILINE_MAX_CONFIGURABLE_LINES) ||
(s2s->received_bytes + add_bytes > MULTILINE_MAX_CONFIGURABLE_BYTES))
{
/* Over limit - discard batch */
multiline_free_s2s_batch(s2s);
return;
}
s2s->received_bytes += add_bytes;
}
/* Save first line's mtags for relay (msgid, time, etc.) */
if (s2s->line_count == 0 && !s2s->first_mtags)
{
MessageTag *mtag_iter;
for (mtag_iter = recv_mtags; mtag_iter; mtag_iter = mtag_iter->next)
{
if (!strcmp(mtag_iter->name, "batch"))
continue;
if (!strcmp(mtag_iter->name, "draft/multiline-concat"))
continue;
MessageTag *dup = duplicate_mtag(mtag_iter);
AddListItem(dup, s2s->first_mtags);
}
}
/* Buffer the line */
multiline_append_line(&s2s->lines, &s2s->lines_tail, &s2s->line_count, text, is_concat);
}
/** Handle an S2S multiline batch close */
static void multiline_handle_s2s_batch_close(Client *client, const char *batch_id)
{
S2SMultilineBatch *s2s = multiline_find_s2s_batch(client, batch_id);
if (!s2s)
return;
if (s2s->line_count == 0)
{
multiline_free_s2s_batch(s2s);
return;
}
multiline_deliver_s2s_batch(s2s);
multiline_free_s2s_batch(s2s);
}
/** Deliver a completed S2S multiline batch to local members and relay further */
static void multiline_deliver_s2s_batch(S2SMultilineBatch *s2s)
{
const char *cmd = sendtype_to_cmd(s2s->sendtype);
MessageTag *mtags = NULL;
MultilineBatch *batch = safe_alloc(sizeof(MultilineBatch));
/* Build a temporary MultilineBatch struct for the sending helpers */
strlcpy(batch->batch_id, s2s->batch_id, sizeof(batch->batch_id));
batch->target = s2s->target;
batch->sendtype = s2s->sendtype;
strlcpy(batch->member_modes, s2s->member_modes, sizeof(batch->member_modes));
batch->lines = s2s->lines;
batch->lines_tail = s2s->lines_tail;
batch->line_count = s2s->line_count;
batch->received_bytes = s2s->received_bytes;
if (s2s->is_channel)
{
Channel *channel = multiline_find_channel(s2s->target);
char expanded_modes[64];
const char *filter_modes = NULL;
if (channel)
{
/* Expand member_modes for STATUSMSG filtering */
if (s2s->member_modes[0])
{
channel_member_modes_generate_equal_or_greater(s2s->member_modes, expanded_modes, sizeof(expanded_modes));
filter_modes = expanded_modes;
}
/* Generate outgoing mtags from the first line's tags */
new_message(s2s->sender, s2s->first_mtags, &mtags);
/* Deliver to local members */
multiline_deliver_to_local_members(channel, s2s->sender, batch, mtags,
s2s->target, cmd, filter_modes, NULL, 0);
/* Relay to further server directions (excluding where it came from) */
multiline_send_s2s_channel(s2s->sender, batch, channel, mtags, cmd, s2s->direction, s2s->target);
multiline_run_chanmsg_hooks(s2s->sender, channel, SEND_ALL,
s2s->member_modes[0] ? s2s->member_modes : NULL,
s2s->target, mtags, s2s->lines, s2s->sendtype);
}
} else {
/* User-to-user */
Client *target = find_client(s2s->target, NULL);
if (target)
{
new_message(s2s->sender, s2s->first_mtags, &mtags);
if (MyUser(target))
{
if (HasMultiline(target))
multiline_send_batch_to_client(target, s2s->sender, batch, mtags, target->name, cmd);
else
multiline_send_fallback_to_client(target, s2s->sender, batch, mtags, target->name, cmd);
} else {
/* Relay further */
multiline_send_s2s_user(s2s->sender, batch, target, mtags, cmd);
}
multiline_run_usermsg_hooks(s2s->sender, target, mtags, s2s->lines, s2s->sendtype);
}
}
/* Clean up - NULL out borrowed pointers before freeing */
batch->target = NULL;
batch->lines = NULL;
batch->lines_tail = NULL;
multiline_free_batch(batch);
free_message_tags(mtags);
}
/* ===================== TIMEOUT AND CLEANUP ===================== */
/** Periodic timer: check for timed-out multiline batches */
EVENT(multiline_timeout_check)
{
Client *client;
S2SMultilineBatch *s2s, *s2s_next;
/* Check local client batches */
list_for_each_entry(client, &lclient_list, lclient_node)
{
MultilineBatch *batch;
if (!IsUser(client))
continue;
batch = moddata_local_client(client, multiline_md).ptr;
if (batch && (TStime() - batch->start_time > cfg.batch_timeout))
{
sendto_one(client, NULL, ":%s FAIL BATCH TIMEOUT %s :Batch timed out", me.name, batch->batch_id);
multiline_abort_batch(client, batch);
}
}
/* Check S2S batches */
for (s2s = s2s_batches; s2s; s2s = s2s_next)
{
s2s_next = s2s->next;
if (TStime() - s2s->start_time > cfg.batch_timeout)
{
multiline_free_s2s_batch(s2s);
}
}
}
/** Hook: local client disconnecting */
int multiline_close_connection(Client *client)
{
/* ModData free callback handles cleanup, but we might need
* additional cleanup here in the future.
*/
return 0;
}
/** Hook: remote user quitting */
int multiline_remote_quit(Client *client, MessageTag *mtags, const char *comment)
{
S2SMultilineBatch *s2s, *s2s_next;
for (s2s = s2s_batches; s2s; s2s = s2s_next)
{
s2s_next = s2s->next;
if (s2s->sender == client)
multiline_free_s2s_batch(s2s);
}
return 0;
}
/** Hook: server disconnecting */
int multiline_server_quit(Client *client, MessageTag *mtags)
{
S2SMultilineBatch *s2s, *s2s_next;
for (s2s = s2s_batches; s2s; s2s = s2s_next)
{
s2s_next = s2s->next;
if (s2s->direction == client)
multiline_free_s2s_batch(s2s);
}
return 0;
}