mirror of
https://github.com/znc/znc.git
synced 2026-08-07 09:22:55 +02:00
Implement cap negotiation 3.2 on server side
Used for "server-dependent" caps which already rely on sending NEW and DEL to client. This functionality is not yet available for caps added by modules.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+10
-1
@@ -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<CModule*>, 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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+39
-8
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+98
-61
@@ -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<CString, std::function<void(bool bVal)>> 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<CString, std::function<void(bool bVal)>> 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);
|
||||
|
||||
+6
-3
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user