Created
September 4, 2012 02:08
Revisions
-
garrettreid created this gist
Sep 4, 2012 .There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,633 @@ /* * Copyright (C) 2009 flakes @ EFNet * New match logic by Gm4n @ freenode * Version 1.0 (2012-08-19) * * 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. */ #define REQUIRESSL #include "znc.h" #include "User.h" #include "Chan.h" #include "Nick.h" #include "Modules.h" #include <string> #if (!defined(VERSION_MAJOR) || !defined(VERSION_MINOR) || (VERSION_MAJOR == 0 && VERSION_MINOR < 72)) #error This module needs ZNC 0.072 or newer. #endif class CProwlMod : public CModule { protected: // Internal variables CString m_host; unsigned int m_notificationsSent; time_t m_lastActivity; // Settings for prowl CString m_apiKey; int m_priority; CString m_subjectSuffix; // Settings for matching int m_idleThreshold; CString m_matchMode; CString m_chanOverride; CString m_privOverride; VCString m_hilights; public: MODCONSTRUCTOR(CProwlMod) { m_host = "api.prowlapp.com"; m_notificationsSent = 0; m_lastActivity = time(NULL); // defaults: m_priority = 0; m_idleThreshold = 5; m_matchMode = "basic"; m_chanOverride = "normal"; m_privOverride = "normal"; m_subjectSuffix = ""; // defaults end. } protected: static CString URLEscape(const CString& sStr) { return sStr.Escape_n(CString::EASCII, CString::EURL); } // This strips any forms of IRC formatting that google found for me static CString StripFormatting(const CString& sStr) { CString s; int state; int length = sStr.length(); char *content = (char *) malloc(length+1), *cptr = content; // Loop through the source string one character at a time for(int i = 0; i < length; i++){ switch(sStr.data()[i]) { case 0x03: // Remove color codes state = 0; i++; while(i < length) { // If we see a digit in 0 or 1, advance state if((state == 0 || state == 1) && (sStr.data()[i] >= 0x30 && sStr.data()[i] <= 0x39)) { i++; state++; continue; } // If we see a comma in either 1 or 2, move along to 3 if((state == 1 || state == 2) && sStr.data()[i] == ',') { i++; state = 3; continue; } // If we're at-or-post comma, a digit is all we need if((state == 3 || state == 4) && (sStr.data()[i] >= 0x30 || sStr.data()[i] <= 0x39)) { i++; state++; continue; } // If no other conditions have matched, this character can't be parsed - abort! i--; break; } // And now that we're done looping through this character break; case 0x02: // Remove start/end bold break; case 0x1F: // Remove start/end underline break; case 0x0F: // Remove reset color break; case 0x0E: // Remove italic/reverse break; default: // If this is a normal character, just copy it over *cptr = sStr.data()[i]; cptr++; break; } } // Null terminate our string and return *cptr = 0x00; s = CString(content); free(content); return s; } CString BuildRequest(const CString& sEvent, const CString& sDescription) { CString s; s += "GET /publicapi/add"; s += "?apikey=" + URLEscape(m_apiKey); s += "&priority=" + CString(m_priority); s += "&application=ZNC"; s += "&event=" + URLEscape(sEvent); s += "&description=" + URLEscape(StripFormatting(sDescription)); s += " HTTP/1.0\r\n"; s += "Connection: close\r\n"; s += "Host: " + m_host + "\r\n"; s += "User-Agent: " + CZNC::GetTag() + "\r\n"; s += "\r\n"; return s; } void SendNotification(const CString& sEvent, const CString& sMessage) { if(!m_apiKey.empty()) { CSocket *p = new CSocket(this); p->Connect(m_host, 443, true); // connect to host at port 443 using SSL p->Write(BuildRequest(sEvent, sMessage)); p->Close(Csock::CLT_AFTERWRITE); // discard the response... AddSocket(p); m_notificationsSent++; } } // Called internally to do "is this a hilight" logic bool CheckHilight(const CString& sMessage) { // Do case insensitive matching: const CString sLcMessage = sMessage.AsLower(); // Do some setup: const CString sLcNick = m_pUser->GetCurNick(); // Prepend current nick to list of hilights m_hilights.push_back(sLcNick); // Iterate through all the hilights, attempting one match at a time bool retval = false; for(VCString::iterator it = m_hilights.begin(); it != m_hilights.end(); it++) { // If the string is too short to match, do nothing if(sLcMessage.length() < (*it).length()) continue; // If lengths are equal, comparison is simple if(sLcMessage.length() == (*it).length()) { // If we're a direct match, we win. if(sLcMessage.CaseCmp(*it) == 0) { retval = true; break; } // If we don't match, this loop failed continue; } // Check message if we're using "addressed" type matching: if(m_matchMode.StrCmp("addressed") == 0) { // If the message starts with our nick if(strncmp(sLcMessage.data(), (*it).data(), (*it).length()) == 0) { // See if we have an immediate delimiter: char c = sLcMessage.data()[(*it).length()]; if(c == ' ' || c == ':' || c == ',') { retval = true; break; } } continue; } // Check message if we're using "basic" type matching: if(m_matchMode.StrCmp("basic") == 0) { // Check for a plain old match if(sLcMessage.find((*it).AsLower()) != CString::npos) { retval = true; break; } } } // Clean up, and return our result m_hilights.erase(m_hilights.begin()); return retval; } void ParsePrivMsg(const CString& sNick, const CString& sMessage) { // Sanity check if(!m_pUser) return; // If user is attached and recently active, return if (m_pUser->IsUserAttached() && m_lastActivity > time(NULL) - m_idleThreshold * 60) return; // Check if we have an override preventing match if (m_privOverride == "disable") return; // If we're here, send if override forces match, or if match if(m_privOverride == "force" || CheckHilight(sMessage)) { return SendNotification("Private message" + m_subjectSuffix, "<" + sNick + ">: " + sMessage); } } void ParseChanMsg(const CString& sNick, const CString &chan, const CString& sMessage) { // Sanity check if(!m_pUser) return; // If idle is non-zero and user is attached, user must be idle if (m_idleThreshold != 0 && m_pUser->IsUserAttached()) if (m_lastActivity > time(NULL) - m_idleThreshold * 60) return; // Check if we have an override preventing match if (m_chanOverride == "disable") return; // If we're here, send if override forces match, or if match if(m_chanOverride == "force" || CheckHilight(sMessage)) { return SendNotification(chan + " hilight" + m_subjectSuffix, "<" + sNick + ">: " + sMessage); } } void LoadSettings() { for(MCString::iterator it = BeginNV(); it != EndNV(); it++) { // Settings for prowl if(it->first == "api:key") { m_apiKey = it->second; } else if(it->first == "api:priority") { m_priority = it->second.ToInt(); } // Settings for matching else if(it->first == "u:idle") { m_idleThreshold = it->second.ToInt(); } else if(it->first == "u:matchmode") { m_matchMode = it->second; } else if(it->first == "u:privoverride") { m_privOverride = it->second; } else if(it->first == "u:chanoverride") { m_chanOverride = it->second; } else if(it->first == "u:suffix") { m_subjectSuffix = it->second; } else if(it->first == "u:hilights") { it->second.Split("\n", m_hilights, false); } } } void SaveSettings() { ClearNV(); // Settings for prowl SetNV("api:key", m_apiKey, false); SetNV("api:priority", CString(m_priority), false); // Settings for matching SetNV("u:idle", CString(m_idleThreshold), false); SetNV("u:matchmode", m_matchMode, false); SetNV("u:privoverride", m_privOverride, false); SetNV("u:chanoverride", m_chanOverride, false); SetNV("u:suffix", m_subjectSuffix, false); CString sTmp; for(VCString::const_iterator it = m_hilights.begin(); it != m_hilights.end(); it++) { sTmp += *it + "\n"; } SetNV("u:hilights", sTmp, true); } bool OnLoad(const CString& sArgs, CString& sMessage) { LoadSettings(); return true; } public: void OnModCommand(const CString& sCommand) { const CString sCmd = sCommand.Token(0).AsUpper(); if(sCmd == "HELP") { CTable CmdTable; CmdTable.AddColumn("Command"); CmdTable.AddColumn("Description"); CmdTable.AddRow(); CmdTable.SetCell("Command", "SET [<variable> [<value>]]"); CmdTable.SetCell("Description", "View and set configuration variables."); CmdTable.AddRow(); CmdTable.SetCell("Command", "STATUS"); CmdTable.SetCell("Description", "Show module status information."); CmdTable.AddRow(); CmdTable.SetCell("Command", "HIGHLIGHTS"); CmdTable.SetCell("Description", "Shows words (besides your nick) that trigger a notification."); CmdTable.AddRow(); CmdTable.SetCell("Command", "HIGHLIGHTS ADD <word>"); CmdTable.SetCell("Description", "Adds a word or string to match and notify."); CmdTable.AddRow(); CmdTable.SetCell("Command", "HIGHLIGHTS REMOVE <index>"); CmdTable.SetCell("Description", "Removes a word from the hilights list by index number."); CmdTable.AddRow(); CmdTable.SetCell("Command", "HELP"); CmdTable.SetCell("Description", "This help message."); PutModule(CmdTable); return; } const CString sSubCmd = sCommand.Token(1).AsLower(); if(sCmd == "SET" || sCmd == "CHANGE") { if(sSubCmd == "") { CTable CmdTable; CmdTable.AddColumn("Setting"); CmdTable.AddColumn("Description"); CmdTable.AddRow(); CmdTable.SetCell("Setting", "apikey <key>"); CmdTable.SetCell("Description", "Your prowl API key."); CmdTable.AddRow(); CmdTable.SetCell("Setting", "priority <number>"); CmdTable.SetCell("Description", "The priority of the delivered prowl notification."); CmdTable.AddRow(); CmdTable.SetCell("Setting", "idle <minutes>"); CmdTable.SetCell("Description", "Only send notifications if idle for <minutes> or not connected. 0 disables this check."); CmdTable.AddRow(); CmdTable.SetCell("Setting", "matchmode basic"); CmdTable.SetCell("Description", "Match any message containing a hilighted term (nickname or highlights list)."); CmdTable.AddRow(); CmdTable.SetCell("Setting", "matchmode addressed"); CmdTable.SetCell("Description", "Match if line begins with a hilighted term and is followed by a space, comma or colon."); CmdTable.AddRow(); CmdTable.SetCell("Setting", "privoverride <normal|disable|force>"); CmdTable.SetCell("Description", "For PMs, optionally disable hilights or force all messages to hilight."); CmdTable.AddRow(); CmdTable.SetCell("Setting", "chanoverride <normal|disable|force>"); CmdTable.SetCell("Description", "For channel messages, optionally disable hilights or force all messages to hilight."); CmdTable.AddRow(); CmdTable.SetCell("Setting", "suffix [string]"); CmdTable.SetCell("Description", "An optional suffix for message subjects to differentiate accounts. Eg 'on Freenode'."); PutModule(CmdTable); } if(sSubCmd == "apikey") { if(sCommand.Token(2) == "") { PutModule("Your API key is '" + m_apiKey + "'."); } else { m_apiKey = sCommand.Token(2).AsLower(); PutModule("Your API key is now '" + m_apiKey + "'."); } } else if(sSubCmd == "priority") { if(sCommand.Token(2) == "") { PutModule("Your priority is " + CString(m_priority) + "."); } else { m_priority = sCommand.Token(2).ToInt(); PutModule("Your priority is now " + CString(m_priority) + "."); } } else if(sSubCmd == "idle") { if(sCommand.Token(2) == "") { PutModule("Your idle time is " + CString(m_idleThreshold) + " minutes."); } else { m_idleThreshold = sCommand.Token(2).ToInt(); PutModule("Your idle time is now " + CString(m_idleThreshold) + " minutes."); } } else if(sSubCmd == "matchmode") { if(sCommand.Token(2) == "") { PutModule("Your matchmode is '" + m_matchMode + "'."); } else { const CString tmp = sCommand.Token(2).AsLower(); if(tmp == "basic" || tmp == "addressed") { m_matchMode = tmp; PutModule("Your matchmode is now '" + m_matchMode + "'."); } else { PutModule("Invalid matchmode '" + tmp + "'!"); } } } else if(sSubCmd == "privoverride") { if(sCommand.Token(2) == "") { PutModule("Your privoverride is '" + m_privOverride + "'."); } else { const CString tmp = sCommand.Token(2).AsLower(); if(tmp == "normal" || tmp == "disable" || tmp == "force") { m_privOverride = tmp; PutModule("Your privoverride is now '" + m_privOverride + "'."); } else { PutModule("Invalid privoverride '" + tmp + "'!"); } } } else if(sSubCmd == "chanoverride") { if(sCommand.Token(2) == "") { PutModule("Your chanoverride is '" + m_chanOverride + "'."); } else { const CString tmp = sCommand.Token(2).AsLower(); if(tmp == "normal" || tmp == "disable" || tmp == "force") { m_chanOverride = tmp; PutModule("Your chanoverride is now '" + m_chanOverride + "'."); } else { PutModule("Invalid chanoverride '" + tmp + "'!"); } } } else if(sSubCmd == "suffix") { if(sCommand.Token(2) == "") { PutModule("Your suffix is" + m_subjectSuffix + ". To unset, use suffix 'unset'."); } else if(sCommand.Token(2) == "unset") { m_subjectSuffix = ""; PutModule("Your suffix is now" + m_subjectSuffix + "."); } else { m_subjectSuffix = sCommand.LeftChomp_n(10); // strip off the "set suffix" PutModule("Your suffix is now" + m_subjectSuffix + "."); } } else if(sSubCmd != "") { PutModule("Unknown setting. Use SET to list available settings."); } // Save what we've done, so reboot doesn't lose anything SaveSettings(); } else if(sCmd == "HIGHLIGHTS" || sCmd == "HIGHLIGHT" || sCmd == "HILIGHTS" || sCmd == "HILIGHT") { if(sSubCmd == "") { size_t iIndex = 1; PutModule("Active additional hilights:"); for(VCString::const_iterator it = m_hilights.begin(); it != m_hilights.end(); it++) { PutModule(CString(iIndex) + ": " + *it); iIndex++; } PutModule("--End of list"); } else if(sSubCmd == "add") { const CString sParam = sCommand.Token(2, true); if(!sParam.empty()) { m_hilights.push_back(sParam); PutModule("Entry '" + sParam + "' added."); SaveSettings(); } else { PutModule("Usage: HIGHTLIGHTS ADD <string>"); } } else if(sSubCmd == "remove" || sSubCmd == "delete") { size_t iIndex = sCommand.Token(2).ToUInt(); if(iIndex > 0 && iIndex <= m_hilights.size()) { m_hilights.erase(m_hilights.begin() + iIndex - 1); PutModule("Entry removed."); SaveSettings(); } else { PutModule("Invalid list index."); } } else { PutModule("Unknown action. Try HELP."); } } else if(sCmd == "STATUS" || sCmd == "SHOW") { CTable CmdTable; CmdTable.AddColumn("Status Item"); CmdTable.AddColumn("Value"); CmdTable.AddRow(); CmdTable.SetCell("Status Item", "Additional Hilights"); CmdTable.SetCell("Value", CString(m_hilights.size())); CmdTable.AddRow(); CmdTable.SetCell("Status Item", "Notifications Sent"); CmdTable.SetCell("Value", CString(m_notificationsSent)); PutModule(CmdTable); } else { PutModule("Unknown command! Try HELP."); } } EModRet OnPrivMsg(CNick& Nick, CString& sMessage) { ParsePrivMsg(Nick.GetNick(), sMessage); return CONTINUE; } EModRet OnPrivAction(CNick& Nick, CString& sMessage) { ParsePrivMsg(Nick.GetNick(), sMessage); return CONTINUE; } EModRet OnChanMsg(CNick& Nick, CChan& Channel, CString& sMessage) { ParseChanMsg(Nick.GetNick(), Channel.GetName(), sMessage); return CONTINUE; } EModRet OnChanAction(CNick& Nick, CChan& Channel, CString& sMessage) { ParseChanMsg(Nick.GetNick(), Channel.GetName(), sMessage); return CONTINUE; } EModRet OnUserAction(CString& sTarget, CString& sMessage) { m_lastActivity = time(NULL); return CONTINUE; } EModRet OnUserMsg(CString& sTarget, CString& sMessage) { m_lastActivity = time(NULL); return CONTINUE; } EModRet OnUserNotice(CString& sTarget, CString& sMessage) { m_lastActivity = time(NULL); return CONTINUE; } EModRet OnUserJoin(CString& sChannel, CString& sKey) { m_lastActivity = time(NULL); return CONTINUE; } EModRet OnUserPart(CString& sChannel, CString& sMessage) { m_lastActivity = time(NULL); return CONTINUE; } }; MODULEDEFS(CProwlMod, "Forwards hilights and PMs to prowl.")