/* * Copyright (C) 2004-2025 ZNC, see the NOTICE file for details. * Copyright (C) 2006-2007, CNU *(http://cnu.dieplz.net/znc) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include using std::vector; class CLogRule { public: CLogRule(const CString& sRule, bool bEnabled = true) : m_sRule(sRule), m_bEnabled(bEnabled) {} const CString& GetRule() const { return m_sRule; } bool IsEnabled() const { return m_bEnabled; } void SetEnabled(bool bEnabled) { m_bEnabled = bEnabled; } bool Compare(const CString& sTarget) const { return sTarget.WildCmp(m_sRule, CString::CaseInsensitive); } bool operator==(const CLogRule& sOther) const { return m_sRule == sOther.GetRule(); } CString ToString() const { return (m_bEnabled ? "" : "!") + m_sRule; } private: CString m_sRule; bool m_bEnabled; }; class CLogMod : public CModule { public: MODCONSTRUCTOR(CLogMod) { m_bSanitize = false; AddHelpCommand(); AddCommand( "SetRules", t_d(""), t_d("Set logging rules, use !#chan or !query to negate and * "), [=](const CString& sLine) { SetRulesCmd(sLine); }); AddCommand("ClearRules", "", t_d("Clear all logging rules"), [=](const CString& sLine) { ClearRulesCmd(sLine); }); AddCommand("ListRules", "", t_d("List all logging rules"), [=](const CString& sLine) { ListRulesCmd(sLine); }); AddCommand( "Set", t_d(" true|false"), t_d("Set one of the following options: joins, quits, nickchanges"), [=](const CString& sLine) { SetCmd(sLine); }); AddCommand("ShowSettings", "", t_d("Show current settings set by Set command"), [=](const CString& sLine) { ShowSettingsCmd(sLine); }); } void SetRulesCmd(const CString& sLine); void ClearRulesCmd(const CString& sLine); void ListRulesCmd(const CString& sLine = ""); void SetCmd(const CString& sLine); void ShowSettingsCmd(const CString& sLine); void SetRules(const VCString& vsRules); VCString SplitRules(const CString& sRules) const; CString JoinRules(const CString& sSeparator) const; bool TestRules(const CString& sTarget) const; void PutLog(const CString& sLine, const CString& sWindow = "status"); void PutLog(const CString& sLine, const CChan& Channel); void PutLog(const CString& sLine, const CNick& Nick); CString GetServer(); bool OnLoad(const CString& sArgs, CString& sMessage) override; void OnIRCConnected() override; void OnIRCDisconnected() override; EModRet OnBroadcast(CString& sMessage) override; void OnRawMode2(const CNick* pOpNick, CChan& Channel, const CString& sModes, const CString& sArgs) override; void OnKick(const CNick& OpNick, const CString& sKickedNick, CChan& Channel, const CString& sMessage) override; void OnQuit(const CNick& Nick, const CString& sMessage, const vector& vChans) override; void OnJoinMessage(CJoinMessage& Message) override; void OnPart(const CNick& Nick, CChan& Channel, const CString& sMessage) override; void OnNick(const CNick& OldNick, const CString& sNewNick, const vector& vChans) override; EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) override; EModRet OnSendToIRCMessage(CMessage& Message) override; /* notices */ EModRet OnUserNotice(CString& sTarget, CString& sMessage) override; EModRet OnPrivNotice(CNick& Nick, CString& sMessage) override; EModRet OnChanNotice(CNick& Nick, CChan& Channel, CString& sMessage) override; /* actions */ EModRet OnUserAction(CString& sTarget, CString& sMessage) override; EModRet OnPrivAction(CNick& Nick, CString& sMessage) override; EModRet OnChanAction(CNick& Nick, CChan& Channel, CString& sMessage) override; /* msgs */ EModRet OnUserMsg(CString& sTarget, CString& sMessage) override; EModRet OnPrivMsg(CNick& Nick, CString& sMessage) override; EModRet OnChanMsg(CNick& Nick, CChan& Channel, CString& sMessage) override; private: bool NeedJoins() const; bool NeedQuits() const; bool NeedNickChanges() const; CString m_sLogPath; CString m_sTimestamp; bool m_bSanitize; vector m_vRules; }; void CLogMod::SetRulesCmd(const CString& sLine) { VCString vsRules = SplitRules(sLine.Token(1, true)); if (vsRules.empty()) { PutModule(t_s("Usage: SetRules ")); PutModule(t_s("Wildcards are allowed")); } else { SetRules(vsRules); SetNV("rules", JoinRules(",")); ListRulesCmd(); } } void CLogMod::ClearRulesCmd(const CString& sLine) { size_t uCount = m_vRules.size(); if (uCount == 0) { PutModule(t_s("No logging rules. Everything is logged.")); } else { CString sRules = JoinRules(" "); SetRules(VCString()); DelNV("rules"); PutModule(t_p("1 rule removed: {2}", "{1} rules removed: {2}", uCount)( uCount, sRules)); } } void CLogMod::ListRulesCmd(const CString& sLine) { CTable Table; Table.AddColumn(t_s("Rule", "listrules")); Table.AddColumn(t_s("Logging enabled", "listrules")); Table.SetStyle(CTable::ListStyle); for (const CLogRule& Rule : m_vRules) { Table.AddRow(); Table.SetCell(t_s("Rule", "listrules"), Rule.GetRule()); Table.SetCell(t_s("Logging enabled", "listrules"), CString(Rule.IsEnabled())); } if (Table.empty()) { PutModule(t_s("No logging rules. Everything is logged.")); } else { PutModule(Table); } } void CLogMod::SetCmd(const CString& sLine) { const CString sVar = sLine.Token(1).AsLower(); const CString sValue = sLine.Token(2); if (sValue.empty()) { PutModule( t_s("Usage: Set true|false, where is one of: joins, " "quits, nickchanges")); return; } bool b = sLine.Token(2).ToBool(); const std::unordered_map> mssResponses = { {"joins", {t_s("Will log joins"), t_s("Will not log joins")}}, {"quits", {t_s("Will log quits"), t_s("Will not log quits")}}, {"nickchanges", {t_s("Will log nick changes"), t_s("Will not log nick changes")}}}; auto it = mssResponses.find(sVar); if (it == mssResponses.end()) { PutModule(t_s( "Unknown variable. Known variables: joins, quits, nickchanges")); return; } SetNV(sVar, CString(b)); PutModule(b ? it->second.first : it->second.second); } void CLogMod::ShowSettingsCmd(const CString& sLine) { PutModule(NeedJoins() ? t_s("Logging joins") : t_s("Not logging joins")); PutModule(NeedQuits() ? t_s("Logging quits") : t_s("Not logging quits")); PutModule(NeedNickChanges() ? t_s("Logging nick changes") : t_s("Not logging nick changes")); } bool CLogMod::NeedJoins() const { return !HasNV("joins") || GetNV("joins").ToBool(); } bool CLogMod::NeedQuits() const { return !HasNV("quits") || GetNV("quits").ToBool(); } bool CLogMod::NeedNickChanges() const { return !HasNV("nickchanges") || GetNV("nickchanges").ToBool(); } void CLogMod::SetRules(const VCString& vsRules) { m_vRules.clear(); for (CString sRule : vsRules) { bool bEnabled = !sRule.TrimPrefix("!"); m_vRules.push_back(CLogRule(sRule, bEnabled)); } } VCString CLogMod::SplitRules(const CString& sRules) const { CString sCopy = sRules; sCopy.Replace(",", " "); VCString vsRules; sCopy.Split(" ", vsRules, false, "", "", true, true); return vsRules; } CString CLogMod::JoinRules(const CString& sSeparator) const { VCString vsRules; for (const CLogRule& Rule : m_vRules) { vsRules.push_back(Rule.ToString()); } return sSeparator.Join(vsRules.begin(), vsRules.end()); } bool CLogMod::TestRules(const CString& sTarget) const { for (const CLogRule& Rule : m_vRules) { if (Rule.Compare(sTarget)) { return Rule.IsEnabled(); } } return true; } void CLogMod::PutLog(const CString& sLine, const CString& sWindow /*= "Status"*/) { if (!TestRules(sWindow)) { return; } CString sPath; timeval curtime; gettimeofday(&curtime, nullptr); // Generate file name sPath = CUtils::FormatTime(curtime, m_sLogPath, GetUser()->GetTimezone()); if (sPath.empty()) { DEBUG("Could not format log path [" << sPath << "]"); return; } // TODO: Properly handle IRC case mapping // $WINDOW has to be handled last, since it can contain % sPath.Replace("$USER", CString((GetUser() ? GetUser()->GetUsername() : "UNKNOWN"))); sPath.Replace("$NETWORK", CString((GetNetwork() ? GetNetwork()->GetName() : "znc"))); sPath.Replace("$WINDOW", CString(sWindow.Replace_n("/", "-") .Replace_n("\\", "-")).AsLower()); // Check if it's allowed to write in this specific path sPath = CDir::CheckPathPrefix(GetSavePath(), sPath); if (sPath.empty()) { DEBUG("Invalid log path [" << m_sLogPath << "]."); return; } CFile LogFile(sPath); CString sLogDir = LogFile.GetDir(); struct stat ModDirInfo; CFile::GetInfo(GetSavePath(), ModDirInfo); if (!CFile::Exists(sLogDir)) CDir::MakeDir(sLogDir, ModDirInfo.st_mode); if (LogFile.Open(O_WRONLY | O_APPEND | O_CREAT)) { LogFile.Write(CUtils::FormatTime(curtime, m_sTimestamp, GetUser()->GetTimezone()) + " " + (m_bSanitize ? sLine.StripControls_n() : sLine) + "\n"); } else DEBUG("Could not open log file [" << sPath << "]: " << strerror(errno)); } void CLogMod::PutLog(const CString& sLine, const CChan& Channel) { PutLog(sLine, Channel.GetName()); } void CLogMod::PutLog(const CString& sLine, const CNick& Nick) { PutLog(sLine, Nick.GetNick()); } CString CLogMod::GetServer() { CServer* pServer = GetNetwork()->GetCurrentServer(); CString sSSL; if (!pServer) return "(no server)"; if (pServer->IsSSL()) sSSL = "+"; return pServer->GetName() + " " + sSSL + CString(pServer->GetPort()); } bool CLogMod::OnLoad(const CString& sArgs, CString& sMessage) { VCString vsArgs; sArgs.QuoteSplit(vsArgs); bool bReadingTimestamp = false; bool bHaveLogPath = false; for (CString& sArg : vsArgs) { if (bReadingTimestamp) { m_sTimestamp = sArg; bReadingTimestamp = false; } else if (sArg.Equals("-sanitize")) { m_bSanitize = true; } else if (sArg.Equals("-timestamp")) { bReadingTimestamp = true; } else { // Only one arg may be LogPath if (bHaveLogPath) { sMessage = t_f("Invalid args [{1}]. Only one log path allowed. Check " "that there are no spaces in the path.")(sArgs); return false; } m_sLogPath = sArg; bHaveLogPath = true; } } if (m_sTimestamp.empty()) { m_sTimestamp = "[%H:%M:%S]"; } // Add default filename to path if it's a folder if (GetType() == CModInfo::UserModule) { if (m_sLogPath.Right(1) == "/" || m_sLogPath.find("$WINDOW") == CString::npos || m_sLogPath.find("$NETWORK") == CString::npos) { if (!m_sLogPath.empty()) { m_sLogPath += "/"; } m_sLogPath += "$NETWORK/$WINDOW/%Y-%m-%d.log"; } } else if (GetType() == CModInfo::NetworkModule) { if (m_sLogPath.Right(1) == "/" || m_sLogPath.find("$WINDOW") == CString::npos) { if (!m_sLogPath.empty()) { m_sLogPath += "/"; } m_sLogPath += "$WINDOW/%Y-%m-%d.log"; } } else { if (m_sLogPath.Right(1) == "/" || m_sLogPath.find("$USER") == CString::npos || m_sLogPath.find("$WINDOW") == CString::npos || m_sLogPath.find("$NETWORK") == CString::npos) { if (!m_sLogPath.empty()) { m_sLogPath += "/"; } m_sLogPath += "$USER/$NETWORK/$WINDOW/%Y-%m-%d.log"; } } CString sRules = GetNV("rules"); VCString vsRules = SplitRules(sRules); SetRules(vsRules); // Check if it's allowed to write in this path in general m_sLogPath = CDir::CheckPathPrefix(GetSavePath(), m_sLogPath); if (m_sLogPath.empty()) { sMessage = t_f("Invalid log path [{1}]")(m_sLogPath); return false; } else { sMessage = t_f("Logging to [{1}]. Using timestamp format '{2}'")( m_sLogPath, m_sTimestamp); return true; } } // TODO consider writing translated strings to log. Currently user language // affects only UI. void CLogMod::OnIRCConnected() { PutLog("Connected to IRC (" + GetServer() + ")"); } void CLogMod::OnIRCDisconnected() { PutLog("Disconnected from IRC (" + GetServer() + ")"); } CModule::EModRet CLogMod::OnBroadcast(CString& sMessage) { PutLog("Broadcast: " + sMessage); return CONTINUE; } void CLogMod::OnRawMode2(const CNick* pOpNick, CChan& Channel, const CString& sModes, const CString& sArgs) { const CString sNick = pOpNick ? pOpNick->GetNick() : "Server"; PutLog("*** " + sNick + " sets mode: " + sModes + " " + sArgs, Channel); } void CLogMod::OnKick(const CNick& OpNick, const CString& sKickedNick, CChan& Channel, const CString& sMessage) { PutLog("*** " + sKickedNick + " was kicked by " + OpNick.GetNick() + " (" + sMessage + ")", Channel); } void CLogMod::OnQuit(const CNick& Nick, const CString& sMessage, const vector& vChans) { if (NeedQuits()) { for (CChan* pChan : vChans) { // Core calls this only for enabled channels, but // OnSendToIRCMessage() below calls OnQuit() for all channels. if (pChan->IsDisabled()) continue; PutLog("*** Quits: " + Nick.GetNick() + " (" + Nick.GetIdent() + "@" + Nick.GetHost() + ") (" + sMessage + ")", *pChan); } } } CModule::EModRet CLogMod::OnSendToIRCMessage(CMessage& Message) { if (Message.GetType() != CMessage::Type::Quit) { return CONTINUE; } CIRCNetwork* pNetwork = Message.GetNetwork(); OnQuit(pNetwork->GetIRCNick(), Message.As().GetReason(), pNetwork->GetChans()); return CONTINUE; } void CLogMod::OnJoinMessage(CJoinMessage& Message) { if (!NeedJoins()) return; const CNick& Nick = Message.GetNick(); CChan& Channel = *Message.GetChan(); // TODO: Move account logic to a separate Message method. CString sAccount = Message.GetTag("account"); const char* s = " "; if (sAccount.empty()) sAccount = Message.GetParam(1); if (sAccount.empty() || sAccount == "*") { sAccount = ""; s = ""; } PutLog("*** Joins: " + Nick.GetNick() + " (" + Nick.GetIdent() + "@" + Nick.GetHost() + ")" + s + sAccount, Channel); } void CLogMod::OnPart(const CNick& Nick, CChan& Channel, const CString& sMessage) { PutLog("*** Parts: " + Nick.GetNick() + " (" + Nick.GetIdent() + "@" + Nick.GetHost() + ") (" + sMessage + ")", Channel); } void CLogMod::OnNick(const CNick& OldNick, const CString& sNewNick, const vector& vChans) { if (NeedNickChanges()) { for (CChan* pChan : vChans) PutLog("*** " + OldNick.GetNick() + " is now known as " + sNewNick, *pChan); } } CModule::EModRet CLogMod::OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) { PutLog("*** " + Nick.GetNick() + " changes topic to '" + sTopic + "'", Channel); return CONTINUE; } /* notices */ CModule::EModRet CLogMod::OnUserNotice(CString& sTarget, CString& sMessage) { CIRCNetwork* pNetwork = GetNetwork(); if (pNetwork) { PutLog("-" + pNetwork->GetCurNick() + "- " + sMessage, sTarget); } return CONTINUE; } CModule::EModRet CLogMod::OnPrivNotice(CNick& Nick, CString& sMessage) { PutLog("-" + Nick.GetNick() + "- " + sMessage, Nick); return CONTINUE; } CModule::EModRet CLogMod::OnChanNotice(CNick& Nick, CChan& Channel, CString& sMessage) { PutLog("-" + Nick.GetNick() + "- " + sMessage, Channel); return CONTINUE; } /* actions */ CModule::EModRet CLogMod::OnUserAction(CString& sTarget, CString& sMessage) { CIRCNetwork* pNetwork = GetNetwork(); if (pNetwork) { PutLog("* " + pNetwork->GetCurNick() + " " + sMessage, sTarget); } return CONTINUE; } CModule::EModRet CLogMod::OnPrivAction(CNick& Nick, CString& sMessage) { PutLog("* " + Nick.GetNick() + " " + sMessage, Nick); return CONTINUE; } CModule::EModRet CLogMod::OnChanAction(CNick& Nick, CChan& Channel, CString& sMessage) { PutLog("* " + Nick.GetNick() + " " + sMessage, Channel); return CONTINUE; } /* msgs */ CModule::EModRet CLogMod::OnUserMsg(CString& sTarget, CString& sMessage) { CIRCNetwork* pNetwork = GetNetwork(); if (pNetwork) { PutLog("<" + pNetwork->GetCurNick() + "> " + sMessage, sTarget); } return CONTINUE; } CModule::EModRet CLogMod::OnPrivMsg(CNick& Nick, CString& sMessage) { PutLog("<" + Nick.GetNick() + "> " + sMessage, Nick); return CONTINUE; } CModule::EModRet CLogMod::OnChanMsg(CNick& Nick, CChan& Channel, CString& sMessage) { PutLog("<" + Nick.GetNick() + "> " + sMessage, Channel); return CONTINUE; } template <> void TModInfo(CModInfo& Info) { Info.AddType(CModInfo::NetworkModule); Info.AddType(CModInfo::GlobalModule); Info.SetHasArgs(true); Info.SetArgsHelpText( Info.t_s("[-sanitize] Optional path where to store logs.")); Info.SetWikiPage("log"); } USERMODULEDEFS(CLogMod, t_s("Writes IRC logs."))