diff --git a/include/znc/Client.h b/include/znc/Client.h index 631c6528..2312698d 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -296,6 +296,7 @@ class CClient : public CIRCSocket { */ void SetTagSupport(const CString& sTag, bool bState); + void NotifyServerDependentCap(const CString& sCap, bool bValue); void NotifyServerDependentCaps(const SCString& ssCaps); void ClearServerDependentCaps(); @@ -384,6 +385,8 @@ class CClient : public CIRCSocket { m_mCoreCaps; // A subset of CIRCSock::GetAcceptedCaps(), the caps that can be listed // in CAP LS and may be notified to the client with CAP NEW (cap-notify). + // TODO: come up with a way for modules to work with this, and with + // =values in NEW. SCString m_ssServerDependentCaps; friend class ClientTest; diff --git a/include/znc/IRCNetwork.h b/include/znc/IRCNetwork.h index 2d16e088..73c907d2 100644 --- a/include/znc/IRCNetwork.h +++ b/include/znc/IRCNetwork.h @@ -156,6 +156,7 @@ class CIRCNetwork : private CCoreTranslationMixin { void IRCConnected(); void IRCDisconnected(); void CheckIRCConnect(); + void NotifyServerDependentCap(const CString& sCap, bool bValue); bool PutIRC(const CString& sLine); bool PutIRC(const CMessage& Message); diff --git a/include/znc/IRCSock.h b/include/znc/IRCSock.h index a9f34f6d..e40448c5 100644 --- a/include/znc/IRCSock.h +++ b/include/znc/IRCSock.h @@ -192,7 +192,7 @@ class CIRCSock : public CIRCSocket { bool OnTextMessage(CTextMessage& Message); bool OnTopicMessage(CTopicMessage& Message); bool OnWallopsMessage(CMessage& Message); - bool OnServerCapAvailable(const CString& sCap); + bool OnServerCapAvailable(const CString& sCap, const CString& sValue); // !Message Handlers void SetNick(const CString& sNick); diff --git a/include/znc/Modules.h b/include/znc/Modules.h index ef8f2219..4d29afa5 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -1003,6 +1003,15 @@ class CModule { * needs to turn it on with CAP REQ. */ virtual bool OnServerCapAvailable(const CString& sCap); + /** Called for every CAP received via CAP LS from server. + * By default just calls OnServerCapAvailable() without sValue, so + * overriding one of the two is enough. + * @param sCap capability name supported by server. + * @param sValue value. + * @return true if your module supports this CAP and + * needs to turn it on with CAP REQ. + */ + virtual bool OnServerCap302Available(const CString& sCap, const CString& sValue); /** Called for every CAP accepted or rejected by server * (with CAP ACK or CAP NAK after our CAP REQ). * @param sCap capability accepted/rejected by server. @@ -1542,7 +1551,7 @@ class CModules : public std::vector, private CCoreTranslationMixin { bool OnSendToIRC(CString& sLine); bool OnSendToIRCMessage(CMessage& Message); - bool OnServerCapAvailable(const CString& sCap); + bool OnServerCapAvailable(const CString& sCap, const CString& sValue); bool OnServerCapResult(const CString& sCap, bool bSuccess); CModule* FindModule(const CString& sModule) const; diff --git a/modules/modperl/functions.in b/modules/modperl/functions.in index c4f843e9..4efb3e8a 100644 --- a/modules/modperl/functions.in +++ b/modules/modperl/functions.in @@ -61,6 +61,7 @@ EModRet OnPrivNotice(CNick& Nick, CString& sMessage) EModRet OnChanNotice(CNick& Nick, CChan& Channel, CString& sMessage) EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) bool OnServerCapAvailable(const CString& sCap) +bool OnServerCap302Available(const CString& sCap, const CString& sValue) void OnServerCapResult(const CString& sCap, bool bSuccess) EModRet OnTimerAutoJoin(CChan& Channel) bool OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl) diff --git a/modules/modperl/module.h b/modules/modperl/module.h index 06f976dd..ddf95698 100644 --- a/modules/modperl/module.h +++ b/modules/modperl/module.h @@ -116,6 +116,7 @@ class ZNC_EXPORT_LIB_EXPORT CPerlModule : public CModule { CString& sMessage) override; EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) override; bool OnServerCapAvailable(const CString& sCap) override; + bool OnServerCap302Available(const CString& sCap, const CString& sValue) override; void OnServerCapResult(const CString& sCap, bool bSuccess) override; EModRet OnTimerAutoJoin(CChan& Channel) override; bool OnEmbeddedWebRequest(CWebSock&, const CString&, CTemplate&) override; diff --git a/modules/modperl/startup.pl b/modules/modperl/startup.pl index 753c1a55..bd3d8823 100644 --- a/modules/modperl/startup.pl +++ b/modules/modperl/startup.pl @@ -405,6 +405,7 @@ sub OnPrivNotice {} sub OnChanNotice {} sub OnTopic {} sub OnServerCapAvailable {} +sub OnServerCap302Available { my ($self, $cap, $value) = @_; $self->OnServerCapAvailable($cap) } sub OnServerCapResult {} sub OnTimerAutoJoin {} sub OnEmbeddedWebRequest {} diff --git a/modules/modpython/functions.in b/modules/modpython/functions.in index f779e577..142522d7 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -61,6 +61,7 @@ EModRet OnPrivNotice(CNick& Nick, CString& sMessage) EModRet OnChanNotice(CNick& Nick, CChan& Channel, CString& sMessage) EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) bool OnServerCapAvailable(const CString& sCap) +bool OnServerCap302Available(const CString& sCap, const CString& sValue) void OnServerCapResult(const CString& sCap, bool bSuccess) EModRet OnTimerAutoJoin(CChan& Channel) bool OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl) diff --git a/modules/modpython/module.h b/modules/modpython/module.h index a0847a20..bae43458 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -136,6 +136,7 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule { CString& sMessage) override; EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) override; bool OnServerCapAvailable(const CString& sCap) override; + bool OnServerCap302Available(const CString& sCap, const CString& sValue) override; void OnServerCapResult(const CString& sCap, bool bSuccess) override; EModRet OnTimerAutoJoin(CChan& Channel) override; bool OnEmbeddedWebRequest(CWebSock&, const CString&, CTemplate&) override; diff --git a/modules/modpython/znc.py b/modules/modpython/znc.py index 4e8fc295..52b529a8 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -410,6 +410,9 @@ class Module: def OnServerCapAvailable(self, sCap): pass + def OnServerCap302Available(self, sCap, sValue): + return self.OnServerCapAvailable(sCap) + def OnServerCapResult(self, sCap, bSuccess): pass diff --git a/src/Client.cpp b/src/Client.cpp index 7cc81f32..fd1e1655 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -852,9 +852,38 @@ void CClient::SetTagSupport(const CString& sTag, bool bState) { } } +void CClient::NotifyServerDependentCap(const CString& sCap, bool bValue) { + auto it = m_mCoreCaps.find(sCap); + if (bValue) { + if (m_mCoreCaps.end() != it) { + bool bServerDependent = std::get<0>(it->second); + if (bServerDependent) { + if (m_ssServerDependentCaps.count(sCap) == 0) { + m_ssServerDependentCaps.insert(sCap); + if (HasCapNotify()) { + PutClient(":irc.znc.in CAP " + GetNick() + " NEW :" + sCap); + } + } + } + } + } else { + if (HasCapNotify() && m_ssServerDependentCaps.count(sCap) > 0) { + PutClient(":irc.znc.in CAP " + GetNick() + " DEL :" + sCap); + } + if (m_mCoreCaps.end() != it) { + bool bServerDependent = std::get<0>(it->second); + const auto& handler = std::get<1>(it->second); + if (bServerDependent) { + handler(false); + } + } + m_ssServerDependentCaps.erase(sCap); + } +} + void CClient::NotifyServerDependentCaps(const SCString& ssCaps) { for (const CString& sCap : ssCaps) { - const auto& it = m_mCoreCaps.find(sCap); + auto it = m_mCoreCaps.find(sCap); if (m_mCoreCaps.end() != it) { bool bServerDependent = std::get<0>(it->second); if (bServerDependent) { @@ -864,20 +893,22 @@ void CClient::NotifyServerDependentCaps(const SCString& ssCaps) { } if (HasCapNotify() && !m_ssServerDependentCaps.empty()) { - CString sCaps = CString(" ").Join(m_ssServerDependentCaps.begin(), - m_ssServerDependentCaps.end()); - PutClient(":irc.znc.in CAP " + GetNick() + " NEW :" + sCaps); + VCString vsCaps = MultiLine(m_ssServerDependentCaps); + for (const CString& sLine : vsCaps) { + PutClient(":irc.znc.in CAP " + GetNick() + " NEW :" + sLine); + } } } void CClient::ClearServerDependentCaps() { if (HasCapNotify() && !m_ssServerDependentCaps.empty()) { - CString sCaps = CString(" ").Join(m_ssServerDependentCaps.begin(), - m_ssServerDependentCaps.end()); - PutClient(":irc.znc.in CAP " + GetNick() + " DEL :" + sCaps); + VCString vsCaps = MultiLine(m_ssServerDependentCaps); + for (const CString& sLine : vsCaps) { + PutClient(":irc.znc.in CAP " + GetNick() + " DEL :" + sLine); + } for (const CString& sCap : m_ssServerDependentCaps) { - const auto& it = m_mCoreCaps.find(sCap); + auto it = m_mCoreCaps.find(sCap); if (m_mCoreCaps.end() != it) { const auto& handler = std::get<1>(it->second); handler(false); diff --git a/src/IRCNetwork.cpp b/src/IRCNetwork.cpp index fe1ab8dc..b4f33618 100644 --- a/src/IRCNetwork.cpp +++ b/src/IRCNetwork.cpp @@ -1417,6 +1417,12 @@ void CIRCNetwork::IRCDisconnected() { CheckIRCConnect(); } +void CIRCNetwork::NotifyServerDependentCap(const CString& sCap, bool bValue) { + for (CClient* pClient : m_vClients) { + pClient->NotifyServerDependentCap(sCap, bValue); + } +} + void CIRCNetwork::SetIRCConnectEnabled(bool b) { m_bIRCConnectEnabled = b; diff --git a/src/IRCSock.cpp b/src/IRCSock.cpp index 45465ca1..825fbc9a 100644 --- a/src/IRCSock.cpp +++ b/src/IRCSock.cpp @@ -246,7 +246,9 @@ void CIRCSock::SendNextCap() { if (!m_uCapPaused) { if (m_ssPendingCaps.empty()) { // We already got all needed ACK/NAK replies. - PutIRC("CAP END"); + if (!m_bAuthed) { + PutIRC("CAP END"); + } } else { CString sCap = *m_ssPendingCaps.begin(); m_ssPendingCaps.erase(m_ssPendingCaps.begin()); @@ -262,9 +264,9 @@ void CIRCSock::ResumeCap() { SendNextCap(); } -bool CIRCSock::OnServerCapAvailable(const CString& sCap) { +bool CIRCSock::OnServerCapAvailable(const CString& sCap, const CString& sValue) { bool bResult = false; - IRCSOCKMODULECALL(OnServerCapAvailable(sCap), &bResult); + IRCSOCKMODULECALL(OnServerCapAvailable(sCap, sValue), &bResult); return bResult; } @@ -347,67 +349,102 @@ bool CIRCSock::OnAwayMessage(CMessage& Message) { } bool CIRCSock::OnCapabilityMessage(CMessage& Message) { - // CAPs are supported only before authorization. - if (!m_bAuthed) { - // The first parameter is most likely "*". No idea why, the - // CAP spec don't mention this, but all implementations - // I've seen add this extra asterisk - CString sSubCmd = Message.GetParam(1); + // The first parameter is most likely "*". No idea why, the + // CAP spec don't mention this, but all implementations + // I've seen add this extra asterisk + CString sSubCmd = Message.GetParam(1); - // If the caplist of a reply is too long, it's split - // into multiple replies. A "*" is prepended to show - // that the list was split into multiple replies. - // This is useful mainly for LS. For ACK and NAK - // replies, there's no real need for this, because - // we request only 1 capability per line. - // If we will need to support broken servers or will - // send several requests per line, need to delay ACK - // actions until all ACK lines are received and - // to recognize past request of NAK by 100 chars - // of this reply. - CString sArgs; - if (Message.GetParam(2) == "*") { - sArgs = Message.GetParam(3); - } else { - sArgs = Message.GetParam(2); + // If the caplist of a reply is too long, it's split + // into multiple replies. A "*" is prepended to show + // that the list was split into multiple replies. + // This is useful mainly for LS. For ACK and NAK + // replies, there's no real need for this, because + // we request only 1 capability per line. + // If we will need to support broken servers or will + // send several requests per line, need to delay ACK + // actions until all ACK lines are received and + // to recognize past request of NAK by 100 chars + // of this reply. + // As for LS, we shouldn't don't send END after receiving first line, + // because interesting caps can be on next line. + CString sArgs; + bool bSendNext = true; + if (Message.GetParam(2) == "*") { + bSendNext = false; + sArgs = Message.GetParam(3); + } else { + sArgs = Message.GetParam(2); + } + + std::map> mSupportedCaps = { + {"multi-prefix", [this](bool bVal) { m_bNamesx = bVal; }}, + {"userhost-in-names", [this](bool bVal) { m_bUHNames = bVal; }}, + {"away-notify", [this](bool bVal) { m_bAwayNotify = bVal; }}, + {"account-notify", [this](bool bVal) { m_bAccountNotify = bVal; }}, + {"account-tag", [this](bool bVal) { m_bAccountTag = bVal; }}, + {"cap-notify", [](bool bVal) {}}, + {"extended-join", [this](bool bVal) { m_bExtendedJoin = bVal; }}, + {"server-time", [this](bool bVal) { m_bServerTime = bVal; }}, + {"znc.in/server-time-iso", + [this](bool bVal) { m_bServerTime = bVal; }}, + }; + + auto RemoveCap = [&](const CString& sCap) { + IRCSOCKMODULECALL(OnServerCapResult(sCap, false), NOTHING); + auto it = mSupportedCaps.find(sCap); + if (it != mSupportedCaps.end()) { + it->second(false); } - - std::map> mSupportedCaps = { - {"multi-prefix", [this](bool bVal) { m_bNamesx = bVal; }}, - {"userhost-in-names", [this](bool bVal) { m_bUHNames = bVal; }}, - {"away-notify", [this](bool bVal) { m_bAwayNotify = bVal; }}, - {"account-notify", [this](bool bVal) { m_bAccountNotify = bVal; }}, - {"account-tag", [this](bool bVal) { m_bAccountTag = bVal; }}, - {"extended-join", [this](bool bVal) { m_bExtendedJoin = bVal; }}, - {"server-time", [this](bool bVal) { m_bServerTime = bVal; }}, - {"znc.in/server-time-iso", - [this](bool bVal) { m_bServerTime = bVal; }}, - }; - - if (sSubCmd == "LS") { - VCString vsTokens; - sArgs.Split(" ", vsTokens, false); - - for (const CString& sCap : vsTokens) { - if (OnServerCapAvailable(sCap) || mSupportedCaps.count(sCap)) { - m_ssPendingCaps.insert(sCap); - } - } - } else if (sSubCmd == "ACK") { - sArgs.Trim(); - IRCSOCKMODULECALL(OnServerCapResult(sArgs, true), NOTHING); - const auto& it = mSupportedCaps.find(sArgs); - if (it != mSupportedCaps.end()) { - it->second(true); - } - m_ssAcceptedCaps.insert(sArgs); - } else if (sSubCmd == "NAK") { - // This should work because there's no [known] - // capability with length of name more than 100 characters. - sArgs.Trim(); - IRCSOCKMODULECALL(OnServerCapResult(sArgs, false), NOTHING); + m_ssAcceptedCaps.erase(sCap); + m_ssPendingCaps.erase(sCap); + if (m_bAuthed) { + m_pNetwork->NotifyServerDependentCap(sCap, false); } + }; + if (sSubCmd == "LS" || sSubCmd == "NEW") { + VCString vsTokens; + sArgs.Split(" ", vsTokens, false); + + for (const CString& sToken : vsTokens) { + CString sCap, sValue; + int eq = sToken.find('='); + if (eq == std::string::npos) { + sCap = sToken; + } else { + sCap = sToken.substr(0, eq); + sValue = sToken.substr(eq + 1); + } + if (OnServerCapAvailable(sCap, sValue) || mSupportedCaps.count(sCap)) { + m_ssPendingCaps.insert(sCap); + } + } + } else if (sSubCmd == "ACK") { + sArgs.Trim(); + IRCSOCKMODULECALL(OnServerCapResult(sArgs, true), NOTHING); + auto it = mSupportedCaps.find(sArgs); + if (it != mSupportedCaps.end()) { + it->second(true); + } + m_ssAcceptedCaps.insert(sArgs); + if (m_bAuthed) { + m_pNetwork->NotifyServerDependentCap(sArgs, true); + } + } else if (sSubCmd == "NAK") { + // This should work because there's no [known] + // capability with length of name more than 100 characters. + sArgs.Trim(); + RemoveCap(sArgs); + } else if (sSubCmd == "DEL") { + VCString vsTokens; + sArgs.Split(" ", vsTokens, false); + + for (const CString& sCap : vsTokens) { + RemoveCap(sCap); + } + } + + if (bSendNext) { SendNextCap(); } // Don't forward any CAP stuff to the client @@ -1222,7 +1259,7 @@ void CIRCSock::Connected() { &bReturn); if (bReturn) return; - PutIRC("CAP LS"); + PutIRC("CAP LS 302"); if (!sPass.empty()) { PutIRC("PASS " + sPass); diff --git a/src/Modules.cpp b/src/Modules.cpp index d69d6050..fb868352 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -999,6 +999,9 @@ CModule::EModRet CModule::OnSendToIRCMessage(CMessage& Message) { } bool CModule::OnServerCapAvailable(const CString& sCap) { return false; } +bool CModule::OnServerCap302Available(const CString& sCap, const CString& sValue) { + return OnServerCapAvailable(sCap); +} void CModule::OnServerCapResult(const CString& sCap, bool bSuccess) {} bool CModule::PutIRC(const CString& sLine) { @@ -1491,7 +1494,7 @@ bool CModules::OnModCTCP(const CString& sMessage) { } // Why MODHALTCHK works only with functions returning EModRet ? :( -bool CModules::OnServerCapAvailable(const CString& sCap) { +bool CModules::OnServerCapAvailable(const CString& sCap, const CString& sValue) { bool bResult = false; for (CModule* pMod : *this) { try { @@ -1500,11 +1503,11 @@ bool CModules::OnServerCapAvailable(const CString& sCap) { if (m_pUser) { CUser* pOldUser = pMod->GetUser(); pMod->SetUser(m_pUser); - bResult |= pMod->OnServerCapAvailable(sCap); + bResult |= pMod->OnServerCap302Available(sCap, sValue); pMod->SetUser(pOldUser); } else { // WTF? Is that possible? - bResult |= pMod->OnServerCapAvailable(sCap); + bResult |= pMod->OnServerCap302Available(sCap, sValue); } pMod->SetClient(pOldClient); } catch (const CModule::EModException& e) { diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp index fab871cb..a01baca5 100644 --- a/test/integration/tests/core.cpp +++ b/test/integration/tests/core.cpp @@ -270,6 +270,35 @@ TEST_F(ZNCTest, AwayNotify) { client.Write("znc shutdown"); } +TEST_F(ZNCTest, CAP302LSWaitFull) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + ircd.ReadUntil("CAP LS 302"); + ircd.Write("CAP user LS * :away-notify"); + ASSERT_THAT(ircd.ReadRemainder().toStdString(), Not(HasSubstr("away-notify"))); + ircd.Write("CAP user LS :blahblah"); + ircd.ReadUntil("CAP REQ :away-notify"); +} + +TEST_F(ZNCTest, CAP302NewDel) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + ircd.Write("CAP nick LS :blahblah"); + ircd.ReadUntil("CAP END"); + ircd.Write(":server 001 nick :Hello"); + client.Write("CAP REQ :away-notify"); + client.ReadUntil("NAK :away-notify"); + client.Write("CAP REQ :cap-notify"); + client.ReadUntil("ACK :cap-notify"); + ircd.Write("CAP nick NEW :away-notify"); + ircd.ReadUntil("CAP REQ :away-notify"); + ircd.Write("CAP nick ACK :away-notify"); + client.ReadUntil("CAP nick NEW :away-notify"); + ircd.Write("CAP nick DEL :away-notify"); + client.ReadUntil("CAP nick DEL :away-notify"); +} + TEST_F(ZNCTest, JoinKey) { QFile conf(m_dir.path() + "/configs/znc.conf"); ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text));