diff options
-rw-r--r-- | README.md | 95 | ||||
-rwxr-xr-x | sniffa.py | 108 |
2 files changed, 203 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c74686 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +sniffa +====== + +sniffa is a small utility that allows you to watch Discuss forums for keywords. + +Every time it is invoked, it checks for new posts matching the keywords and creates a notification in the Mac OS X notification bar. + +# Requirements + +* Mac OS X 10.8 or later: As it uses Mac OS X notifications, sniffa works only on Mac OS X 10.8 or later. +* Python 3 +* certifi: Install with `pip3 install certifi` +* pync: Install with `pip3 install pync` + +# Installation + +Ensure that all prerequisites are installed, then copy `sniffa.py` to any directory, e.g. `~/bin` and run `chmod u+x sniffa.py`. + +# Usage + +sniffa can be used to query multiple Discuss forums. The keywords and the ids of all already seen posts are maintained in a file `~/.sniffa/watch-$(FORUM_NAME).ini`, where `$(FORUM_NAME)` is a name that you can choose to identify this forum. + +## Example + +Consider you want to watch for the keywords "Rally" and "JMeter" in the Elastic Discuss forum at https://discuss.elastic.co. + +1. Create `~/.sniffa/watch-elastic.ini` +2. Add the following lines: + +``` +[sniffa.domain] +url = https://discuss.elastic.co + +[Rally] + +[JMeter] +``` + +Now invoke sniffa: ``python3 sniffa.py elastic``. It will load the watches file for the forum named "elastic", check for new posts (which will be a lot at the first time) and show a notice for each of them in the Mac OS X notification bar. + +## Automatic regular invocation + +Quite likely you don't want to invoke sniffa manually every time you want to check for new posts. Therefore, you can install sniffa as a launch agent. + +Create a new file in `~/Library/LaunchAgents/org.github.sniffa.plist` with the following contents: + +```plist +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>org.github.sniffa</string> + + <key>ProgramArguments</key> + <array> + <string>~/bin/sniffa.py</string> + <string>elastic</string> + </array> + + <key>Nice</key> + <integer>1</integer> + + <key>StartInterval</key> + <integer>7200</integer> + + <key>RunAtLoad</key> + <true/> + + <key>StandardErrorPath</key> + <string>~/.sniffa/sniffa-elastic.err.log</string> + + <key>StandardOutPath</key> + <string>~/.sniffa/sniffa-elastic.out.log</string> +</dict> +</plist> +``` + +Change the paths depending on the install location of sniffa and also your domain parameter. + +With this plist file, sniffa will check the Elastic Discuss forum every two hours (7200 seconds) for new posts. + +Finally register the launch agent with Mac OS X: + +``` +launchctl load ~/Library/LaunchAgents/org.github.sniffa.plist +``` + +After a restart, Mac OS X will pick up the plist file automatically. + +If you are interested in more details about launch agents, check [Alvin Alexander's blog post about plist files](http://alvinalexander.com/mac-os-x/mac-osx-startup-crontab-launchd-jobs) (on which this description is based). + +# License + +'sniffa' is distributed under the terms of the [Apache Software Foundation license, version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). diff --git a/sniffa.py b/sniffa.py new file mode 100755 index 0000000..b1e8b18 --- /dev/null +++ b/sniffa.py @@ -0,0 +1,108 @@ +#!/usr/local/bin/python3 + +# +# Note: Ideally the shebang line would contain /usr/bin/env python3 instead of the +# hardcoded path but it seems the user's environment is not inherited by +# LaunchAgents (at least not on El Capitan). +# + +import os +import sys +import errno +import json +import configparser +import urllib3 +import urllib.parse +import datetime + +import certifi +import pync + +DOMAIN_SECTION_KEY = "sniffa.domain" + + +def ensure_dir(directory): + try: + os.makedirs(directory) + except OSError as exception: + if exception.errno != errno.EEXIST: + raise + +def creation_date(item): + return datetime.datetime.strptime(item["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ").timestamp() + + +def main(): + if not len(sys.argv) == 2: + print("usage: %s domain_key" % sys.argv[0], file=sys.stderr) + exit(1) + + domain_key = sys.argv[1] + + http = urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where()) + + watches_dir = "%s/.sniffa" % os.getenv("HOME") + watches_file = "%s/watches-%s.ini" % (watches_dir, domain_key) + + ensure_dir(watches_dir) + + config = configparser.ConfigParser() + config.read(watches_file) + + if not DOMAIN_SECTION_KEY in config or not "url" in config[DOMAIN_SECTION_KEY]: + print("Invalid configuration in [%s]" % watches_file, file=sys.stderr) + print("", file=sys.stderr) + print("Please add a domain to query according to the following example in [%s]" % watches_file, file=sys.stderr) + print("", file=sys.stderr) + print(" [%s]" % DOMAIN_SECTION_KEY, file=sys.stderr) + print(" url=https://discuss.example.org", file=sys.stderr) + print("", file=sys.stderr) + exit(1) + + domain = config[DOMAIN_SECTION_KEY]["url"] + print("Checking for new posts on %s" % domain_key) + + for keyword in config.sections(): + if keyword == DOMAIN_SECTION_KEY: + continue + + if "ids" in config[keyword]: + ids = config[keyword]["ids"] + if len(ids) > 0: + known_ids = set([int(id) for id in config[keyword]["ids"].split(",")]) + else: + known_ids = set() + else: + known_ids = set() + + query_string = urllib.parse.quote_plus("%s order:latest" % keyword) + + r = http.request("GET", "%s/search?q=%s" % (domain, query_string), headers={"Accept": "application/json"}) + results = json.loads(r.data.decode("utf-8")) + + topics_by_id = {} + for topic in results["topics"]: + topics_by_id[topic["id"]] = topic + + posts = [] + for post in results["posts"]: + post_id = post["id"] + if post_id not in known_ids: + known_ids.add(post_id) + posts.append(post) + + config[keyword]["ids"] = ",".join([str(id) for id in known_ids]) + + sorted(posts, key=creation_date, reverse=True) + + for post in posts: + topic = topics_by_id[post["topic_id"]] + pync.Notifier.notify(topic["title"], title="New post mentioning '%s'" % keyword, + open="%s/t/%s/%d" % (domain, topic["slug"], topic["id"]), group=str(topic["id"])) + + with open(watches_file, "w") as f: + config.write(f) + print("Finished") + +if __name__ == "__main__": + main() |