diff --git a/AUTHORS b/AUTHORS index 9f22d589..960566f9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,3 +22,4 @@ Sebastian Ramacher cnu - master of destruction (security issues) Ingmar "KiNgMaR" Runge Michael "Svedrin" Ziegler +Robert Lacroix (http://www.robertlacroix.com) diff --git a/modules/extra/antiidle.cpp b/modules/extra/antiidle.cpp new file mode 100644 index 00000000..dde19569 --- /dev/null +++ b/modules/extra/antiidle.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "Nick.h" +#include "User.h" + +class CAntiIdle; + +class CAntiIdleJob : public CTimer +{ +public: + CAntiIdleJob(CModule* pModule, unsigned int uInterval, unsigned int uCycles, const CString& sLabel, const CString& sDescription) + : CTimer(pModule, uInterval, uCycles, sLabel, sDescription) {} + virtual ~CAntiIdleJob() {} + +protected: + virtual void RunJob(); +}; + +class CAntiIdle : public CModule +{ +public: + MODCONSTRUCTOR(CAntiIdle) { + SetInterval(30); + } + + virtual ~CAntiIdle() {} + + virtual bool OnLoad(const CString& sArgs, CString& sErrorMsg) + { + if(!sArgs.Trim_n().empty()) + SetInterval(sArgs.ToInt()); + return true; + } + + virtual void OnModCommand( const CString& sCommand ) + { + CString sCmdName = sCommand.Token(0).AsLower(); + if(sCmdName == "set") + { + CString sInterval = sCommand.Token(1, true); + SetInterval(sInterval.ToInt()); + + if(m_uiInterval == 0) + PutModule("AntiIdle is now turned off."); + else + PutModule("AntiIdle is now set to " + CString(m_uiInterval) + " seconds."); + } else if(sCmdName == "off") { + SetInterval(0); + PutModule("AntiIdle is now turned off"); + } else if(sCmdName == "show") { + if(m_uiInterval == 0) + PutModule("AntiIdle is turned off."); + else + PutModule("AntiIdle is set to " + CString(m_uiInterval) + " seconds."); + } else { + PutModule("Commands: set, off, show"); + } + } + + virtual EModRet OnPrivMsg(CNick &Nick, CString &sMessage) + { + if(Nick.GetNick() == GetUser()->GetIRCNick().GetNick() + && sMessage == "\xAE") + return HALTCORE; + + return CONTINUE; + } + +private: + void SetInterval(int i) + { + if(i < 0) + return; + + m_uiInterval = i; + + RemTimer("AntiIdle"); + + if(m_uiInterval == 0) { + return; + } + + AddTimer(new CAntiIdleJob(this, m_uiInterval, 0, "AntiIdle", "Periodically sends a msg to the user")); + } + + unsigned int m_uiInterval; +}; + +//! This function sends a query with (r) back to the user +void CAntiIdleJob::RunJob() { + CString sNick = GetModule()->GetUser()->GetIRCNick().GetNick(); + GetModule()->PutIRC("PRIVMSG " + sNick + " :\xAE"); +} + +MODULEDEFS(CAntiIdle, "Hides your real idle time") diff --git a/modules/extra/autovoice.cpp b/modules/extra/autovoice.cpp new file mode 100644 index 00000000..7f37cf2b --- /dev/null +++ b/modules/extra/autovoice.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "Chan.h" +#include "User.h" + +class CAutoVoiceUser { +public: + CAutoVoiceUser() {} + + CAutoVoiceUser(const CString& sLine) { + FromString(sLine); + } + + CAutoVoiceUser(const CString& sUsername, const CString& sHostmask, const CString& sChannels) : + m_sUsername(sUsername), + m_sHostmask(sHostmask) { + AddChans(sChannels); + } + + virtual ~CAutoVoiceUser() {} + + const CString& GetUsername() const { return m_sUsername; } + const CString& GetHostmask() const { return m_sHostmask; } + + bool ChannelMatches(const CString& sChan) const { + for (set::const_iterator it = m_ssChans.begin(); it != m_ssChans.end(); it++) { + if (sChan.AsLower().WildCmp(*it)) { + return true; + } + } + + return false; + } + + bool HostMatches(const CString& sHostmask) { + return sHostmask.WildCmp(m_sHostmask); + } + + CString GetChannels() const { + CString sRet; + + for (set::const_iterator it = m_ssChans.begin(); it != m_ssChans.end(); it++) { + if (!sRet.empty()) { + sRet += " "; + } + + sRet += *it; + } + + return sRet; + } + + void DelChans(const CString& sChans) { + VCString vsChans; + sChans.Split(" ", vsChans); + + for (unsigned int a = 0; a < vsChans.size(); a++) { + m_ssChans.erase(vsChans[a].AsLower()); + } + } + + void AddChans(const CString& sChans) { + VCString vsChans; + sChans.Split(" ", vsChans); + + for (unsigned int a = 0; a < vsChans.size(); a++) { + m_ssChans.insert(vsChans[a].AsLower()); + } + } + + CString ToString() const { + CString sChans; + + for (set::const_iterator it = m_ssChans.begin(); it != m_ssChans.end(); it++) { + if (!sChans.empty()) { + sChans += " "; + } + + sChans += *it; + } + + return m_sUsername + "\t" + m_sHostmask + "\t" + sChans; + } + + bool FromString(const CString& sLine) { + m_sUsername = sLine.Token(0, false, "\t"); + m_sHostmask = sLine.Token(1, false, "\t"); + sLine.Token(2, false, "\t").Split(" ", m_ssChans); + + return !m_sHostmask.empty(); + } +private: +protected: + CString m_sUsername; + CString m_sHostmask; + set m_ssChans; +}; + +class CAutoVoiceMod : public CModule { +public: + MODCONSTRUCTOR(CAutoVoiceMod) {} + + virtual bool OnLoad(const CString& sArgs, CString& sMessage) { + // Load the chans from the command line + unsigned int a = 0; + CString sChan = sArgs.Token(a++); + + while (!sChan.empty()) { + CString sName = "Args"; + sName += CString(a); + AddUser(sName, "*", sChan); + + sChan = sArgs.Token(a++); + } + + // Load the saved users + for (MCString::iterator it = BeginNV(); it != EndNV(); it++) { + const CString& sLine = it->second; + CAutoVoiceUser* pUser = new CAutoVoiceUser; + + if (!pUser->FromString(sLine) || FindUser(pUser->GetUsername().AsLower())) { + delete pUser; + } else { + m_msUsers[pUser->GetUsername().AsLower()] = pUser; + } + } + + return true; + } + + virtual ~CAutoVoiceMod() { + for (map::iterator it = m_msUsers.begin(); it != m_msUsers.end(); it++) { + delete it->second; + } + + m_msUsers.clear(); + } + + virtual void OnJoin(const CNick& Nick, CChan& Channel) { + // If we have ops in this chan + if (Channel.HasPerm(CChan::Op) || Channel.HasPerm(CChan::HalfOp)) { + for (map::iterator it = m_msUsers.begin(); it != m_msUsers.end(); it++) { + // and the nick who joined is a valid user + if (it->second->HostMatches(Nick.GetHostMask()) && it->second->ChannelMatches(Channel.GetName())) { + PutIRC("MODE " + Channel.GetName() + " +v " + Nick.GetNick()); + break; + } + } + } + } + + virtual void OnModCommand(const CString& sLine) { + CString sCommand = sLine.Token(0).AsUpper(); + + if (sCommand.Equals("HELP")) { + PutModule("Commands are: ListUsers, AddChans, DelChans, AddUser, DelUser"); + } else if (sCommand.Equals("ADDUSER") || sCommand.Equals("DELUSER")) { + CString sUser = sLine.Token(1); + CString sHost = sLine.Token(2); + + if (sCommand.Equals("ADDUSER")) { + if (sHost.empty()) { + PutModule("Usage: " + sCommand + " [channels]"); + } else { + CAutoVoiceUser* pUser = AddUser(sUser, sHost, sLine.Token(3, true)); + + if (pUser) { + SetNV(sUser, pUser->ToString()); + } + } + } else { + DelUser(sUser); + DelNV(sUser); + } + } else if (sCommand.Equals("LISTUSERS")) { + if (m_msUsers.empty()) { + PutModule("There are no users defined"); + return; + } + + CTable Table; + + Table.AddColumn("User"); + Table.AddColumn("Hostmask"); + Table.AddColumn("Channels"); + + for (map::iterator it = m_msUsers.begin(); it != m_msUsers.end(); it++) { + Table.AddRow(); + Table.SetCell("User", it->second->GetUsername()); + Table.SetCell("Hostmask", it->second->GetHostmask()); + Table.SetCell("Channels", it->second->GetChannels()); + } + + PutModule(Table); + } else if (sCommand.Equals("ADDCHANS") || sCommand.Equals("DELCHANS")) { + CString sUser = sLine.Token(1); + CString sChans = sLine.Token(2, true); + + if (sChans.empty()) { + PutModule("Usage: " + sCommand + " [channel] ..."); + return; + } + + CAutoVoiceUser* pUser = FindUser(sUser); + + if (!pUser) { + PutModule("No such user"); + return; + } + + if (sCommand.Equals("ADDCHANS")) { + pUser->AddChans(sChans); + PutModule("Channel(s) added to user [" + pUser->GetUsername() + "]"); + } else { + pUser->DelChans(sChans); + PutModule("Channel(s) Removed from user [" + pUser->GetUsername() + "]"); + } + + SetNV(pUser->GetUsername(), pUser->ToString()); + } else { + PutModule("Unknown command, try HELP"); + } + } + + CAutoVoiceUser* FindUser(const CString& sUser) { + map::iterator it = m_msUsers.find(sUser.AsLower()); + + return (it != m_msUsers.end()) ? it->second : NULL; + } + + CAutoVoiceUser* FindUserByHost(const CString& sHostmask, const CString& sChannel = "") { + for (map::iterator it = m_msUsers.begin(); it != m_msUsers.end(); it++) { + CAutoVoiceUser* pUser = it->second; + + if (pUser->HostMatches(sHostmask) && (sChannel.empty() || pUser->ChannelMatches(sChannel))) { + return pUser; + } + } + + return NULL; + } + + void DelUser(const CString& sUser) { + map::iterator it = m_msUsers.find(sUser.AsLower()); + + if (it == m_msUsers.end()) { + PutModule("That user does not exist"); + return; + } + + delete it->second; + m_msUsers.erase(it); + PutModule("User [" + sUser + "] removed"); + } + + CAutoVoiceUser* AddUser(const CString& sUser, const CString& sHost, const CString& sChans) { + if (m_msUsers.find(sUser) != m_msUsers.end()) { + PutModule("That user already exists"); + return NULL; + } + + CAutoVoiceUser* pUser = new CAutoVoiceUser(sUser, sHost, sChans); + m_msUsers[sUser.AsLower()] = pUser; + PutModule("User [" + sUser + "] added with hostmask [" + sHost + "]"); + return pUser; + } + +private: + map m_msUsers; +}; + +MODULEDEFS(CAutoVoiceMod, "Auto voice the good guys") diff --git a/modules/extra/blockuser.cpp b/modules/extra/blockuser.cpp new file mode 100644 index 00000000..2815220e --- /dev/null +++ b/modules/extra/blockuser.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "User.h" +#include "IRCSock.h" +#include "znc.h" + +#define MESSAGE "Your account has been disabled. Contact your administrator." + +class CBlockUser : public CGlobalModule { +public: + GLOBALMODCONSTRUCTOR(CBlockUser) {} + + virtual ~CBlockUser() {} + + virtual bool OnLoad(const CString& sArgs, CString& sMessage) { + VCString vArgs; + VCString::iterator it; + MCString::iterator it2; + + // Load saved settings + for (it2 = BeginNV(); it2 != EndNV(); it2++) { + // Ignore errors + Block(it2->first); + } + + // Parse arguments, each argument is a user name to block + sArgs.Split(" ", vArgs, false); + + for (it = vArgs.begin(); it != vArgs.end(); it++) { + if (!Block(*it)) { + sMessage = "Could not block [" + *it + "]"; + return false; + } + } + + return true; + } + + virtual EModRet OnLoginAttempt(CSmartPtr Auth) { + if (IsBlocked(Auth->GetUsername())) { + Auth->RefuseLogin(MESSAGE); + return HALT; + } + + return CONTINUE; + } + + void OnModCommand(const CString& sCommand) { + CString sCmd = sCommand.Token(0); + + if (sCmd.Equals("list")) { + CTable Table; + MCString::iterator it; + + Table.AddColumn("Blocked user"); + + for (it = BeginNV(); it != EndNV(); it++) { + Table.AddRow(); + Table.SetCell("Blocked user", it->first); + } + + if (PutModule(Table) == 0) + PutModule("No users blocked"); + } else if (sCmd.Equals("block")) { + CString sUser = sCommand.Token(1, true); + + if (m_pUser->GetUserName().Equals(sUser)) { + PutModule("You can't block yourself"); + return; + } + + if (Block(sUser)) + PutModule("Blocked [" + sUser + "]"); + else + PutModule("Could not block [" + sUser + "] (misspelled?)"); + } else if (sCmd.Equals("unblock")) { + CString sUser = sCommand.Token(1, true); + + if (DelNV(sUser)) + PutModule("Unblocked [" + sUser + "]"); + else + PutModule("This user is not blocked"); + } else if (sCmd.Equals("help")) { + PutModule("Commands: list, block [user], unblock [user]"); + } + } + +private: + bool IsBlocked(const CString& sUser) { + MCString::iterator it; + for (it = BeginNV(); it != EndNV(); it++) { + if (sUser.Equals(it->first)) { + return true; + } + } + return false; + } + + bool Block(const CString& sUser) { + CUser *pUser = CZNC::Get().FindUser(sUser); + + if (!pUser) + return false; + + // Disconnect all clients + vector& vpClients = pUser->GetClients(); + vector::iterator it; + for (it = vpClients.begin(); it != vpClients.end(); it++) { + (*it)->PutStatusNotice(MESSAGE); + (*it)->Close(Csock::CLT_AFTERWRITE); + } + + // Disconnect from IRC... + CIRCSock *pIRCSock = pUser->GetIRCSock(); + if (pIRCSock) { + pIRCSock->Quit(); + } + + // ...and don't reconnect + pUser->SetIRCConnectEnabled(false); + + SetNV(pUser->GetUserName(), ""); + return true; + } +}; + +GLOBALMODULEDEFS(CBlockUser, "Block certain users from logging in") diff --git a/modules/extra/connect_throttle.cpp b/modules/extra/connect_throttle.cpp new file mode 100644 index 00000000..e1cb1246 --- /dev/null +++ b/modules/extra/connect_throttle.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "main.h" +#include "User.h" +#include "Nick.h" +#include "Modules.h" +#include "Chan.h" +#include "znc.h" + +class CConnectThrottleMod : public CGlobalModule { +public: + GLOBALMODCONSTRUCTOR(CConnectThrottleMod) {} + virtual ~CConnectThrottleMod() {} + + virtual bool OnLoad(const CString& sArgs, CString& sMessage) + { + unsigned int timeout = sArgs.ToUInt(); + + if (sArgs.empty()) { + timeout = 60; // 1 min + } else if (timeout == 0 && sArgs != "0") { + sMessage = "Invalid argument, must be a positive number " + "which is the time one has to wait after failed login attempts"; + return false; + } + + // SetTTL() wants milliseconds + m_Cache.SetTTL(timeout * 1000); + + return true; + } + + virtual void OnClientConnect(CClient* pClient, const CString& sHost, unsigned short uPort) { + if (sHost.empty() || !m_Cache.HasItem(sHost)) { + return; + } + + // refresh their ban + m_Cache.AddItem(sHost); + + pClient->PutClient("ERROR :Closing link [Please try again later - reconnecting too fast]"); + pClient->Close(Csock::CLT_AFTERWRITE); + } + + virtual void OnFailedLogin(const CString& sUsername, const CString& sRemoteIP) { + m_Cache.AddItem(sRemoteIP); + } + + virtual EModRet OnLoginAttempt(CSmartPtr Auth) { + // e.g. webadmin ends up here + const CString &sRemoteIP = Auth->GetRemoteIP(); + + if (sRemoteIP.empty()) + return CONTINUE; + + if (m_Cache.HasItem(sRemoteIP)) { + // refresh their ban + m_Cache.AddItem(sRemoteIP); + + Auth->RefuseLogin("Please try again later - reconnecting too fast"); + return HALT; + } + + return CONTINUE; + } + +private: + TCacheMap m_Cache; +}; + +GLOBALMODULEDEFS(CConnectThrottleMod, "Limit the number of login attempts a user can make per time") diff --git a/modules/extra/ctcpflood.cpp b/modules/extra/ctcpflood.cpp new file mode 100644 index 00000000..7f756f12 --- /dev/null +++ b/modules/extra/ctcpflood.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "Modules.h" +#include "Chan.h" + +class CCtcpFloodMod : public CModule { +public: + MODCONSTRUCTOR(CCtcpFloodMod) { + m_tLastCTCP = 0; + m_iNumCTCP = 0; + } + + ~CCtcpFloodMod() { + } + + void Save() { + // We save the settings twice because the module arguments can + // be more easily edited via webadmin, while the SetNV() stuff + // survives e.g. /msg *status reloadmod ctcpflood. + SetNV("secs", CString(m_iThresholdSecs)); + SetNV("msgs", CString(m_iThresholdMsgs)); + + SetArgs(CString(m_iThresholdMsgs) + " " + CString(m_iThresholdSecs)); + } + + bool OnLoad(const CString& sArgs, CString& sMessage) { + m_iThresholdMsgs = sArgs.Token(0).ToUInt(); + m_iThresholdSecs = sArgs.Token(1).ToUInt(); + + if (m_iThresholdMsgs == 0 || m_iThresholdSecs == 0) { + m_iThresholdMsgs = GetNV("msgs").ToUInt(); + m_iThresholdSecs = GetNV("secs").ToUInt(); + } + + if (m_iThresholdSecs == 0) + m_iThresholdSecs = 2; + if (m_iThresholdMsgs == 0) + m_iThresholdMsgs = 4; + + Save(); + + return true; + } + + EModRet Message(const CNick& Nick, const CString& sMessage) { + // We never block /me, because it doesn't cause a reply + if (sMessage.Token(0).Equals("ACTION")) + return CONTINUE; + + if (m_tLastCTCP + m_iThresholdSecs < time(NULL)) { + m_tLastCTCP = time(NULL); + m_iNumCTCP = 0; + } + + m_iNumCTCP++; + + if (m_iNumCTCP < m_iThresholdMsgs) + return CONTINUE; + else if (m_iNumCTCP == m_iThresholdMsgs) + PutModule("Limit reached by [" + Nick.GetHostMask() + "], blocking all CTCP"); + + // Reset the timeout so that we continue blocking messages + m_tLastCTCP = time(NULL); + + return HALT; + } + + EModRet OnPrivCTCP(CNick& Nick, CString& sMessage) { + return Message(Nick, sMessage); + } + + EModRet OnChanCTCP(CNick& Nick, CChan& Channel, CString& sMessage) { + return Message(Nick, sMessage); + } + + void OnModCommand(const CString& sCommand) { + const CString& sCmd = sCommand.Token(0); + const CString& sArg = sCommand.Token(1, true); + + if (sCmd.Equals("secs") && !sArg.empty()) { + m_iThresholdSecs = sArg.ToUInt(); + if (m_iThresholdSecs == 0) + m_iThresholdSecs = 1; + + PutModule("Set seconds limit to [" + CString(m_iThresholdSecs) + "]"); + Save(); + } else if (sCmd.Equals("lines") && !sArg.empty()) { + m_iThresholdMsgs = sArg.ToUInt(); + if (m_iThresholdMsgs == 0) + m_iThresholdMsgs = 2; + + PutModule("Set lines limit to [" + CString(m_iThresholdMsgs) + "]"); + Save(); + } else if (sCmd.Equals("show")) { + PutModule("Current limit is " + CString(m_iThresholdMsgs) + " CTCPs " + "in " + CString(m_iThresholdSecs) + " secs"); + } else { + PutModule("Commands: show, secs [limit], lines [limit]"); + } + } + +private: + time_t m_tLastCTCP; + unsigned int m_iNumCTCP; + + time_t m_iThresholdSecs; + unsigned int m_iThresholdMsgs; +}; + +MODULEDEFS(CCtcpFloodMod, "Don't forward CTCP floods to clients") diff --git a/modules/extra/discon_kick.cpp b/modules/extra/discon_kick.cpp new file mode 100644 index 00000000..f31aa9e5 --- /dev/null +++ b/modules/extra/discon_kick.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + + +#include "Modules.h" +#include "User.h" +#include "Chan.h" + +class CKickClientOnIRCDisconnect: public CModule { +public: + MODCONSTRUCTOR(CKickClientOnIRCDisconnect) {} + + void OnIRCDisconnected() + { + const vector& vChans = m_pUser->GetChans(); + + for(vector::const_iterator it = vChans.begin(); it != vChans.end(); it++) + { + PutUser(":ZNC!znc@znc.in KICK " + (*it)->GetName() + " " + m_pUser->GetIRCNick().GetNick() + + " :You have been disconnected from the IRC server"); + } + } +}; + +MODULEDEFS(CKickClientOnIRCDisconnect, "Kicks the client from all channels when the connection to the IRC server is lost") diff --git a/modules/extra/droproot.cpp b/modules/extra/droproot.cpp new file mode 100644 index 00000000..cd17402d --- /dev/null +++ b/modules/extra/droproot.cpp @@ -0,0 +1,144 @@ +/* + * droproot.cpp + * + * Copyright (c) 2009 Vadtec (vadtec@vadtec.net) + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + * + * Copyright (C) 2004-2008 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "znc.h" +#include "User.h" +#include +#include + +class CDroproot : public CGlobalModule { + +public: + GLOBALMODCONSTRUCTOR(CDroproot) { + } + + virtual ~CDroproot() { + } + + uid_t GetUser(const CString& sUser, CString& sMessage) { + uid_t ret = sUser.ToUInt(); + + if (ret != 0) + return ret; + + struct passwd *pUser = getpwnam(sUser.c_str()); + + if (!pUser) { + sMessage = "User [" + sUser + "] not found!"; + return 0; + } + + return pUser->pw_uid; + } + + gid_t GetGroup(const CString& sGroup, CString& sMessage) { + gid_t ret = sGroup.ToUInt(); + + if (ret != 0) + return ret; + + struct group *pGroup = getgrnam(sGroup.c_str()); + + if (!pGroup) { + sMessage = "Group [" + sGroup + "] not found!"; + return 0; + } + + return pGroup->gr_gid; + } + + virtual bool OnLoad(const CString& sArgs, CString& sMessage) { + CString sUser = sArgs.Token(0); + CString sGroup = sArgs.Token(1, true); + + if (sUser.empty() || sGroup.empty()) { + sMessage = "Usage: LoadModule = Droproot "; + return false; + } + + m_user = GetUser(sUser, sMessage); + + if (m_user == 0) { + sMessage + = "Error: Cannot run as root, check your config file | Useage: LoadModule = Droproot "; + return false; + } + + m_group = GetGroup(sGroup, sMessage); + + if (m_group == 0) { + sMessage + = "Error: Cannot run as root, check your config file | Useage: LoadModule = Droproot "; + return false; + } + + return true; + } + + virtual bool OnBoot() { + int u, eu, g, eg, sg; + + if ((geteuid() == 0) || (getuid() == 0) || (getegid() == 0) || (getgid() + == 0)) { + + CUtils::PrintAction("Dropping root permissions"); + + // Clear all the supplementary groups + sg = setgroups(0, NULL); + + if (sg < 0) { + CUtils::PrintStatus(false, + "Could not remove supplementary groups! [" + + CString(strerror(errno)) + "]"); + + return false; + } + + // Set the group (if we are root, this sets all three group IDs) + g = setgid(m_group); + eg = setegid(m_group); + + if ((g < 0) || (eg < 0)) { + CUtils::PrintStatus(false, "Could not switch group id! [" + + CString(strerror(errno)) + "]"); + + return false; + } + + // and set the user (if we are root, this sets all three user IDs) + u = setuid(m_user); + eu = seteuid(m_user); + + if ((u < 0) || (eu < 0)) { + CUtils::PrintStatus(false, "Could not switch user id! [" + + CString(strerror(errno)) + "]"); + + return false; + } + + CUtils::PrintStatus(true); + + return true; + } + + return true; + } + +protected: + uid_t m_user; + gid_t m_group; +}; + +GLOBALMODULEDEFS(CDroproot, "Allows ZNC to drop root privileges and run as an un-privileged user.") diff --git a/modules/extra/fakeonline.cpp b/modules/extra/fakeonline.cpp new file mode 100644 index 00000000..ef3ff586 --- /dev/null +++ b/modules/extra/fakeonline.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2008-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "User.h" +#include "znc.h" + +class CFOModule : public CModule { +public: + MODCONSTRUCTOR(CFOModule) {} + virtual ~CFOModule() {} + + bool IsOnlineModNick(const CString& sNick) { + const CString& sPrefix = m_pUser->GetStatusPrefix(); + if (!sNick.Equals(sPrefix, false, sPrefix.length())) + return false; + + CString sModNick = sNick.substr(sPrefix.length()); + if (!sModNick.Equals("status") && + !m_pUser->GetModules().FindModule(sModNick) && + !CZNC::Get().GetModules().FindModule(sModNick)) + return false; + return true; + } + + virtual EModRet OnUserRaw(CString& sLine) { + //Handle ISON + if (sLine.Token(0).Equals("ison")) { + VCString vsNicks; + VCString::const_iterator it; + + // Get the list of nicks which are being asked for + sLine.Token(1, true).TrimLeft_n(":").Split(" ", vsNicks, false); + + CString sBNCNicks = ""; + for (it = vsNicks.begin(); it != vsNicks.end(); it++) { + if (IsOnlineModNick(*it)) { + sBNCNicks += " " + *it; + } + } + // Remove the leading space + sBNCNicks.LeftChomp(); + + // We let the server handle this request and then act on + // the 303 response. + m_ISONRequests.push_back(sBNCNicks); + } + + //Handle WHOIS + if (sLine.Token(0).Equals("whois")) { + CString sNick = sLine.Token(1); + + if (IsOnlineModNick(sNick)) { + PutUser(":znc.in 311 " + m_pUser->GetCurNick() + " " + sNick + " " + sNick + " znc.in * :" + sNick); + PutUser(":znc.in 312 " + m_pUser->GetCurNick() + " " + sNick + " *.znc.in :Bouncer"); + PutUser(":znc.in 318 " + m_pUser->GetCurNick() + " " + sNick + " :End of /WHOIS list."); + + return HALT; + } + } + + return CONTINUE; + } + + virtual EModRet OnRaw(CString& sLine) { + //Handle 303 reply if m_Requests is not empty + if (sLine.Token(1) == "303" && !m_ISONRequests.empty()) { + VCString::iterator it = m_ISONRequests.begin(); + + sLine.Trim(); + + // Only append a space if this isn't an empty reply + if (sLine.Right(1) != ":") { + sLine += " "; + } + + //add BNC nicks to the reply + sLine += *it; + m_ISONRequests.erase(it); + } + + return CONTINUE; + } + +private: + VCString m_ISONRequests; +}; + +MODULEDEFS(CFOModule, "Fakes online status of ZNC *-users.") diff --git a/modules/extra/lastseen.cpp b/modules/extra/lastseen.cpp new file mode 100644 index 00000000..6cbbcac8 --- /dev/null +++ b/modules/extra/lastseen.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "User.h" +#include "znc.h" + +using std::map; + +class CLastSeenMod : public CGlobalModule { +public: + GLOBALMODCONSTRUCTOR(CLastSeenMod) + { + } + + virtual ~CLastSeenMod() {} + + time_t GetTime(CUser *pUser) + { + return GetNV(pUser->GetUserName()).ToULong(); + } + + void SetTime(CUser *pUser) + { + SetNV(pUser->GetUserName(), CString(time(NULL))); + } + + virtual void OnModCommand(const CString& sLine) + { + const CString sCommand = sLine.Token(0).AsLower(); + + if (!GetUser()->IsAdmin()) { + PutModule("Access denied"); + return; + } + + if (sCommand == "show") { + char buf[1024]; + const map& mUsers = CZNC::Get().GetUserMap(); + map::const_iterator it; + CTable Table; + + Table.AddColumn("User"); + Table.AddColumn("Last Seen"); + + for (it = mUsers.begin(); it != mUsers.end(); it++) { + CUser *pUser = it->second; + time_t last = GetTime(pUser); + + Table.AddRow(); + Table.SetCell("User", it->first); + + if (last == 0) + Table.SetCell("Last Seen", "never"); + else { + strftime(buf, sizeof(buf), "%c", localtime(&last)); + Table.SetCell("Last Seen", buf); + } + } + + PutModule(Table); + } else { + PutModule("This module only supports 'show'"); + } + } + + virtual void OnClientLogin() + { + SetTime(GetUser()); + } + + virtual void OnClientDisconnect() + { + SetTime(GetUser()); + } + +private: +}; + +GLOBALMODULEDEFS(CLastSeenMod, "Collects data about when a user last logged in") diff --git a/modules/extra/listsockets.cpp b/modules/extra/listsockets.cpp new file mode 100644 index 00000000..2c463d9a --- /dev/null +++ b/modules/extra/listsockets.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "Modules.h" +#include "User.h" +#include "znc.h" +#include + +class CSocketSorter { +public: + CSocketSorter(Csock* p) { + m_pSock = p; + } + bool operator<(const CSocketSorter& other) const { + // The 'biggest' item is displayed first. + // return false: this is first + // return true: other is first + + // Listeners go to the top + if (m_pSock->GetType() != other.m_pSock->GetType()) { + if (m_pSock->GetType() == Csock::LISTENER) + return false; + if (other.m_pSock->GetType() == Csock::LISTENER) + return true; + } + const CString& sMyName = m_pSock->GetSockName(); + const CString& sMyName2 = sMyName.Token(1, true, "::"); + bool bMyEmpty = sMyName2.empty(); + const CString& sHisName = other.GetSock()->GetSockName(); + const CString& sHisName2 = sHisName.Token(1, true, "::"); + bool bHisEmpty = sHisName2.empty(); + + // Then sort by first token after "::" + if (bMyEmpty && !bHisEmpty) + return false; + if (bHisEmpty && !bMyEmpty) + return true; + + if (!bMyEmpty && !bHisEmpty) { + int c = sMyName2.StrCmp(sHisName2); + if (c < 0) + return false; + if (c > 0) + return true; + } + // and finally sort by the whole socket name + return sMyName.StrCmp(sHisName) > 0; + } + Csock* GetSock() const { return m_pSock; } +private: + Csock* m_pSock; +}; + +class CListSockets : public CModule { +public: + MODCONSTRUCTOR(CListSockets) {} + + virtual bool OnLoad(const CString& sArgs, CString& sMessage) + { +#ifndef MOD_LISTSOCKETS_ALLOW_EVERYONE + if (!m_pUser->IsAdmin()) { + sMessage = "You must be admin to use this module"; + return false; + } +#endif + + return true; + } + + virtual void OnModCommand(const CString& sLine) { + CString sCommand = sLine.Token(0); + CString sArg = sLine.Token(1, true); + + if (sCommand.Equals("LIST")) { + bool bShowHosts = true; + if (sArg.Equals("-n")) { + bShowHosts = false; + } + ShowSocks(bShowHosts); + } else { + PutModule("Use 'list' to view a list of active sockets"); + PutModule("Use 'list -n' if you want IP addresses to be displayed"); + } + } + + void ShowSocks(bool bShowHosts) { + CSockManager& m = CZNC::Get().GetManager(); + if (!m.size()) { + PutStatus("You have no open sockets."); + return; + } + + std::priority_queue socks; + + for (unsigned int a = 0; a < m.size(); a++) { + socks.push(m[a]); + } + + CTable Table; + Table.AddColumn("Name"); + Table.AddColumn("Created"); + Table.AddColumn("State"); +#ifdef HAVE_LIBSSL + Table.AddColumn("SSL"); +#endif + Table.AddColumn("Local"); + Table.AddColumn("Remote"); + + while (!socks.empty()) { + Csock* pSocket = socks.top().GetSock(); + socks.pop(); + + Table.AddRow(); + + switch (pSocket->GetType()) { + case Csock::LISTENER: + Table.SetCell("State", "Listen"); + break; + case Csock::INBOUND: + Table.SetCell("State", "Inbound"); + break; + case Csock::OUTBOUND: + if (pSocket->IsConnected()) + Table.SetCell("State", "Outbound"); + else + Table.SetCell("State", "Connecting"); + break; + default: + Table.SetCell("State", "UNKNOWN"); + break; + } + + unsigned long long iStartTime = pSocket->GetStartTime(); + time_t iTime = iStartTime / 1000; + Table.SetCell("Created", FormatTime("%Y-%m-%d %H:%M:%S", iTime)); + +#ifdef HAVE_LIBSSL + if (pSocket->GetSSL()) { + Table.SetCell("SSL", "Yes"); + } else { + Table.SetCell("SSL", "No"); + } +#endif + + + Table.SetCell("Name", pSocket->GetSockName()); + CString sVHost; + if (bShowHosts) { + sVHost = pSocket->GetBindHost(); + } + if (sVHost.empty()) { + sVHost = pSocket->GetLocalIP(); + } + Table.SetCell("Local", sVHost + " " + CString(pSocket->GetLocalPort())); + + CString sHost; + if (!bShowHosts) { + sHost = pSocket->GetRemoteIP(); + } + // While connecting, there might be no ip available + if (sHost.empty()) { + sHost = pSocket->GetHostName(); + } + + u_short uPort; + // While connecting, GetRemotePort() would return 0 + if (pSocket->GetType() == Csock::OUTBOUND) { + uPort = pSocket->GetPort(); + } else { + uPort = pSocket->GetRemotePort(); + } + if (uPort != 0) { + Table.SetCell("Remote", sHost + " " + CString(uPort)); + } else { + Table.SetCell("Remote", sHost); + } + } + + PutModule(Table); + return; + } + + virtual ~CListSockets() { + } + + CString FormatTime(const CString& sFormat, time_t tm = 0) const { + char szTimestamp[1024]; + + if (tm == 0) { + tm = time(NULL); + } + + // offset is in hours + tm += (time_t)(m_pUser->GetTimezoneOffset() * 60 * 60); + strftime(szTimestamp, sizeof(szTimestamp) / sizeof(char), + sFormat.c_str(), localtime(&tm)); + + return szTimestamp; + } +}; + +MODULEDEFS(CListSockets, "List active sockets") + diff --git a/modules/extra/log.cpp b/modules/extra/log.cpp new file mode 100644 index 00000000..a05f1fe3 --- /dev/null +++ b/modules/extra/log.cpp @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2008-2009 See the AUTHORS file for details. + * Copyright (C) 2006-2007, CNU (http://cnu.dieplz.net/znc) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "User.h" +#include "Chan.h" +#include "Server.h" + +class CLogMod: public CModule { +public: + MODCONSTRUCTOR(CLogMod) {} + + 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(); + + virtual void OnIRCConnected(); + virtual void OnIRCDisconnected(); + virtual EModRet OnBroadcast(CString& sMessage); + + virtual void OnRawMode(const CNick& OpNick, CChan& Channel, const CString& sModes, const CString& sArgs); + virtual void OnKick(const CNick& OpNick, const CString& sKickedNick, CChan& Channel, const CString& sMessage); + virtual void OnQuit(const CNick& Nick, const CString& sMessage, const vector& vChans); + virtual void OnJoin(const CNick& Nick, CChan& Channel); + virtual void OnPart(const CNick& Nick, CChan& Channel); + virtual void OnNick(const CNick& OldNick, const CString& sNewNick, const vector& vChans); + virtual EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic); + + /* notices */ + virtual EModRet OnUserNotice(CString& sTarget, CString& sMessage); + virtual EModRet OnPrivNotice(CNick& Nick, CString& sMessage); + virtual EModRet OnChanNotice(CNick& Nick, CChan& Channel, CString& sMessage); + + /* actions */ + virtual EModRet OnUserAction(CString& sTarget, CString& sMessage); + virtual EModRet OnPrivAction(CNick& Nick, CString& sMessage); + virtual EModRet OnChanAction(CNick& Nick, CChan& Channel, CString& sMessage); + + /* msgs */ + virtual EModRet OnUserMsg(CString& sTarget, CString& sMessage); + virtual EModRet OnPrivMsg(CNick& Nick, CString& sMessage); + virtual EModRet OnChanMsg(CNick& Nick, CChan& Channel, CString& sMessage); +}; + +void CLogMod::PutLog(const CString& sLine, const CString& sWindow /*= "Status"*/) +{ + CString sPath; + time_t curtime; + tm* timeinfo; + char buffer[1024]; + + time(&curtime); + // Don't forget the user's timezone offset (which is in hours and we want seconds) + curtime += (time_t) (m_pUser->GetTimezoneOffset() * 60 * 60); + timeinfo = localtime(&curtime); + + /* Generate file name: ~/.znc/users//moddata/log/WINDOW_YYYYMMDD.log */ + sPath = GetSavePath() + "/" + sWindow.Replace_n("/", "?") + "_"; + snprintf(buffer, sizeof(buffer), "%04d%02d%02d.log", timeinfo->tm_year + 1900, + timeinfo->tm_mon + 1, timeinfo->tm_mday); + sPath += buffer; + + CFile LogFile(sPath); + if (LogFile.Open(O_WRONLY | O_APPEND | O_CREAT)) + { + snprintf(buffer, sizeof(buffer), "[%02d:%02d:%02d] ", + timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec); + + LogFile.Write(buffer + 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 = m_pUser->GetCurrentServer(); + CString sSSL; + + if (!pServer) + return "(no server)"; + + if (pServer->IsSSL()) + sSSL = "+"; + return pServer->GetName() + ":" + sSSL + CString(pServer->GetPort()); +} + +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::OnRawMode(const CNick& OpNick, CChan& Channel, const CString& sModes, const CString& sArgs) +{ + PutLog("*** " + OpNick.GetNick() + " 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) +{ + for (std::vector::const_iterator pChan = vChans.begin(); pChan != vChans.end(); ++pChan) + PutLog("*** Quits: " + Nick.GetNick() + " (" + Nick.GetIdent() + "@" + Nick.GetHost() + ") (" + sMessage + ")", **pChan); +} + +void CLogMod::OnJoin(const CNick& Nick, CChan& Channel) +{ + PutLog("*** Joins: " + Nick.GetNick() + " (" + Nick.GetIdent() + "@" + Nick.GetHost() + ")", Channel); +} + +void CLogMod::OnPart(const CNick& Nick, CChan& Channel) +{ + PutLog("*** Parts: " + Nick.GetNick() + " (" + Nick.GetIdent() + "@" + Nick.GetHost() + ")", Channel); +} + +void CLogMod::OnNick(const CNick& OldNick, const CString& sNewNick, const vector& vChans) +{ + for (std::vector::const_iterator pChan = vChans.begin(); pChan != vChans.end(); ++pChan) + 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) +{ + PutLog("-" + GetUser()->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) +{ + PutLog("* " + GetUser()->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) +{ + PutLog("<" + GetUser()->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; +} + +MODULEDEFS(CLogMod, "Write IRC logs") diff --git a/modules/extra/motdfile.cpp b/modules/extra/motdfile.cpp new file mode 100644 index 00000000..64e3e463 --- /dev/null +++ b/modules/extra/motdfile.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "Modules.h" +#include "Client.h" + +class CMotdFileMod : public CGlobalModule { +public: + GLOBALMODCONSTRUCTOR(CMotdFileMod) {} + virtual ~CMotdFileMod() {} + + virtual bool OnLoad(const CString& sArgs, CString& sMessage) { + if (sArgs.empty()) { + sMessage = "Argument must be path to a MOTD file"; + return false; + } + return true; + } + + virtual void OnClientLogin() { + CString sFile = GetArgs(); + CString sBuffer; + CFile cFile(sFile); + if (!cFile.Open(O_RDONLY)) { + m_pClient->PutStatusNotice("Could not open MOTD file"); + return; + } + while (cFile.ReadLine(sBuffer)) + m_pClient->PutStatusNotice(sBuffer); + cFile.Close(); + } +}; + +GLOBALMODULEDEFS(CMotdFileMod, "Send MOTD from a file") diff --git a/modules/extra/notify_connect.cpp b/modules/extra/notify_connect.cpp new file mode 100644 index 00000000..c392c299 --- /dev/null +++ b/modules/extra/notify_connect.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "main.h" +#include "User.h" +#include "Nick.h" +#include "Modules.h" +#include "Chan.h" +#include "znc.h" + +class CNotifyConnectMod : public CGlobalModule { +public: + GLOBALMODCONSTRUCTOR(CNotifyConnectMod) {} + + virtual void OnClientLogin() + { + SendAdmins(m_pUser->GetUserName() + " attached"); + } + + virtual void OnClientDisconnect() + { + SendAdmins(m_pUser->GetUserName() + " detached"); + } + +private: + void SendAdmins(const CString &msg) + { + CZNC::Get().Broadcast(msg, true, NULL, GetClient()); + } +}; + +GLOBALMODULEDEFS(CNotifyConnectMod, ""); diff --git a/modules/extra/send_raw.cpp b/modules/extra/send_raw.cpp new file mode 100644 index 00000000..8244dd60 --- /dev/null +++ b/modules/extra/send_raw.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2004-2009 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "User.h" +#include "znc.h" + +class CSendRaw_Mod: public CGlobalModule { +public: + GLOBALMODCONSTRUCTOR(CSendRaw_Mod) {} + + virtual ~CSendRaw_Mod() { + } + + virtual void OnModCommand(const CString& sLine) { + CString sUser = sLine.Token(0); + CString sSend = sLine.Token(1, true); + CUser *pUser; + + if (!m_pUser->IsAdmin()) { + PutModule("You must have admin privileges to use this"); + return; + } + + pUser = CZNC::Get().FindUser(sUser); + + if (!pUser) { + PutModule("User not found"); + PutModule("The expected format is: "); + return; + } + + pUser->PutIRC(sSend); + PutModule("done"); + } +}; + +GLOBALMODULEDEFS(CSendRaw_Mod, "Let's you send some raw IRC lines as someone else");