diff --git a/include/znc/Client.h b/include/znc/Client.h index 2c77fa17..e68e3920 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -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; diff --git a/include/znc/IRCSock.h b/include/znc/IRCSock.h index e3b60198..9a4ffefb 100644 --- a/include/znc/IRCSock.h +++ b/include/znc/IRCSock.h @@ -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& GetUserModes() const { - return m_scUserModes; - } + bool HasMessageTagCap() const { return m_bMessageTagCap; } + const std::set& 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 m_scUserModes; diff --git a/include/znc/Message.h b/include/znc/Message.h index b31a4f17..f2686f00 100644 --- a/include/znc/Message.h +++ b/include/znc/Message.h @@ -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; diff --git a/include/znc/Modules.h b/include/znc/Modules.h index 39eece67..7a6f6769 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -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 from IRC. + * @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 from IRC. + * @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 @@ -1628,6 +1647,9 @@ class CModules : public std::vector, 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); diff --git a/modules/modperl/functions.in b/modules/modperl/functions.in index 105acb61..7e03fdff 100644 --- a/modules/modperl/functions.in +++ b/modules/modperl/functions.in @@ -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) diff --git a/modules/modperl/module.h b/modules/modperl/module.h index 61caa74b..a5ba56cb 100644 --- a/modules/modperl/module.h +++ b/modules/modperl/module.h @@ -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, diff --git a/modules/modperl/startup.pl b/modules/modperl/startup.pl index b4846b20..8834d436 100644 --- a/modules/modperl/startup.pl +++ b/modules/modperl/startup.pl @@ -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(@_) } diff --git a/modules/modpython/functions.in b/modules/modpython/functions.in index 366c6307..2ec2eee7 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -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) diff --git a/modules/modpython/module.h b/modules/modpython/module.h index 57a7ffbb..295c170f 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -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; diff --git a/modules/modpython/znc.py b/modules/modpython/znc.py index 64e429d3..064dec6f 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -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 = '' diff --git a/src/Client.cpp b/src/Client.cpp index 01195b50..f279d81c 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -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 +struct message_has_text : std::false_type {}; + +template +struct message_has_text< + X, std::void_t().GetText()), + decltype(std::declval().SetText(""))>> : std::true_type { +}; +} + template 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::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::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::value) { + sText = Message.GetText(); + } + pQuery->AddBuffer(Format, sText); } } } @@ -1131,11 +1175,15 @@ bool CClient::OnActionMessage(CActionMessage& Message) { this, &bContinue); if (bContinue) continue; + if (sTarget.TrimPrefix(m_pUser->GetStatusPrefix())) { + EchoMessage(Message); + continue; + } + if (m_pNetwork) { AddBuffer(Message); EchoMessage(Message); - PutIRC(Message.ToString(CMessage::ExcludePrefix | - CMessage::ExcludeTags)); + PutIRCStripping(Message); } } @@ -1342,6 +1390,10 @@ bool CClient::OnCTCPMessage(CCTCPMessage& Message) { } if (bContinue) continue; + if (sTarget.TrimPrefix(m_pUser->GetStatusPrefix())) { + continue; + } + if (!GetIRCSock()) { // Some lagmeters do a NOTICE to their own nick, ignore those. if (!sTarget.Equals(m_sNick)) @@ -1352,8 +1404,7 @@ bool CClient::OnCTCPMessage(CCTCPMessage& Message) { } if (m_pNetwork) { - PutIRC(Message.ToString(CMessage::ExcludePrefix | - CMessage::ExcludeTags)); + PutIRCStripping(Message); } } @@ -1456,6 +1507,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 +1533,7 @@ bool CClient::OnNoticeMessage(CNoticeMessage& Message) { if (m_pNetwork) { AddBuffer(Message); EchoMessage(Message); - PutIRC(Message.ToString(CMessage::ExcludePrefix | - CMessage::ExcludeTags)); + PutIRCStripping(Message); } } @@ -1554,6 +1606,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)); + } + + bool bContinue = false; + NETWORKMODULECALL(OnUserTagMessage(Message), m_pUser, m_pNetwork, + this, &bContinue); + if (bContinue) continue; + + if (sTarget.TrimPrefix(m_pUser->GetStatusPrefix())) { + EchoMessage(Message); + 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 +1684,7 @@ bool CClient::OnTextMessage(CTextMessage& Message) { if (m_pNetwork) { AddBuffer(Message); EchoMessage(Message); - PutIRC(Message.ToString(CMessage::ExcludePrefix | - CMessage::ExcludeTags)); + PutIRCStripping(Message); } } diff --git a/src/IRCSock.cpp b/src/IRCSock.cpp index 2580406e..1eea2b24 100644 --- a/src/IRCSock.cpp +++ b/src/IRCSock.cpp @@ -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; diff --git a/src/Message.cpp b/src/Message.cpp index 9455002c..acaa9964 100644 --- a/src/Message.cpp +++ b/src/Message.cpp @@ -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}, }; diff --git a/src/Modules.cpp b/src/Modules.cpp index 6cbaba4f..3b3246db 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -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)); } diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp index 6d61a65e..f1a0768e 100644 --- a/test/integration/tests/core.cpp +++ b/test/integration/tests/core.cpp @@ -981,5 +981,64 @@ 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"); +} + +TEST_F(ZNCTest, StatusAction) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + ircd.Write("001 nick Welcome"); + + client.Write("PRIVMSG *status :\1ACTION waves\1"); + client.Write("PRIVMSG *status :\1VERSION\1"); + ASSERT_THAT(ircd.ReadRemainder().toStdString(), Not(HasSubstr("PRIVMSG"))); +} + } // namespace } // namespace znc_inttest