Skip to content

Instantly share code, notes, and snippets.

@garrettreid
Created September 4, 2012 02:08

Revisions

  1. garrettreid created this gist Sep 4, 2012.
    633 changes: 633 additions & 0 deletions prowl.cpp
    Original 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.")