/* * Copyright (C) 2004-2013 ZNC, see the NOTICE file for details. * Copyright (C) 2013 Rasmus Eskola * based on sample.cpp sample module code * * 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 #include #include class CBacklogMod : public CModule { public: MODCONSTRUCTOR(CBacklogMod) {} virtual bool OnLoad(const CString& sArgs, CString& sMessage); virtual ~CBacklogMod(); virtual void OnModCommand(const CString& sCommand); }; bool CBacklogMod::OnLoad(const CString& sArgs, CString& sMessage) { CString LogPath = sArgs; if(LogPath.empty()) { LogPath = GetNV("LogPath"); if(LogPath.empty()) { // TODO: guess logpath? PutModule("LogPath is empty, set it with the LogPath command (help for more info)"); } } else { SetNV("LogPath", LogPath); PutModule("LogPath set to: " + LogPath); } return true; } CBacklogMod::~CBacklogMod() { } void CBacklogMod::OnModCommand(const CString& sCommand) { CString Arg = sCommand.Token(1); if (sCommand.Token(0).Equals("help")) { // TODO: proper help text, look how AddHelpCommand() does it in other ZNC code PutModule("Usage:"); PutModule(" [num-lines] (e.g. #foo 42)"); PutModule(""); PutModule("Commands:"); PutModule("Help (print this text)"); PutModule("LogPath (use keywords $USER, $NETWORK, $WINDOW, $user, $network and an asterisk * for date)"); PutModule("PrintStatusMsgs true/false (print join/part/rename messages)"); return; } else if (sCommand.Token(0).Equals("logpath")) { if(Arg.empty()) { PutModule("Usage: LogPath (use keywords $USER, $NETWORK, $WINDOW, $user, $network and an asterisk * for date:)"); PutModule("Current LogPath is set to: " + GetNV("LogPath")); return; } CString LogPath = sCommand.Token(1, true); SetNV("LogPath", LogPath); PutModule("LogPath set to: " + LogPath); return; } else if (sCommand.Token(0).Equals("PrintStatusMsgs")) { if(Arg.empty() || (!Arg.Equals("true", true) && !Arg.Equals("false", true))) { PutModule("Usage: PrintStatusMsgs true/false"); return; } SetNV("PrintStatusMsgs", Arg); PutModule("PrintStatusMsgs set to: " + Arg); return; } // TODO: handle these differently depending on how the module was loaded CString User = (m_pUser ? m_pUser->GetUserName() : "UNKNOWN"); CString Network = (m_pNetwork ? m_pNetwork->GetName() : "znc"); CString Channel = sCommand.Token(0); int printedLines = 0; int reqLines = sCommand.Token(1).ToInt(); if(reqLines <= 0) { reqLines = 150; } reqLines = std::max(std::min(reqLines, 1000), 1); CString Path = GetNV("LogPath").substr(); // make copy Path.Replace("$NETWORK", Network); Path.Replace("$network", Network.AsLower()); Path.Replace("$WINDOW", CString(Channel.Replace_n("/", "-").Replace_n("\\", "-")).AsLower()); Path.Replace("$USER", User); Path.Replace("$user", User.AsLower()); CString DirPath = Path.substr(0, Path.find_last_of("/")); CString FilePath; std::vector FileList; std::vector LinesToPrint; // gather list of all log files for requested channel/window DIR *dir; struct dirent *ent; if ((dir = opendir (DirPath.c_str())) != NULL) { while ((ent = readdir (dir)) != NULL) { FilePath = DirPath + "/" + ent->d_name; //PutModule("DEBUG: " + FilePath + " " + Path); if(FilePath.AsLower().StrCmp(Path.AsLower(), Path.find_last_of("*")) == 0) { FileList.push_back(FilePath); } } closedir (dir); } else { PutModule("Could not list directory " + DirPath + ": " + strerror(errno)); return; } std::sort(FileList.begin(), FileList.end()); // loop through list of log files one by one starting from most recent... for (std::vector::reverse_iterator it = FileList.rbegin(); it != FileList.rend(); ++it) { CFile LogFile(*it); CString Line; std::vector Lines; if (LogFile.GetShortName() == "." || LogFile.GetShortName() == "..") { continue; } if (LogFile.Open()) { Lines.push_back("[00:00:00] <***> File: " + LogFile.GetShortName()); //Parse name to get date prettier? printedLines--; while (LogFile.ReadLine(Line)) { try { // is line a part/join/rename etc message (nick ***), do we want to print it? // find nick by finding first whitespace, then moving one char right if(Line.at(Line.find_first_of(' ') + 1) == '*' && !GetNV("PrintStatusMsgs").ToBool()) { continue; } Lines.push_back(Line); } catch (int e) { // malformed log line, ignore PutModule("Malformed log line found in " + *it + ": " + Line); } } } else { PutModule("Could not open log file [" + sCommand + "]: " + strerror(errno)); continue; } LogFile.Close(); // loop through Lines in reverse order, push to LinesToPrint for (std::vector::reverse_iterator itl = Lines.rbegin(); itl != Lines.rend(); ++itl) { LinesToPrint.push_back(*itl); printedLines++; if(printedLines >= reqLines) { break; } } if(printedLines >= reqLines) { break; } } if(printedLines <= 0) { m_pNetwork->PutUser(":***!znc@znc.in PRIVMSG " + Channel + " :No log files found for window " + Channel + " in " + DirPath + "/", GetClient()); return; } else { //RFC prefixes: # ! & + if (Channel.StartsWith("#") || Channel.StartsWith("&") || Channel.StartsWith("!") || Channel.StartsWith("+")) { //Normal channel m_pNetwork->PutUser(":***!znc@znc.in PRIVMSG " + Channel + " :Backlog playback...", GetClient()); } else { m_pNetwork->PutUser(":" + Channel + "!znc@znc.in PRIVMSG " + Channel + " :<***> Backlog playback...", GetClient()); } } // now actually print for (std::vector::reverse_iterator it = LinesToPrint.rbegin(); it != LinesToPrint.rend(); ++it) { CString Line = *it; size_t FirstWS = Line.find_first_of(' '); // position of first whitespace char in line size_t NickLen = 3; CString Nick = "***"; CString MessageType = "PRIVMSG "; try { // a log line looks like: [HH:MM:SS] Message // < and > are illegal characters in nicknames, so we can // search for these if(Line.at(FirstWS + 1) == '<') { // normal message // nicklen includes surrounding < > NickLen = Line.find_first_of('>') - Line.find_first_of('<') + 1; // but we don't want them in Nick so subtract by two Nick = Line.substr(FirstWS + 2, NickLen - 2); } else if(Line.at(FirstWS + 1) == '-') { // IRC notice // nicklen includes surrounding - - NickLen = Line.find_first_of('-', FirstWS + 2) - Line.find_first_of('-') + 1; // but we don't want them in Nick so subtract by two Nick = Line.substr(FirstWS + 2, NickLen - 2); MessageType = "NOTICE "; } //RFC prefixes: # ! & + if (Channel.StartsWith("#") || Channel.StartsWith("&") || Channel.StartsWith("!") || Channel.StartsWith("+")) { //Normal channel m_pNetwork->PutUser(":" + Nick + "!znc@znc.in " + MessageType + Channel + " :" + Line.substr(0, FirstWS) + Line.substr(FirstWS + NickLen + 1, Line.npos), GetClient()); } else { //Privmsg m_pNetwork->PutUser(":" + Channel + "!znc@znc.in " + MessageType + Channel + " :" + Line.substr(0, FirstWS) + " <" + Nick + ">" + Line.substr(FirstWS + NickLen + 1, Line.npos), GetClient()); } } catch (int e) { PutModule("Malformed log line! " + Line); } } if (Channel.StartsWith("#") || Channel.StartsWith("&") || Channel.StartsWith("!") || Channel.StartsWith("+")) { //Normal channel m_pNetwork->PutUser(":***!znc@znc.in PRIVMSG " + Channel + " :" + "Playback Complete.", GetClient()); } else { m_pNetwork->PutUser(":" + Channel + "!znc@znc.in PRIVMSG " + Channel + " :<***> " + "Playback Complete.", GetClient()); } } template<> void TModInfo(CModInfo& Info) { Info.AddType(CModInfo::NetworkModule); Info.AddType(CModInfo::GlobalModule); Info.SetWikiPage("backlog"); Info.SetArgsHelpText("Takes as optional argument (and if given, sets) the log path, use keywords: $USER, $NETWORK and $WINDOW"); Info.SetHasArgs(true); } NETWORKMODULEDEFS(CBacklogMod, "Module for getting the last X lines of a channels log.")