From 1bb01d8fb3efa15bc0a39e99fb7d7339ae0b10b0 Mon Sep 17 00:00:00 2001 From: WarPigs1602 Date: Wed, 19 Nov 2025 13:00:39 +0100 Subject: [PATCH] Added message tags again, typing indicator works now correctly --- include/capab.h | 3 +- include/client.h | 10 ++ include/handlers.h | 3 + include/ircd_features.h | 3 + include/msg.h | 4 + include/send.h | 7 ++ ircd/Makefile.in | 1 + ircd/ircd_features.c | 3 + ircd/m_tagmsg.c | 269 ++++++++++++++++++++++++++++++++++++++++ ircd/parse.c | 42 ++++++- ircd/send.c | 70 +++++++++++ 11 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 ircd/m_tagmsg.c diff --git a/include/capab.h b/include/capab.h index 972fd4cd..c4246e21 100644 --- a/include/capab.h +++ b/include/capab.h @@ -42,7 +42,8 @@ _CAP(CHGHOST, FEAT_CAP_CHGHOST, 0, "chghost"), \ _CAP(ECHOMESSAGE, FEAT_CAP_ECHOMESSAGE, 0, "echo-message"), \ _CAP(EXTJOIN, FEAT_CAP_EXTJOIN, 0, "extended-join"), \ - _CAP(INVITENOTIFY, FEAT_CAP_INVITENOTIFY, 0, "invite-notify") + _CAP(INVITENOTIFY, FEAT_CAP_INVITENOTIFY, 0, "invite-notify"), \ + _CAP(MESSAGETAGS, FEAT_CAP_MESSAGETAGS, 0, "message-tags") /** Client capabilities, counting by index. */ enum Capab { diff --git a/include/client.h b/include/client.h index 92e4816c..d255e112 100644 --- a/include/client.h +++ b/include/client.h @@ -202,6 +202,8 @@ struct Connection time_t con_nexttarget;/**< Next time a target change is allowed */ time_t con_lasttime; /**< Last time data read from socket */ time_t con_since; /**< Last time we accepted a command */ + time_t con_tagmsg_window; /**< Window start (seconds) for TAGMSG rate */ + unsigned int con_tagmsg_count; /**< TAGMSGs seen in current window */ struct MsgQ con_sendQ; /**< Outgoing message queue */ struct DBuf con_recvQ; /**< Incoming data yet to be parsed */ unsigned int con_sendM; /**< Stats: protocol messages sent */ @@ -339,6 +341,10 @@ struct Client { #define cli_nextnick(cli) con_nextnick(cli_connect(cli)) /** Get next time a target change is allowed for the client. */ #define cli_nexttarget(cli) con_nexttarget(cli_connect(cli)) +/** Get TAGMSG window start time for the client. */ +#define cli_tagmsg_window(cli) con_tagmsg_window(cli_connect(cli)) +/** Get TAGMSG count in current window for the client. */ +#define cli_tagmsg_count(cli) con_tagmsg_count(cli_connect(cli)) /** Get SendQ for client. */ #define cli_sendQ(cli) con_sendQ(cli_connect(cli)) /** Get RecvQ for client. */ @@ -418,6 +424,10 @@ struct Client { #define con_lasttime(con) ((con)->con_lasttime) /** Get last time we accepted a command from the connection. */ #define con_since(con) ((con)->con_since) +/** Get TAGMSG window start time for the connection. */ +#define con_tagmsg_window(con) ((con)->con_tagmsg_window) +/** Get TAGMSG count for the connection. */ +#define con_tagmsg_count(con) ((con)->con_tagmsg_count) /** Get SendQ for connection. */ #define con_sendQ(con) ((con)->con_sendQ) /** Get RecvQ for connection. */ diff --git a/include/handlers.h b/include/handlers.h index f8bc6073..64549b7b 100644 --- a/include/handlers.h +++ b/include/handlers.h @@ -126,6 +126,7 @@ extern int m_quit(struct Client*, struct Client*, int, char*[]); extern int m_registered(struct Client*, struct Client*, int, char*[]); extern int m_silence(struct Client*, struct Client*, int, char*[]); extern int m_stats(struct Client*, struct Client*, int, char*[]); +extern int m_tagmsg(struct Client*, struct Client*, int, char*[]); extern int m_time(struct Client*, struct Client*, int, char*[]); extern int m_topic(struct Client*, struct Client*, int, char*[]); extern int m_trace(struct Client*, struct Client*, int, char*[]); @@ -217,6 +218,7 @@ extern int ms_settime(struct Client*, struct Client*, int, char*[]); extern int ms_silence(struct Client*, struct Client*, int, char*[]); extern int ms_squit(struct Client*, struct Client*, int, char*[]); extern int ms_stats(struct Client*, struct Client*, int, char*[]); +extern int ms_tagmsg(struct Client*, struct Client*, int, char*[]); extern int ms_topic(struct Client*, struct Client*, int, char*[]); extern int ms_trace(struct Client*, struct Client*, int, char*[]); extern int ms_uping(struct Client*, struct Client*, int, char*[]); @@ -227,6 +229,7 @@ extern int ms_wallvoices(struct Client*, struct Client*, int, char*[]); extern int ms_whois(struct Client*, struct Client*, int, char*[]); extern int ms_xquery(struct Client*, struct Client*, int, char*[]); extern int ms_xreply(struct Client*, struct Client*, int, char*[]); +extern int mu_tagmsg(struct Client*, struct Client*, int, char*[]); #endif /* INCLUDED_handlers_h */ diff --git a/include/ircd_features.h b/include/ircd_features.h index 10167947..2a938b0b 100644 --- a/include/ircd_features.h +++ b/include/ircd_features.h @@ -111,6 +111,9 @@ enum Feature { FEAT_CAP_ECHOMESSAGE, FEAT_CAP_EXTJOIN, FEAT_CAP_INVITENOTIFY, + FEAT_CAP_MESSAGETAGS, + FEAT_TAGMSG_MAX_PER_WINDOW, + FEAT_TAGMSG_WINDOW_SECONDS, /* HEAD_IN_SAND Features */ FEAT_HIS_SNOTICES, diff --git a/include/msg.h b/include/msg.h index 2fba1e79..667a3f2e 100644 --- a/include/msg.h +++ b/include/msg.h @@ -376,6 +376,10 @@ struct Client; #define TOK_CHGHOST "CHGHOST" #define CMD_CHGHOST MSG_CHGHOST, TOK_CHGHOST +#define MSG_TAGMSG "TAGMSG" +#define TOK_TAGMSG "TAGMSG" +#define CMD_TAGMSG MSG_TAGMSG, TOK_TAGMSG + /* * Constants */ diff --git a/include/send.h b/include/send.h index 0bbbda83..4c52103e 100644 --- a/include/send.h +++ b/include/send.h @@ -106,6 +106,13 @@ extern void sendcmdto_channel_butone(struct Client *from, const char *cmd, struct Client *one, unsigned int skip, const char *pattern, ...); +/* Send TAGMSG to channel users with message-tags capability */ +extern void sendcmdto_channel_tagmsg(struct Client *from, struct Channel *to, + struct Client *one, const char *tags); + +/* Send TAGMSG to private user with message-tags capability */ +extern void sendcmdto_user_tagmsg(struct Client *from, struct Client *to, + struct Client *one, const char *tags); /* Send JOIN to all local channel users matching or not matching capability flags */ extern void sendjointo_channel_butserv(struct Client *from, diff --git a/ircd/Makefile.in b/ircd/Makefile.in index 432c66e2..cbcb8b59 100644 --- a/ircd/Makefile.in +++ b/ircd/Makefile.in @@ -170,6 +170,7 @@ IRCD_SRC = \ m_silence.c \ m_squit.c \ m_stats.c \ + m_tagmsg.c \ m_time.c \ m_topic.c \ m_trace.c \ diff --git a/ircd/ircd_features.c b/ircd/ircd_features.c index 6410099b..ded9602b 100644 --- a/ircd/ircd_features.c +++ b/ircd/ircd_features.c @@ -376,6 +376,9 @@ static struct FeatureDesc { F_B(CAP_ECHOMESSAGE, 0, 1, 0), F_B(CAP_EXTJOIN, 0, 1, 0), F_B(CAP_INVITENOTIFY, 0, 1, 0), + F_B(CAP_MESSAGETAGS, 0, 1, 0), + F_I(TAGMSG_MAX_PER_WINDOW, 0, 10, 0), + F_I(TAGMSG_WINDOW_SECONDS, 0, 1, 0), /* HEAD_IN_SAND Features */ F_B(HIS_SNOTICES, 0, 1, 0), diff --git a/ircd/m_tagmsg.c b/ircd/m_tagmsg.c new file mode 100644 index 00000000..25745222 --- /dev/null +++ b/ircd/m_tagmsg.c @@ -0,0 +1,269 @@ +/* + * IRC - Internet Relay Chat, ircd/m_tagmsg.c + * Copyright (C) 1990 Jarkko Oikarinen and + * University of Oulu, Computing Center + * + * 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. + * + * $Id$ + */ + +/** @file + * @brief Handlers for TAGMSG command (IRCv3 message-tags). + * @version $Id$ + */ + +#include "config.h" + +#include "capab.h" +#include "channel.h" +#include "client.h" +#include "hash.h" +#include "ircd.h" +#include "ircd_chattr.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_string.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "s_user.h" +#include "send.h" + +/* #include -- Now using assert in ircd_log.h */ +#include + +/* Forward declaration */ +int ms_tagmsg(struct Client* cptr, struct Client* sptr, int parc, char* parv[]); + +/* Limits (conservative) */ +#define TAGMSG_LINE_TAGS_MAX 1024 /**< total length of tag segment */ +#define TAGMSG_KEY_MAX 64 /**< max length of a tag key */ +#define TAGMSG_VALUE_MAX 256 /**< max length of a tag value */ +#define TAGMSG_COUNT_MAX 64 /**< max number of tags */ + +/** Check if character is valid in a tag key. + * @param[in] c Character to check. + * @return Non-zero if valid, zero otherwise. + */ +static int valid_tag_key_char(int c) +{ + return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '/' || c == '+' || c == '.'); +} + +/** Validate and normalize tag string. + * Tags are modified in-place to decode escape sequences. + * @param[in,out] tags Tag string to validate (modified in-place). + * @return 0 on success, -1 on validation error. + */ +static int validate_and_normalize_tags(char *tags) +{ + if (!tags || *tags != '@') + return 0; /* no tag prefix */ + + char *p = tags + 1; /* skip leading '@' */ + int tag_count = 0; + + while (*p) { + if (tag_count++ >= TAGMSG_COUNT_MAX) + return -1; + + char *key_start = p; + while (*p && *p != '=' && *p != ';') { + if (!valid_tag_key_char((unsigned char)*p)) + return -1; + if ((p - key_start) >= TAGMSG_KEY_MAX) + return -1; + p++; + } + + if (*p == '=') { + p++; /* value start */ + char *val_start = p; + char *write = p; /* decode escape sequences */ + + while (*p && *p != ';') { + if (*p == '\\') { /* escape */ + p++; + if (*p == ':' || *p == 's' || *p == 'r' || *p == 'n' || *p == '\\') { + /* Decode IRCv3 tag escapes */ + switch (*p) { + case ':': *write++ = ';'; break; + case 's': *write++ = ' '; break; + case 'r': *write++ = '\r'; break; + case 'n': *write++ = '\n'; break; + case '\\': *write++ = '\\'; break; + } + p++; + } else if (*p) { + /* unknown escape, keep char if present */ + *write++ = *p++; + } + } else { + *write++ = *p++; + } + + if ((write - val_start) >= TAGMSG_VALUE_MAX) + return -1; + } + + *write = '\0'; + p = (*p == ';') ? p + 1 : p; /* skip separator */ + } else if (*p == ';') { + /* key-only tag */ + p++; + } else { + /* end of string after a key-only tag */ + break; + } + } + + return 0; +} + +/** Handle TAGMSG from local clients. + * @param[in] cptr Client that sent the command. + * @param[in] sptr Original source of the command. + * @param[in] parc Number of parameters. + * @param[in] parv Parameter array. parv[1] = target + */ +int m_tagmsg(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + char *tags; + struct Channel *chptr; + struct Client *acptr; + + /* After parser shift: parv[1]=tags, parv[2]=target */ + if (parc < 3 || EmptyString(parv[1]) || EmptyString(parv[2])) + return need_more_params(sptr, "TAGMSG"); + + /* Check if capability is negotiated */ + if (!feature_bool(FEAT_CAP_MESSAGETAGS) || !CapActive(sptr, CAP_MESSAGETAGS)) + return 0; + + /* Get tags from parv[1] (inserted by parser) */ + tags = parv[1]; + + /* Validate tag length */ + if (strlen(tags) > TAGMSG_LINE_TAGS_MAX) + return 0; /* silently drop oversized tag block */ + + /* Validate and normalize tags */ + if (validate_and_normalize_tags(tags) != 0) + return 0; /* invalid tag syntax */ + + /* Apply per-client TAGMSG rate limiting for local users */ + if (MyUser(sptr)) { + time_t now = CurrentTime; + int win = feature_int(FEAT_TAGMSG_WINDOW_SECONDS); + int max = feature_int(FEAT_TAGMSG_MAX_PER_WINDOW); + + if (win < 1) win = 1; /* safety */ + if (max < 1) max = 1; + + if (cli_tagmsg_window(sptr) + win > now) { + /* same window */ + if (cli_tagmsg_count(sptr) + 1 > (unsigned int)max) { + /* Silently drop excess TAGMSG */ + return 0; + } + cli_tagmsg_count(sptr)++; + } else { + /* start a new window */ + cli_tagmsg_window(sptr) = now; + cli_tagmsg_count(sptr) = 1; + } + } + + /* Find target (now in parv[2] after parser shift) */ + if (IsChannelName(parv[2])) { + if (!(chptr = FindChannel(parv[2]))) + return 0; /* No such channel */ + + /* Check permissions */ + if (!client_can_send_to_channel(sptr, chptr, 0)) + return 0; + + /* Send to channel members with message-tags capability */ + sendcmdto_channel_tagmsg(sptr, chptr, cptr, tags); + } else { + if (!(acptr = FindUser(parv[2]))) + return 0; /* No such nick */ + + /* Check if silenced */ + if (is_silenced(sptr, acptr)) + return 0; + + /* Send to user if they have message-tags capability */ + sendcmdto_user_tagmsg(sptr, acptr, cptr, tags); + } + + return 0; +} + +/** Handle TAGMSG from unregistered connections. + * @param[in] cptr Client that sent the command. + * @param[in] sptr Original source of the command. + * @param[in] parc Number of parameters. + * @param[in] parv Parameter array. + */ +int mu_tagmsg(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + /* If this is a server in handshake state, route through server handler */ + if (IsHandshake(cptr) || IsServer(cptr)) + return ms_tagmsg(cptr, sptr, parc, parv); + + /* Unregistered clients cannot send TAGMSG */ + return send_reply(sptr, ERR_NOTREGISTERED); +} + +/** Handle TAGMSG from servers. + * @param[in] cptr Client that sent the command. + * @param[in] sptr Original source of the command. + * @param[in] parc Number of parameters. + * @param[in] parv Parameter array. parv[1] = target + */ +int ms_tagmsg(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + struct Channel *chptr; + struct Client *acptr; + char *tags; + + /* After parser shift: parv[1]=tags, parv[2]=target */ + if (parc < 3) + return need_more_params(sptr, "TAGMSG"); + + if (!feature_bool(FEAT_CAP_MESSAGETAGS)) + return 0; + + /* Trust upstream server validation */ + tags = parv[1]; + + /* Find target and forward */ + if (IsChannelName(parv[2])) { + if ((chptr = FindChannel(parv[2]))) + sendcmdto_channel_tagmsg(sptr, chptr, cptr, tags); + } else { + if ((acptr = FindUser(parv[2]))) + sendcmdto_user_tagmsg(sptr, acptr, cptr, tags); + } + + return 0; +} diff --git a/ircd/parse.c b/ircd/parse.c index cfc78ca3..69735843 100644 --- a/ircd/parse.c +++ b/ircd/parse.c @@ -645,6 +645,13 @@ struct Message msgtab[] = { /* UNREG, CLIENT, SERVER, OPER, SERVICE */ { m_cap, m_cap, m_ignore, m_cap, m_ignore } }, + { + MSG_TAGMSG, + TOK_TAGMSG, + 0, MAXPARA, 0, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { mu_tagmsg, m_tagmsg, ms_tagmsg, m_tagmsg, m_ignore } + }, /* This command is an alias for QUIT during the unregistered part of * of the server. This is because someone jumping via a broken web * proxy will send a 'POST' as their first command - which we will @@ -827,6 +834,7 @@ parse_client(struct Client *cptr, char *buffer, char *bufend) struct Client* from = cptr; char* ch; char* s; + char* tags = NULL; int i; int paramcount; struct Message* mptr; @@ -838,7 +846,22 @@ parse_client(struct Client *cptr, char *buffer, char *bufend) return 0; para[0] = cli_name(from); + + /* Check for message tags (IRCv3) */ for (ch = buffer; *ch == ' '; ch++); /* Eat leading spaces */ + if (*ch == '@') + { + /* Extract tags */ + tags = ch; + for (++ch; *ch && *ch != ' '; ++ch) + ; /* Find end of tag block */ + if (*ch == ' ') + { + *ch = '\0'; /* Null-terminate the tag block */ + for (++ch; *ch == ' '; ch++); /* Eat spaces after tags */ + } + } + if (*ch == ':') /* Is any client doing this ? */ { for (++ch; *ch && *ch != ' '; ++ch) @@ -884,8 +907,12 @@ parse_client(struct Client *cptr, char *buffer, char *bufend) paramcount = mptr->parameters; i = bufend - ((s) ? s : ch); mptr->bytes += i; - if ((mptr->flags & MFLG_SLOW) || !IsAnOper(cptr)) - cli_since(cptr) += (2 + i / 120); + + /* TAGMSG is exempt from penalties - it has its own rate limiting */ + if (!(mptr->cmd && 0 == ircd_strcmp(mptr->cmd, MSG_TAGMSG))) { + if ((mptr->flags & MFLG_SLOW) || !IsAnOper(cptr)) + cli_since(cptr) += (2 + i / 120); + } /* * Allow only 1 msg per 2 seconds * (on average) to prevent dumping. @@ -942,6 +969,17 @@ parse_client(struct Client *cptr, char *buffer, char *bufend) for (; *s != ' ' && *s; s++); } } + + /* If this is TAGMSG and we had a tags prefix, insert it as first parameter */ + if (tags && mptr && mptr->cmd && 0 == ircd_strcmp(mptr->cmd, MSG_TAGMSG)) { + int j; + /* shift parameters up by one: para[1..i] -> para[2..i+1] */ + for (j = i; j >= 1; --j) + para[j+1] = para[j]; + para[1] = tags; + ++i; + } + para[++i] = NULL; ++mptr->count; diff --git a/ircd/send.c b/ircd/send.c index 9dbd2921..13039435 100644 --- a/ircd/send.c +++ b/ircd/send.c @@ -998,3 +998,73 @@ void vsendto_opmask_butone(struct Client *one, unsigned int mask, msgq_clean(mb); } + +/** Send TAGMSG to channel users with message-tags capability. + * @param[in] from Source of the TAGMSG. + * @param[in] to Target channel. + * @param[in] one Client direction to skip (or NULL). + * @param[in] tags Tag string to send. + */ +void sendcmdto_channel_tagmsg(struct Client *from, struct Channel *to, + struct Client *one, const char *tags) +{ + struct Membership *member; + struct MsgBuf *user_mb; + struct MsgBuf *serv_mb; + + /* Build buffer to send to users */ + user_mb = msgq_make(0, "%s %:#C %s %H", tags ? tags : "", from, MSG_TAGMSG, to); + + /* Build buffer to send to servers */ + serv_mb = msgq_make(&me, "%s %:#C %s %H", tags ? tags : "", from, TOK_TAGMSG, to); + + /* send buffer along! */ + bump_sentalong(one); + for (member = to->members; member; member = member->next_member) { + /* skip zombies and users without the capability */ + if (IsZombie(member) || + cli_fd(cli_from(member->user)) < 0 || + cli_sentalong(member->user) == sentalong_marker) + continue; + + cli_sentalong(member->user) = sentalong_marker; + + /* Only send to users with message-tags capability */ + if (MyConnect(member->user)) { + if (CapActive(member->user, CAP_MESSAGETAGS)) + send_buffer(member->user, user_mb, 0); + } else { + send_buffer(member->user, serv_mb, 0); + } + } + + msgq_clean(user_mb); + msgq_clean(serv_mb); +} + +/** Send TAGMSG to private user with message-tags capability. + * @param[in] from Source of the TAGMSG. + * @param[in] to Target user. + * @param[in] one Client direction to skip (or NULL). + * @param[in] tags Tag string to send. + */ +void sendcmdto_user_tagmsg(struct Client *from, struct Client *to, + struct Client *one, const char *tags) +{ + if (MyConnect(to)) { + /* Only send to local users with message-tags capability */ + if (CapActive(to, CAP_MESSAGETAGS)) { + struct MsgBuf *mb = msgq_make(0, "%s %:#C %s %C", + tags ? tags : "", from, MSG_TAGMSG, to); + send_buffer(to, mb, 0); + msgq_clean(mb); + } + } else { + /* Forward to remote server */ + struct MsgBuf *mb = msgq_make(&me, "%s %:#C %s %C", + tags ? tags : "", from, TOK_TAGMSG, to); + send_buffer(to, mb, 0); + msgq_clean(mb); + } +} +