Implement message-tags spec

Message tags were supported already, but not arbitrary tags, and not
TAGMSG
This commit is contained in:
Alexey Sokolov
2025-04-11 21:19:24 +01:00
parent df6a5073ff
commit f93829e3b3
15 changed files with 271 additions and 24 deletions
+4
View File
@@ -138,6 +138,8 @@ class CClient : public CIRCSocket {
void SetPlaybackActive(bool bActive) { m_bPlaybackActive = bActive; }
void PutIRC(const CString& sLine);
// Strips prefix and potentially tags before sending to server.
void PutIRCStripping(CMessage Message);
/** Sends a raw data line to the client.
* @param sLine The line to be sent.
*
@@ -305,6 +307,7 @@ class CClient : public CIRCSocket {
bool OnPingMessage(CMessage& Message);
bool OnPongMessage(CMessage& Message);
bool OnQuitMessage(CQuitMessage& Message);
bool OnTagMessage(CTargetMessage& Message);
bool OnTextMessage(CTextMessage& Message);
bool OnTopicMessage(CTopicMessage& Message);
bool OnOtherMessage(CMessage& Message);
@@ -327,6 +330,7 @@ class CClient : public CIRCSocket {
bool m_bBatch;
bool m_bEchoMessage;
bool m_bSelfMessage;
bool m_bMessageTagCap;
bool m_bSASLCap;
bool m_bPlaybackActive;
CUser* m_pUser;
+4 -3
View File
@@ -153,9 +153,8 @@ class CIRCSock : public CIRCSocket {
bool HasAccountNotify() const { return m_bAccountNotify; }
bool HasExtendedJoin() const { return m_bExtendedJoin; }
bool HasServerTime() const { return m_bServerTime; }
const std::set<char>& GetUserModes() const {
return m_scUserModes;
}
bool HasMessageTagCap() const { return m_bMessageTagCap; }
const std::set<char>& GetUserModes() const { return m_scUserModes; }
// This is true if we are past raw 001
bool IsAuthed() const { return m_bAuthed; }
const SCString& GetAcceptedCaps() const { return m_ssAcceptedCaps; }
@@ -192,6 +191,7 @@ class CIRCSock : public CIRCSocket {
bool OnPingMessage(CMessage& Message);
bool OnPongMessage(CMessage& Message);
bool OnQuitMessage(CQuitMessage& Message);
bool OnTagMessage(CTargetMessage& Message);
bool OnTextMessage(CTextMessage& Message);
bool OnTopicMessage(CTopicMessage& Message);
bool OnWallopsMessage(CMessage& Message);
@@ -213,6 +213,7 @@ class CIRCSock : public CIRCSocket {
bool m_bAccountNotify;
bool m_bExtendedJoin;
bool m_bServerTime;
bool m_bMessageTagCap;
CString m_sPerms;
CString m_sPermModes;
std::set<char> m_scUserModes;
+2
View File
@@ -82,6 +82,7 @@ class CMessage {
Ping,
Pong,
Quit,
TagMsg,
Text,
Topic,
Wallops,
@@ -139,6 +140,7 @@ class CMessage {
void SetTime(const timeval& ts) { m_time = ts; }
const MCString& GetTags() const { return m_mssTags; }
MCString& GetTags() { return m_mssTags; }
void SetTags(const MCString& mssTags) { m_mssTags = mssTags; }
CString GetTag(const CString& sKey) const;
+22
View File
@@ -1079,6 +1079,25 @@ class CModule {
/// @deprecated Use OnSendToIRCMessage() instead.
virtual EModRet OnSendToIRC(CString& sLine);
/** This module hook is called when a user sends a TAGMSG message.
* @since 1.10.0
* @param Message The message which was sent.
* @return See CModule::EModRet.
*/
virtual EModRet OnUserTagMessage(CTargetMessage& Message);
/** Called when we receive a channel TAGMSG message <em>from IRC</em>.
* @since 1.10.0
* @param Message The channel message.
* @return See CModule::EModRet.
*/
virtual EModRet OnChanTagMessage(CTargetMessage& Message);
/** Called when we receive a private TAGMSG message <em>from IRC</em>.
* @since 1.10.0
* @param Message The message.
* @return See CModule::EModRet.
*/
virtual EModRet OnPrivTagMessage(CTargetMessage& Message);
ModHandle GetDLL() { return m_pDLL; }
/** This function sends a given IRC line to the IRC server, if we
@@ -1623,6 +1642,9 @@ class CModules : public std::vector<CModule*>, private CCoreTranslationMixin {
bool OnUserTopicRequest(CString& sChannel);
bool OnUserQuit(CString& sMessage);
bool OnUserQuitMessage(CQuitMessage& Message);
bool OnUserTagMessage(CTargetMessage& Message);
bool OnChanTagMessage(CTargetMessage& Message);
bool OnPrivTagMessage(CTargetMessage& Message);
bool OnCTCPReply(CNick& Nick, CString& sMessage);
bool OnCTCPReplyMessage(CCTCPMessage& Message);
+3
View File
@@ -103,6 +103,9 @@ EModRet OnChanNoticeMessage(CNoticeMessage& Message)
EModRet OnTopicMessage(CTopicMessage& Message)
EModRet OnSendToClientMessage(CMessage& Message)
EModRet OnSendToIRCMessage(CMessage& Message)
EModRet OnUserTagMessage(CTargetMessage& Message)
EModRet OnChanTagMessage(CTargetMessage& Message)
EModRet OnPrivTagMessage(CTargetMessage& Message)
void OnClientGetSASLMechanisms(SCString& ssMechanisms)
EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism, CString& sResponse)
+3
View File
@@ -160,6 +160,9 @@ class ZNC_EXPORT_LIB_EXPORT CPerlModule : public CModule {
EModRet OnTopicMessage(CTopicMessage& Message) override;
EModRet OnSendToClientMessage(CMessage& Message) override;
EModRet OnSendToIRCMessage(CMessage& Message) override;
EModRet OnUserTagMessage(CTargetMessage& Message) override;
EModRet OnChanTagMessage(CTargetMessage& Message) override;
EModRet OnPrivTagMessage(CTargetMessage& Message) override;
void OnClientGetSASLMechanisms(SCString& ssMechanisms) override;
EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism,
+3
View File
@@ -589,6 +589,9 @@ sub OnTopicMessage {
}
sub OnSendToClientMessage {}
sub OnSendToIRCMessage {}
sub OnUserTagMessage {}
sub OnChanTagMessage {}
sub OnPrivTagMessage {}
# In Perl "undefined" is allowed value, so perl modules may continue using OnMode and not OnMode2
sub OnChanPermission2 { my $self = shift; $self->OnChanPermission(@_) }
+3
View File
@@ -103,6 +103,9 @@ EModRet OnChanNoticeMessage(CNoticeMessage& Message)
EModRet OnTopicMessage(CTopicMessage& Message)
EModRet OnSendToClientMessage(CMessage& Message)
EModRet OnSendToIRCMessage(CMessage& Message)
EModRet OnUserTagMessage(CTargetMessage& Message)
EModRet OnChanTagMessage(CTargetMessage& Message)
EModRet OnPrivTagMessage(CTargetMessage& Message)
EModRet OnAddUser(CUser& User, CString& sErrorRet)
EModRet OnDeleteUser(CUser& User)
+3
View File
@@ -180,6 +180,9 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule {
EModRet OnTopicMessage(CTopicMessage& Message) override;
EModRet OnSendToClientMessage(CMessage& Message) override;
EModRet OnSendToIRCMessage(CMessage& Message) override;
EModRet OnUserTagMessage(CTargetMessage& Message) override;
EModRet OnChanTagMessage(CTargetMessage& Message) override;
EModRet OnPrivTagMessage(CTargetMessage& Message) override;
// Global Modules
EModRet OnAddUser(CUser& User, CString& sErrorRet) override;
+9
View File
@@ -704,6 +704,15 @@ class Module:
def OnSendToIRCMessage(self, msg):
pass
def OnUserTagMessage(self, msg):
pass
def OnChanTagMessage(self, msg):
pass
def OnPrivTagMessage(self, msg):
pass
class Command:
command = ''
+93 -16
View File
@@ -94,6 +94,7 @@ CClient::CClient()
m_bBatch(false),
m_bEchoMessage(false),
m_bSelfMessage(false),
m_bMessageTagCap(false),
m_bSASLCap(false),
m_bPlaybackActive(false),
m_pUser(nullptr),
@@ -258,6 +259,9 @@ void CClient::ReadLine(const CString& sData) {
case CMessage::Type::Quit:
bReturn = OnQuitMessage(Message);
break;
case CMessage::Type::TagMsg:
bReturn = OnTagMessage(Message);
break;
case CMessage::Type::Text:
bReturn = OnTextMessage(Message);
break;
@@ -271,7 +275,7 @@ void CClient::ReadLine(const CString& sData) {
if (bReturn) return;
PutIRC(Message.ToString(CMessage::ExcludePrefix | CMessage::ExcludeTags));
PutIRCStripping(Message);
}
void CClient::SetNick(const CString& s) { m_sNick = s; }
@@ -560,6 +564,16 @@ void CClient::PutIRC(const CString& sLine) {
}
}
void CClient::PutIRCStripping(CMessage Message) {
if (CIRCSock* pSock = GetIRCSock()) {
Message.SetNick(CNick{});
if (!pSock->HasMessageTagCap()) {
Message.SetTags({});
}
pSock->PutIRC(Message);
}
}
CString CClient::GetFullName() const {
if (!m_pUser) return GetRemoteIP();
CString sFullName = m_pUser->GetUsername();
@@ -578,6 +592,9 @@ bool CClient::PutClient(const CMessage& Message) {
} else if (!m_bAccountNotify &&
Message.GetType() == CMessage::Type::Account) {
return false;
} else if (!m_bMessageTagCap &&
Message.GetType() == CMessage::Type::TagMsg) {
return false;
}
CMessage Msg(Message);
@@ -643,11 +660,14 @@ bool CClient::PutClient(const CMessage& Message) {
}
}
MCString mssTags;
MCString mssNewTags;
MCString& mssTags = m_bMessageTagCap ? Msg.GetTags() : mssNewTags;
for (const auto& it : Msg.GetTags()) {
if (IsTagEnabled(it.first)) {
mssTags[it.first] = it.second;
if (!m_bMessageTagCap) {
for (const auto& it : Msg.GetTags()) {
if (IsTagEnabled(it.first)) {
mssTags[it.first] = it.second;
}
}
}
@@ -816,6 +836,10 @@ CClient::CoreCaps() {
[](CClient* pClient, bool bVal) {
pClient->m_bEchoMessage = bVal;
}},
{"message-tags",
[](CClient* pClient, bool bVal) {
pClient->m_bMessageTagCap = bVal;
}},
{"server-time",
[](CClient* pClient, bool bVal) {
pClient->m_bServerTime = bVal;
@@ -1037,6 +1061,17 @@ void CClient::NotifyServerDependentCap(const CString& sCap, bool bValue, const C
}
}
namespace {
template <typename X, class = void>
struct message_has_text : std::false_type {};
template <typename X>
struct message_has_text<
X, std::void_t<decltype(std::declval<const X&>().GetText()),
decltype(std::declval<X&>().SetText(""))>> : std::true_type {
};
}
template <typename T>
void CClient::AddBuffer(const T& Message) {
if (!m_pNetwork) {
@@ -1048,18 +1083,27 @@ void CClient::AddBuffer(const T& Message) {
Format.Clone(Message);
Format.SetNick(CNick(_NAMEDFMT(GetNickMask())));
Format.SetTarget(_NAMEDFMT(sTarget));
Format.SetText("{text}");
if constexpr (message_has_text<T>::value) {
Format.SetText("{text}");
}
CString sText;
CChan* pChan = m_pNetwork->FindChan(sTarget);
if (pChan) {
if (!pChan->AutoClearChanBuffer() || !m_pNetwork->IsUserOnline()) {
pChan->AddBuffer(Format, Message.GetText());
if constexpr (message_has_text<T>::value) {
sText = Message.GetText();
}
pChan->AddBuffer(Format, sText);
}
} else if (Message.GetType() != CMessage::Type::Notice) {
if (!m_pUser->AutoClearQueryBuffer() || !m_pNetwork->IsUserOnline()) {
CQuery* pQuery = m_pNetwork->AddQuery(sTarget);
if (pQuery) {
pQuery->AddBuffer(Format, Message.GetText());
if constexpr (message_has_text<T>::value) {
sText = Message.GetText();
}
pQuery->AddBuffer(Format, sText);
}
}
}
@@ -1134,8 +1178,7 @@ bool CClient::OnActionMessage(CActionMessage& Message) {
if (m_pNetwork) {
AddBuffer(Message);
EchoMessage(Message);
PutIRC(Message.ToString(CMessage::ExcludePrefix |
CMessage::ExcludeTags));
PutIRCStripping(Message);
}
}
@@ -1352,8 +1395,7 @@ bool CClient::OnCTCPMessage(CCTCPMessage& Message) {
}
if (m_pNetwork) {
PutIRC(Message.ToString(CMessage::ExcludePrefix |
CMessage::ExcludeTags));
PutIRCStripping(Message);
}
}
@@ -1456,6 +1498,8 @@ bool CClient::OnNoticeMessage(CNoticeMessage& Message) {
}
if (sTarget.TrimPrefix(m_pUser->GetStatusPrefix())) {
EchoMessage(Message);
if (!sTarget.Equals("status")) {
CALLMOD(sTarget, this, m_pUser, m_pNetwork,
OnModNotice(Message.GetText()));
@@ -1480,8 +1524,7 @@ bool CClient::OnNoticeMessage(CNoticeMessage& Message) {
if (m_pNetwork) {
AddBuffer(Message);
EchoMessage(Message);
PutIRC(Message.ToString(CMessage::ExcludePrefix |
CMessage::ExcludeTags));
PutIRCStripping(Message);
}
}
@@ -1554,6 +1597,41 @@ bool CClient::OnQuitMessage(CQuitMessage& Message) {
return true;
}
bool CClient::OnTagMessage(CTargetMessage& Message) {
CString sTargets = Message.GetTarget();
VCString vTargets;
sTargets.Split(",", vTargets, false);
for (CString& sTarget : vTargets) {
Message.SetTarget(sTarget);
if (m_pNetwork) {
// May be nullptr.
Message.SetChan(m_pNetwork->FindChan(sTarget));
}
if (sTarget.TrimPrefix(m_pUser->GetStatusPrefix())) {
EchoMessage(Message);
continue;
}
bool bContinue = false;
NETWORKMODULECALL(OnUserTagMessage(Message), m_pUser, m_pNetwork,
this, &bContinue);
if (bContinue) continue;
if (m_pNetwork) {
AddBuffer(Message);
EchoMessage(Message);
if (GetIRCSock()->HasMessageTagCap()) {
PutIRCStripping(Message);
}
}
}
return true;
}
bool CClient::OnTextMessage(CTextMessage& Message) {
CString sTargets = Message.GetTarget();
@@ -1597,8 +1675,7 @@ bool CClient::OnTextMessage(CTextMessage& Message) {
if (m_pNetwork) {
AddBuffer(Message);
EchoMessage(Message);
PutIRC(Message.ToString(CMessage::ExcludePrefix |
CMessage::ExcludeTags));
PutIRCStripping(Message);
}
}
+55 -5
View File
@@ -69,6 +69,7 @@ CIRCSock::CIRCSock(CIRCNetwork* pNetwork)
m_bAccountNotify(false),
m_bExtendedJoin(false),
m_bServerTime(false),
m_bMessageTagCap(false),
m_sPerms("*!@%+"),
m_sPermModes("qaohv"),
m_scUserModes(),
@@ -227,6 +228,9 @@ void CIRCSock::ReadLine(const CString& sData) {
case CMessage::Type::Quit:
bReturn = OnQuitMessage(Message);
break;
case CMessage::Type::TagMsg:
bReturn = OnTagMessage(Message);
break;
case CMessage::Type::Text:
bReturn = OnTextMessage(Message);
break;
@@ -385,6 +389,7 @@ bool CIRCSock::OnCapabilityMessage(CMessage& Message) {
{"server-time", [this](bool bVal) { m_bServerTime = bVal; }},
{"znc.in/server-time-iso", [this](bool bVal) { m_bServerTime = bVal; }},
{"chghost", [](bool) {}},
{"message-tags", [this](bool bVal) { m_bMessageTagCap = bVal; }},
};
auto RemoveCap = [&](const CString& sCap) {
@@ -1160,6 +1165,49 @@ bool CIRCSock::OnQuitMessage(CQuitMessage& Message) {
return !bIsVisible;
}
bool CIRCSock::OnTagMessage(CTargetMessage& Message) {
bool bResult = false;
CChan* pChan = nullptr;
CString sTarget = Message.GetTarget();
if (sTarget.Equals(GetNick())) {
IRCSOCKMODULECALL(OnPrivTagMessage(Message), &bResult);
if (bResult) return true;
if (!m_pNetwork->IsUserOnline() ||
!m_pNetwork->GetUser()->AutoClearQueryBuffer()) {
const CNick& Nick = Message.GetNick();
CQuery* pQuery = m_pNetwork->AddQuery(Nick.GetNick());
if (pQuery) {
CTargetMessage Format;
Format.Clone(Message);
Format.SetNick(_NAMEDFMT(Nick.GetNickMask()));
Format.SetTarget("{target}");
pQuery->AddBuffer(Format);
}
}
} else {
pChan = m_pNetwork->FindChan(sTarget);
if (pChan) {
Message.SetChan(pChan);
FixupChanNick(Message.GetNick(), pChan);
IRCSOCKMODULECALL(OnChanTagMessage(Message), &bResult);
if (bResult) return true;
if (!pChan->AutoClearChanBuffer() || !m_pNetwork->IsUserOnline() ||
pChan->IsDetached()) {
CTargetMessage Format;
Format.Clone(Message);
Format.SetNick(_NAMEDFMT(Message.GetNick().GetNickMask()));
Format.SetTarget(_NAMEDFMT(Message.GetTarget()));
pChan->AddBuffer(Format);
}
}
}
return (pChan && pChan->IsDetached());
}
bool CIRCSock::OnTextMessage(CTextMessage& Message) {
bool bResult = false;
CChan* pChan = nullptr;
@@ -1271,13 +1319,15 @@ void CIRCSock::TrySend() {
m_iSendsAllowed--;
CMessage& Message = m_vSendQueue.front();
MCString mssTags;
for (const auto& it : Message.GetTags()) {
if (IsTagEnabled(it.first)) {
mssTags[it.first] = it.second;
if (!m_bMessageTagCap) {
MCString mssTags;
for (const auto& it : Message.GetTags()) {
if (IsTagEnabled(it.first)) {
mssTags[it.first] = it.second;
}
}
Message.SetTags(mssTags);
}
Message.SetTags(mssTags);
Message.SetNetwork(m_pNetwork);
bool bSkip = false;
+1
View File
@@ -292,6 +292,7 @@ void CMessage::InitType() {
{"PING", Type::Ping},
{"PONG", Type::Pong},
{"QUIT", Type::Quit},
{"TAGMSG", Type::TagMsg},
{"TOPIC", Type::Topic},
{"WALLOPS", Type::Wallops},
};
+18
View File
@@ -881,6 +881,15 @@ CModule::EModRet CModule::OnUserNoticeMessage(CNoticeMessage& Message) {
Message.SetText(sText);
return ret;
}
CModule::EModRet CModule::OnUserTagMessage(CTargetMessage& Message) {
return CONTINUE;
}
CModule::EModRet CModule::OnPrivTagMessage(CTargetMessage& Message) {
return CONTINUE;
}
CModule::EModRet CModule::OnChanTagMessage(CTargetMessage& Message) {
return CONTINUE;
}
CModule::EModRet CModule::OnUserJoin(CString& sChannel, CString& sKey) {
return CONTINUE;
}
@@ -1430,6 +1439,15 @@ bool CModules::OnUserNotice(CString& sTarget, CString& sMessage) {
bool CModules::OnUserNoticeMessage(CNoticeMessage& Message) {
MODHALTCHK(OnUserNoticeMessage(Message));
}
bool CModules::OnUserTagMessage(CTargetMessage& Message) {
MODHALTCHK(OnUserTagMessage(Message));
}
bool CModules::OnPrivTagMessage(CTargetMessage& Message) {
MODHALTCHK(OnPrivTagMessage(Message));
}
bool CModules::OnChanTagMessage(CTargetMessage& Message) {
MODHALTCHK(OnChanTagMessage(Message));
}
bool CModules::OnUserJoin(CString& sChannel, CString& sKey) {
MODHALTCHK(OnUserJoin(sChannel, sKey));
}
+48
View File
@@ -981,5 +981,53 @@ TEST_F(ZNCTest, SpacedServerPassword) {
ircd3.ReadUntil("PASS a");
}
TEST_F(ZNCTest, TagMsg) {
auto znc = Run();
auto ircd = ConnectIRCd();
auto client = LoginClient();
ircd.Write("001 nick Welcome");
client.Write("@foo=bar PRIVMSG #foo hi");
ircd.ReadUntil("\nPRIVMSG");
client.Write("TAGMSG #foo");
ASSERT_THAT(ircd.ReadRemainder().toStdString(), Not(HasSubstr("TAGMSG")));
ircd.Write("@foo=bar :friend PRIVMSG #foo hi");
client.ReadUntil("\n:friend PRIVMSG");
ircd.Write("TAGMSG #foo");
ASSERT_THAT(client.ReadRemainder().toStdString(), Not(HasSubstr("TAGMSG")));
client.Write("CAP REQ :message-tags");
client.ReadUntil("ACK");
ircd.Write("@foo TAGMSG #foo");
client.ReadUntil("@foo TAGMSG #foo");
ircd.Write("CAP * ACK message-tags");
// barrier to make client wait before sending TAGMSG
ircd.Write(":friend PRIVMSG #foo hi");
client.ReadUntil("friend");
client.Write("@foo=bar TAGMSG #foo");
ircd.ReadUntil("@foo=bar TAGMSG #foo");
client.Write("CAP REQ echo-message");
client.Write("@baz TAGMSG #foo");
client.ReadUntil("@baz :nick TAGMSG #foo");
// Check buffers
client.Write("znc addnetwork other");
client.Write("znc jumpnetwork other");
client.ReadUntil(":Switched to other");
ircd.Write(":nick JOIN #bar");
ircd.Write("@bar TAGMSG #bar");
ASSERT_THAT(client.ReadRemainder().toStdString(), Not(HasSubstr("TAGMSG")));
client.Write("znc jumpnetwork test");
client.ReadUntil("@bar TAGMSG #bar");
}
} // namespace
} // namespace znc_inttest