From d49399bbca58750f2273784c47bd6ec597da72da Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Tue, 15 Oct 2024 11:29:36 +0100 Subject: [PATCH] Implement chghost capability Interaction with extended-join doesn't yet work correctly, because ZNC doesn't keep track of everyone's real names --- include/znc/Client.h | 2 + include/znc/IRCSock.h | 2 + include/znc/Message.h | 11 +++++ src/Client.cpp | 5 +- src/IRCSock.cpp | 82 ++++++++++++++++++++++++++++++++- src/Message.cpp | 11 +++++ test/integration/tests/core.cpp | 51 ++++++++++++++++++++ 7 files changed, 161 insertions(+), 3 deletions(-) diff --git a/include/znc/Client.h b/include/znc/Client.h index 9f3c2155..2185740a 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -119,6 +119,7 @@ class CClient : public CIRCSocket { bool HasExtendedJoin() const { return m_bExtendedJoin; } bool HasNamesx() const { return m_bNamesx; } bool HasUHNames() const { return m_bUHNames; } + bool HasChgHost() const { return m_bChgHost; } bool IsAway() const { return m_bAway; } bool HasServerTime() const { return m_bServerTime; } bool HasBatch() const { return m_bBatch; } @@ -289,6 +290,7 @@ class CClient : public CIRCSocket { bool m_bExtendedJoin; bool m_bNamesx; bool m_bUHNames; + bool m_bChgHost; bool m_bAway; bool m_bServerTime; bool m_bBatch; diff --git a/include/znc/IRCSock.h b/include/znc/IRCSock.h index ce93ceea..b2caf354 100644 --- a/include/znc/IRCSock.h +++ b/include/znc/IRCSock.h @@ -131,6 +131,7 @@ class CIRCSock : public CIRCSocket { unsigned int GetMaxNickLen() const { return m_uMaxNickLen; } EChanModeArgs GetModeType(char cMode) const; char GetPermFromMode(char cMode) const; + char GetModeFromPerm(char cPerm) const; const std::map& GetChanModes() const { return m_mceChanModes; } @@ -177,6 +178,7 @@ class CIRCSock : public CIRCSocket { bool OnActionMessage(CActionMessage& Message); bool OnAwayMessage(CMessage& Message); bool OnCapabilityMessage(CMessage& Message); + bool OnChgHostMessage(CChgHostMessage& Message); bool OnCTCPMessage(CCTCPMessage& Message); bool OnErrorMessage(CMessage& Message); bool OnInviteMessage(CMessage& Message); diff --git a/include/znc/Message.h b/include/znc/Message.h index ab067f69..e8b9f8de 100644 --- a/include/znc/Message.h +++ b/include/znc/Message.h @@ -67,6 +67,7 @@ class CMessage { Action, Away, Capability, + ChgHost, CTCP, Error, Invite, @@ -121,6 +122,7 @@ class CMessage { */ VCString GetParamsSplit(unsigned int uIdx, unsigned int uLen = -1) const; void SetParams(const VCString& vsParams); + void SetParams(VCString&& vsParams); /// @deprecated use GetParamsColon() instead. CString GetParams(unsigned int uIdx, unsigned int uLen = -1) const @@ -333,4 +335,13 @@ class CTopicMessage : public CTargetMessage { }; REGISTER_ZNC_MESSAGE(CTopicMessage); +class CChgHostMessage : public CMessage { + public: + CString GetNewIdent() const { return GetParam(0); } + void SetNewIdent(const CString& sIdent) { SetParam(0, sIdent); } + CString GetNewHost() const { return GetParam(1); } + void SetNewHost(const CString& sHost) { SetParam(1, sHost); } +}; +REGISTER_ZNC_MESSAGE(CChgHostMessage); + #endif // !ZNC_MESSAGE_H diff --git a/src/Client.cpp b/src/Client.cpp index 38c1ffe2..f345e362 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -75,7 +75,8 @@ using std::vector; } \ } -CClient::CClient() : CIRCSocket(), +CClient::CClient() + : CIRCSocket(), m_bGotPass(false), m_bGotNick(false), m_bGotUser(false), @@ -87,6 +88,7 @@ CClient::CClient() : CIRCSocket(), m_bExtendedJoin(false), m_bNamesx(false), m_bUHNames(false), + m_bChgHost(false), m_bAway(false), m_bServerTime(false), m_bBatch(false), @@ -762,6 +764,7 @@ CClient::CoreCaps() { }}, {"cap-notify", [](CClient* pClient, bool bVal) { pClient->m_bCapNotify = bVal; }}, + {"chghost", [](CClient* pClient, bool bVal) { pClient->m_bChgHost = bVal; }}, }; // For compatibility with older clients diff --git a/src/IRCSock.cpp b/src/IRCSock.cpp index b7bcec00..b6d7f1bb 100644 --- a/src/IRCSock.cpp +++ b/src/IRCSock.cpp @@ -185,6 +185,9 @@ void CIRCSock::ReadLine(const CString& sData) { case CMessage::Type::Capability: bReturn = OnCapabilityMessage(Message); break; + case CMessage::Type::ChgHost: + bReturn = OnChgHostMessage(Message); + break; case CMessage::Type::CTCP: bReturn = OnCTCPMessage(Message); break; @@ -380,8 +383,8 @@ bool CIRCSock::OnCapabilityMessage(CMessage& Message) { {"userhost-in-names", [this](bool bVal) { m_bUHNames = bVal; }}, {"cap-notify", [](bool bVal) {}}, {"server-time", [this](bool bVal) { m_bServerTime = bVal; }}, - {"znc.in/server-time-iso", - [this](bool bVal) { m_bServerTime = bVal; }}, + {"znc.in/server-time-iso", [this](bool bVal) { m_bServerTime = bVal; }}, + {"chghost", [](bool) {}}, }; auto RemoveCap = [&](const CString& sCap) { @@ -514,6 +517,69 @@ bool CIRCSock::OnCTCPMessage(CCTCPMessage& Message) { return (pChan && pChan->IsDetached()); } +bool CIRCSock::OnChgHostMessage(CChgHostMessage& Message) { + // The emulation of QUIT+JOIN would be cleaner inside CClient::PutClient() + // but computation of new modes is difficult enough so that I don't want to + // repeat it for every client + // + // TODO: make CNick store modes (v, o) instead of perm chars (+, @), that + // would simplify this + bool bNeedEmulate = false; + for (CClient* pClient : m_pNetwork->GetClients()) { + if (pClient->HasChgHost()) { + pClient->PutClient(Message); + } else { + bNeedEmulate = true; + pClient->PutClient(CMessage(Message.GetNick(), "QUIT", + {"Changing hostname"}, + Message.GetTags())); + } + } + + if (!bNeedEmulate) return true; + + CNick NewNick = Message.GetNick(); + NewNick.SetIdent(Message.GetNewIdent()); + NewNick.SetHost(Message.GetNewHost()); + + for (CChan* pChan : m_pNetwork->GetChans()) { + if (CNick* pNick = pChan->FindNick(Message.GetNick().GetNick())) { + pNick->SetIdent(Message.GetNewIdent()); + pNick->SetHost(Message.GetNewHost()); + } + + CTargetMessage ModeMsg; + ModeMsg.SetNick(CNick(":irc.znc.in")); + ModeMsg.SetTags(Message.GetTags()); + ModeMsg.SetCommand("MODE"); + VCString vsModeParams = {pChan->GetName(), "+"}; + if (CNick* pNick = pChan->FindNick(NewNick.GetNick())) { + for (char cPerm : pNick->GetPermStr()) { + char cMode = GetModeFromPerm(cPerm); + if (cMode) { + vsModeParams[1].append(1, cMode); + vsModeParams.push_back(NewNick.GetNick()); + } + } + } + ModeMsg.SetParams(std::move(vsModeParams)); + + for (CClient* pClient : m_pNetwork->GetClients()) { + if (!pClient->HasChgHost()) { + // TODO: send account name and real name too, for + // extended-join + pClient->PutClient(CMessage(NewNick, "JOIN", {pChan->GetName()}, + Message.GetTags())); + if (ModeMsg.GetParams().size() > 2) { + pClient->PutClient(ModeMsg); + } + } + } + } + + return true; +} + bool CIRCSock::OnErrorMessage(CMessage& Message) { // ERROR :Closing Link: nick[24.24.24.24] (Excess Flood) CString sError = Message.GetParam(0); @@ -1517,6 +1583,18 @@ char CIRCSock::GetPermFromMode(char cMode) const { return 0; } +char CIRCSock::GetModeFromPerm(char cPerm) const { + if (m_sPermModes.size() == m_sPerms.size()) { + for (unsigned int a = 0; a < m_sPermModes.size(); a++) { + if (m_sPerms[a] == cPerm) { + return m_sPermModes[a]; + } + } + } + + return 0; +} + CIRCSock::EChanModeArgs CIRCSock::GetModeType(char cMode) const { map::const_iterator it = m_mceChanModes.find(cMode); diff --git a/src/Message.cpp b/src/Message.cpp index e57b3480..4aadc48d 100644 --- a/src/Message.cpp +++ b/src/Message.cpp @@ -81,6 +81,16 @@ void CMessage::SetParams(const VCString& vsParams) { } } +void CMessage::SetParams(VCString&& vsParams) { + m_vsParams = std::move(vsParams); + m_bColon = false; + + if (m_eType == Type::Text || m_eType == Type::Notice || + m_eType == Type::Action || m_eType == Type::CTCP) { + InitType(); + } +} + CString CMessage::GetParam(unsigned int uIdx) const { if (uIdx >= m_vsParams.size()) { return ""; @@ -270,6 +280,7 @@ void CMessage::InitType() { {"ACCOUNT", Type::Account}, {"AWAY", Type::Away}, {"CAP", Type::Capability}, + {"CHGHOST", Type::ChgHost}, {"ERROR", Type::Error}, {"INVITE", Type::Invite}, {"JOIN", Type::Join}, diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp index c32e70be..2b455cd9 100644 --- a/test/integration/tests/core.cpp +++ b/test/integration/tests/core.cpp @@ -745,5 +745,56 @@ TEST_F(ZNCTest, CapReqWithoutLs) { ASSERT_THAT(client.ReadRemainder().toStdString(), Not(HasSubstr("Welcome"))); } +TEST_F(ZNCTest, ChgHostEmulation) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + ircd.Write("CAP user LS :chghost"); + ircd.ReadUntil("CAP REQ :chghost"); + ircd.Write("CAP user ACK :chghost"); + + auto client1 = LoginClient(); + auto client2 = LoginClient(); + client2.Write("CAP REQ :chghost"); + client2.ReadUntil("ACK"); + + ircd.Write(":user!oldident@oldhost JOIN #chan"); + + ircd.Write(":user!oldident@oldhost CHGHOST newident newhost"); + client1.ReadUntil(":user!oldident@oldhost QUIT :Changing hostname"); + client1.ReadUntil(":user!newident@newhost JOIN #chan"); + ASSERT_THAT(client1.ReadRemainder().toStdString(), Not(HasSubstr("MODE"))); + client2.ReadUntil(":user!oldident@oldhost CHGHOST newident newhost"); + client2.Close(); + + ircd.Write(":server MODE #chan +v user"); + client1.ReadUntil("MODE"); + ircd.Write(":user!newident@newhost CHGHOST ident-2 host-2"); + client1.ReadUntil(":irc.znc.in MODE #chan +v user"); + + ircd.Write(":server MODE #chan +o user"); + client1.ReadUntil("MODE"); + ircd.Write(":user!ident-2@host-2 CHGHOST ident-3 host-3"); + client1.ReadUntil(":irc.znc.in MODE #chan +ov user user"); +} + +TEST_F(ZNCTest, ChgHostOnce) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + ircd.Write("CAP user LS :chghost"); + ircd.ReadUntil("CAP REQ :chghost"); + ircd.Write("CAP user ACK :chghost"); + + auto client = LoginClient(); + client.Write("CAP REQ :chghost"); + client.ReadUntil("ACK"); + + ircd.Write(":user!oldident@oldhost JOIN #chan"); + ircd.Write(":user!oldident@oldhost JOIN #chan2"); + ircd.Write(":user!oldident@oldhost CHGHOST newident newhost"); + client.ReadUntil("CHGHOST"); + ASSERT_THAT(client.ReadRemainder().toStdString(), + Not(HasSubstr("CHGHOST"))); +} + } // namespace } // namespace znc_inttest