Skip to content

Instantly share code, notes, and snippets.

@garrettreid
Created September 4, 2012 02:08
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save garrettreid/3615737 to your computer and use it in GitHub Desktop.
Save garrettreid/3615737 to your computer and use it in GitHub Desktop.
Updated Prowl module for ZNC
/*
* 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.")
@cwawak
Copy link

cwawak commented Sep 11, 2012

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?

@sidekix
Copy link

sidekix commented Apr 27, 2015

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