Created
September 4, 2012 02:08
-
-
Save garrettreid/3615737 to your computer and use it in GitHub Desktop.
Updated Prowl module for ZNC
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 characters
/* | |
* 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.") |
Please add an ignore function. Nick or channel.
We have the game IdleRPG (http://idlerpg.net/) run the bot writes the highlight nick very often in the channel, and this should not be sent.
All other messages with the highlight Nick will continue to be recognized and transmitted.
Or there is this already and I've overlooked something?!?
Thank you
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Garrett, I am attempting to use this plugin on znc 0.206, and I'm seeing a strange behavior.
At some seemingly random point, the prowl module loses my highlights, and they get replaced with my nick. This happens fairly soon after I enter the highlights in.
Sep 11 08:43:07 highlights add ping #channel1
Sep 11 08:43:07 <_prowl> Entry 'ping #channel1' added.
Sep 11 08:43:12 highlights add ping #channel2
Sep 11 08:43:12 <_prowl> Entry 'ping #channel2' added.
Sep 11 08:43:19 highlights add cwawak
Sep 11 08:43:19 <_prowl> Entry 'cwawak' added.
Sep 11 08:43:23 highlights
Sep 11 08:43:23 <_prowl> Active additional hilights:
Sep 11 08:43:23 <_prowl> 1: ping #channel1
Sep 11 08:43:23 <_prowl> 2: ping #channel2
Sep 11 08:43:23 <*prowl> 3: cwawak
... and this seems fine until ...
Sep 11 08:45:58 highlights
Sep 11 08:46:01 <_prowl> Active additional hilights:
Sep 11 08:46:01 <_prowl> 1: cwawak
Sep 11 08:46:01 <_prowl> 2: cwawak
Sep 11 08:46:01 <_prowl> 3: cwawak
Sep 11 08:46:01 <*prowl> --End of list
GASP! Once the highlights get wiped out, the only messages that get sent to prowl are ones that contain my nick.
Any suggestions?