mirror of
https://github.com/unrealircd/unrealircd.git
synced 2026-07-06 15:13:12 +02:00
2016 lines
61 KiB
C
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;
|
|
}
|