diff --git a/include/znc/Chan.h b/include/znc/Chan.h index 31fa8e34..831d50ae 100644 --- a/include/znc/Chan.h +++ b/include/znc/Chan.h @@ -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 m_mcsModes; }; diff --git a/src/Chan.cpp b/src/Chan.cpp index b1cd707f..fd370f2a 100644 --- a/src/Chan.cpp +++ b/src/Chan.cpp @@ -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& vpClients = m_pNetwork->GetClients(); for (CClient* pEachClient : vpClients) { CClient* pThisClient; - if (!pClient) + if (!pTarget) pThisClient = pEachClient; else - pThisClient = pClient; + pThisClient = pTarget; for (map::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() { diff --git a/src/Client.cpp b/src/Client.cpp index de5311a1..a5020dd8 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -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); } } diff --git a/src/IRCSock.cpp b/src/IRCSock.cpp index 6695b240..6b1c24c0 100644 --- a/src/IRCSock.cpp +++ b/src/IRCSock.cpp @@ -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 diff --git a/test/IRCSockTest.cpp b/test/IRCSockTest.cpp index f6d38134..6bebe35e 100644 --- a/test/IRCSockTest.cpp +++ b/test/IRCSockTest.cpp @@ -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); +} \ No newline at end of file diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp index d020ca3c..aab1cc8d 100644 --- a/test/integration/tests/core.cpp +++ b/test/integration/tests/core.cpp @@ -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