Merge pull request #2030 from RealKindOne/fix-client-cycle

Fix join/part not working when a client sends '/cycle' or '/hop'.
This commit is contained in:
Alexey Sokolov
2026-07-22 08:47:46 +01:00
committed by GitHub
6 changed files with 224 additions and 10 deletions
+3
View File
@@ -168,6 +168,7 @@ class CChan : private CCoreTranslationMixin {
void Enable();
void IncJoinTries() { m_uJoinTries++; }
void ResetJoinTries() { m_uJoinTries = 0; }
void SetParting(bool b) { m_bParting = b; }
// !Setters
// Getters
@@ -199,6 +200,7 @@ class CChan : private CCoreTranslationMixin {
bool HasAutoClearChanBufferSet() const {
return m_bHasAutoClearChanBufferSet;
}
bool IsParting() const { return m_bParting; }
// !Getters
private:
protected:
@@ -223,6 +225,7 @@ class CChan : private CCoreTranslationMixin {
CBuffer m_Buffer;
bool m_bModeKnown;
bool m_bParting;
std::map<char, CString> m_mcsModes;
};
+20 -9
View File
@@ -30,6 +30,7 @@ CChan::CChan(const CString& sName, CIRCNetwork* pNetwork, bool bInConfig,
CConfig* pConfig)
: m_bDetached(false),
m_bIsOn(false),
m_bParting(false),
m_bAutoClearChanBuffer(pNetwork->GetUser()->AutoClearChanBuffer()),
m_bInConfig(bInConfig),
m_bDisabled(false),
@@ -84,6 +85,7 @@ CChan::~CChan() { ClearNicks(); }
void CChan::Reset() {
m_bIsOn = false;
m_bParting = false;
m_bModeKnown = false;
m_mcsModes.clear();
m_sTopic = "";
@@ -143,27 +145,36 @@ void CChan::JoinUser(const CString& sKey) {
if (!IsOn() && !sKey.empty()) {
SetKey(sKey);
}
if (m_pNetwork->IsIRCConnected() && !IsOn()) {
if (m_pNetwork->IsIRCConnected() && (!IsOn() || IsParting())) {
m_pNetwork->PutIRC("JOIN " + GetName() + " " + GetKey());
}
}
void CChan::AttachUser(CClient* pClient) {
// Sending '/znc attach #channel' applies for all clients connected.
// Sending '/join #channel' only applies for the client that sent
// the command. This causes a de-sync with IRC events like QUIT
// displaying in the other clients server/status window.
// Make '/join #channel' work the same as '/znc attach #channel'
CClient* pTarget = IsDetached() ? nullptr : pClient;
m_pNetwork->PutUser(
":" + m_pNetwork->GetIRCNick().GetNickMask() + " JOIN :" + GetName(),
pClient);
pTarget);
if (!GetTopic().empty()) {
m_pNetwork->PutUser(":" + m_pNetwork->GetIRCServer() + " 332 " +
m_pNetwork->GetIRCNick().GetNick() + " " +
GetName() + " :" + GetTopic(),
pClient);
pTarget);
if (!GetTopicOwner().empty()) {
m_pNetwork->PutUser(":" + m_pNetwork->GetIRCServer() + " 333 " +
m_pNetwork->GetIRCNick().GetNick() + " " +
GetName() + " " + GetTopicOwner() + " " +
CString(GetTopicDate()),
pClient);
pTarget);
}
}
@@ -176,10 +187,10 @@ void CChan::AttachUser(CClient* pClient) {
const vector<CClient*>& vpClients = m_pNetwork->GetClients();
for (CClient* pEachClient : vpClients) {
CClient* pThisClient;
if (!pClient)
if (!pTarget)
pThisClient = pEachClient;
else
pThisClient = pClient;
pThisClient = pTarget;
for (map<CString, CNick>::iterator a = m_msNicks.begin();
a != m_msNicks.end(); ++a) {
@@ -210,18 +221,18 @@ void CChan::AttachUser(CClient* pClient) {
}
}
if (pClient) // We only want to do this for one client
if (pTarget)
break;
}
m_pNetwork->PutUser(":" + m_pNetwork->GetIRCServer() + " 366 " +
m_pNetwork->GetIRCNick().GetNick() + " " +
GetName() + " :End of /NAMES list.",
pClient);
pTarget);
m_bDetached = false;
// Send Buffer
SendBuffer(pClient);
SendBuffer(pTarget);
}
void CChan::DetachUser() {
+2 -1
View File
@@ -1469,7 +1469,7 @@ bool CClient::OnJoinMessage(CJoinMessage& Message) {
if (pChan) {
if (pChan->IsDetached())
pChan->AttachUser(this);
else
else if (!pChan->IsOn() || pChan->IsParting())
pChan->JoinUser(sKey);
continue;
} else if (!sChannel.empty()) {
@@ -1592,6 +1592,7 @@ bool CClient::OnPartMessage(CPartMessage& Message) {
PutStatusNotice(t_f("Removing channel {1}")(sChan));
m_pNetwork->DelChan(sChan);
} else {
if (pChan) pChan->SetParting(true);
sChans += (sChans.empty()) ? sChan : CString("," + sChan);
}
}
+15
View File
@@ -692,6 +692,7 @@ bool CIRCSock::OnJoinMessage(CJoinMessage& Message) {
if (pChan) {
pChan->Enable();
pChan->SetIsOn(true);
pChan->SetParting(false);
PutIRC("MODE " + sChan);
}
} else {
@@ -727,6 +728,7 @@ bool CIRCSock::OnKickMessage(CKickMessage& Message) {
if (GetNick().Equals(sKickedNick) && pChan) {
pChan->SetIsOn(false);
pChan->SetParting(false);
// Don't try to rejoin!
pChan->Disable();
@@ -1119,6 +1121,19 @@ bool CIRCSock::OnNumericMessage(CNumericMessage& Message) {
m_pNetwork->AddMotdBuffer(BufferMessage(Message));
}
break;
case 403: // ERR_NOSUCHCHANNEL
case 442: // ERR_NOTONCHANNEL
{
CString sChan = Message.GetParam(1);
CChan* pChan = m_pNetwork->FindChan(sChan);
if (pChan && pChan->IsParting()) {
pChan->SetIsOn(false);
pChan->SetParting(false);
m_pNetwork->PutStatus(
t_f("PART failed for channel {1}")(sChan));
}
}
break;
case 437:
// :irc.server.net 437 * badnick :Nick/channel is temporarily unavailable
// :irc.server.net 437 mynick badnick :Nick/channel is temporarily unavailable
+38
View File
@@ -558,3 +558,41 @@ TEST_F(IRCSockTest, ChanMode) {
":are supported by this server");
m_pTestSock->ReadLine(":irc.znc.in 324 me #chan +ntf ");
}
TEST_F(IRCSockTest, PartingFlag) {
// Set channel as joined
m_pTestChan->SetIsOn(true);
CMessage clientPart("PART #chan");
m_pTestClient->ReadLine(clientPart.ToString());
EXPECT_THAT(m_pTestSock->vsLines, ElementsAre("PART #chan"));
EXPECT_TRUE(m_pTestChan->IsParting());
m_pTestSock->Reset();
CMessage serverPart(":me PART #chan");
m_pTestSock->ReadLine(serverPart.ToString());
// Verify channel was deleted
EXPECT_EQ(m_pTestNetwork->FindChan("#chan"), nullptr);
}
TEST_F(IRCSockTest, PartingFlagOnError) {
// Set channel as joined
m_pTestChan->SetIsOn(true);
CMessage clientPart("PART #chan");
m_pTestClient->ReadLine(clientPart.ToString());
m_pTestSock->Reset();
CMessage errorMsg(":server 442 me #chan :You're not on that channel");
m_pTestSock->ReadLine(errorMsg.ToString());
// IsParting() should be cleared
EXPECT_FALSE(m_pTestChan->IsParting());
// Verify channel was deleted
EXPECT_NE(m_pTestNetwork->FindChan("#chan"), nullptr);
}
+146
View File
@@ -1278,5 +1278,151 @@ TEST_F(ZNCTest, CAPDetached) {
<< "Client saw chghost even though all channels are detached";
}
// Test for CChan::AttachUser()
TEST_F(ZNCTest, JoinDetachedChannelMultiClient) {
auto znc = Run();
auto ircd = ConnectIRCd();
auto client1 = LoginClient();
ircd.Write(":server 001 nick :Hello");
client1.Write("JOIN #test");
ircd.ReadUntil("JOIN #test");
ircd.Write(":nick JOIN :#test");
ircd.Write(":server 353 nick #test :nick");
ircd.Write(":server 366 nick #test :End of /NAMES list");
client1.ReadUntil("End of /NAMES");
client1.Write("DETACH #test");
client1.ReadUntil("Detached 1 channel");
auto client2 = LoginClient();
client2.ReadUntil("001");
// Both client1 and client2 should be in #test
client2.Write("JOIN #test");
client1.ReadUntil(":nick JOIN :#test");
client2.ReadUntil(":nick JOIN :#test");
client1.ReadUntil("353");
client2.ReadUntil("353");
ircd.Write(":other!user@host JOIN :#test");
client1.ReadUntil(":other!user@host JOIN :#test");
client2.ReadUntil(":other!user@host JOIN :#test");
ircd.Write(":other!user@host QUIT :quit message");
client1.ReadUntil(":other!user@host QUIT :quit message");
client2.ReadUntil(":other!user@host QUIT :quit message");
}
// Commit ad7bd6d7eed84648638e1b6fd69546b9fe496576
// prevented rejoining when a client cycles a channel.
TEST_F(ZNCTest, ClientCycleChannel) {
auto znc = Run();
auto ircd = ConnectIRCd();
auto client = LoginClient();
ircd.Write(":server 001 nick :Hello");
client.Write("JOIN #test");
ircd.ReadUntil("JOIN #test");
ircd.Write(":nick JOIN :#test");
ircd.Write(":server 353 nick #test :nick");
ircd.Write(":server 366 nick #test :End of /NAMES list");
client.ReadUntil("End of /NAMES");
// Clients have '/hop' or '/cycle' command that sends
// 'PART #channel' and 'JOIN #channel'. Verify ZNC rejoins.
client.Write(":nick PART #test");
// Verify PART is forwarded to server
QByteArray partMsg;
ircd.ReadUntilAndGet("PART", partMsg);
EXPECT_THAT(partMsg.toStdString(), HasSubstr("PART #test"));
ircd.Write(":nick PART #test");
client.ReadUntil(":nick PART #test");
client.Write(":nick JOIN #test");
ircd.ReadUntil("JOIN #test");
}
TEST_F(ZNCTest, PartWithError403) {
auto znc = Run();
auto ircd = ConnectIRCd();
auto client = LoginClient();
// Join a channel first
client.Write("JOIN #test");
client.Close();
ircd.Write(":server 001 nick :Hello");
ircd.ReadUntil("JOIN #test");
ircd.Write(":nick JOIN :#test");
ircd.Write(":server 353 nick #test :nick");
ircd.Write(":server 366 nick #test :End of /NAMES list");
// Reconnect client and send PART
client = LoginClient();
client.ReadUntil(":nick JOIN :#test");
client.Write("PART #test");
ircd.ReadUntil("PART #test");
// Server returns 403 error (ERR_NOSUCHCHANNEL) instead of confirming
ircd.Write(":server 403 nick #test :No such channel");
// Verify client receives the status message about PART failure
client.ReadUntil("PART failed for channel #test");
// Verify channel still exists by trying to rejoin
client.Write("JOIN #test");
ircd.ReadUntil("JOIN #test");
ircd.Write(":nick JOIN :#test");
ircd.Write(":server 353 nick #test :nick");
ircd.Write(":server 366 nick #test :End of /NAMES list");
client.ReadUntil(":nick JOIN :#test");
}
TEST_F(ZNCTest, PartWithError442) {
auto znc = Run();
auto ircd = ConnectIRCd();
auto client = LoginClient();
// Join a channel first
client.Write("JOIN #test");
client.Close();
ircd.Write(":server 001 nick :Hello");
ircd.ReadUntil("JOIN #test");
ircd.Write(":nick JOIN :#test");
ircd.Write(":server 353 nick #test :nick");
ircd.Write(":server 366 nick #test :End of /NAMES list");
// Reconnect client and send PART
client = LoginClient();
client.ReadUntil(":nick JOIN :#test");
client.Write("PART #test :leaving");
QByteArray partMsg;
ircd.ReadUntilAndGet("PART", partMsg);
EXPECT_THAT(partMsg.toStdString(), HasSubstr("PART #test"));
// Server returns 442 error (ERR_NOTONCHANNEL) instead of confirming
ircd.Write(":server 442 nick #test :You're not on that channel");
// Verify client receives the status message about PART failure
client.ReadUntil("PART failed for channel #test");
// Verify channel still exists by trying to rejoin
client.Write("JOIN #test");
ircd.ReadUntil("JOIN #test");
ircd.Write(":nick JOIN :#test");
ircd.Write(":server 353 nick #test :nick");
ircd.Write(":server 366 nick #test :End of /NAMES list");
client.ReadUntil(":nick JOIN :#test");
}
} // namespace
} // namespace znc_inttest