Skip to content

Instantly share code, notes, and snippets.

@xer0x
Forked from garrettreid/prowl.cpp
Created November 27, 2012 20:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xer0x/4156762 to your computer and use it in GitHub Desktop.
Save xer0x/4156762 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.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment