Skip to content

Instantly share code, notes, and snippets.

@kaimst
Last active December 15, 2015 15:49
Show Gist options
  • Save kaimst/5285044 to your computer and use it in GitHub Desktop.
Save kaimst/5285044 to your computer and use it in GitHub Desktop.
Groovy script that checks for new Stack Overflow questions and notifies the user via terminal / growl / speech.
import java.text.SimpleDateFormat;
import groovyx.net.http.RESTClient
/**
* This Groovy script connects to the Stack Overflow Api and looks up the latest posts for user supplied tags.
*
* All unseen questions are then displayed on the console and optionally passed on to Growl
* (growlnotify must be installed and in the current execution path).
* It is also possible to let the OSX speech synthesizer (/bin/say) say the number of unseen questions per tag.
*
* Once questions are fetched, their id is stored in a text file for later reference so they won't be displayed again.
*
* Here is an example call that uses growlnotify and /bin/say for notification of new Questions tagged Java or Groovy.
* It re-checks every 10 seconds.
*
* groovy StackoverflowNotifier.groovy --growl true -t java,groovy --say true -r 10
*
*
* Note:
*
* This script will hit the SO rate limit pretty quickly. It is meant as a demonstration of Groovy's
* prototyping capabilities only.
*
* The script was inspired by the following gist: https://gist.github.com/1030653
* ----------------------------------------------------------------------------------------------------------------------
*
* 2013, Kai Sternad
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
@Grab(group='org.codehaus.groovy.modules.http-builder', module='http-builder', version='0.6')
class StackoverflowNotifier {
static final String CACHE_FILE_NAME = "stackoverflow-cache.txt";
static def main(def args) {
def cli = new CliBuilder(usage: 'StackOverflowNotifier [options]');
cli.with {
h longOpt: "help", "Usage information"
_ longOpt: "say", args: 1, argName: 'true|false', required: false, type: Boolean, "Set true to enable speech notification with the OSX '/usr/bin/say' speech synthesizer command. Defaults to false."
_ longOpt: "growl", args: 1, argName: 'true|false', required: false, type: Boolean, "Set true to enable growl notifications. In order for this to work, 'growlnotify' must be installed. Defaults to false."
r longOpt: "repeat", args: 1, argName: 'integer', required: false, type: Long, "Set to enable repeated polling every <integer> seconds. Program will not terminate on its own if set."
t longOpt: "tags", args: 1, type: GString, required: true, "Sets the tag names to be watched. Tags can be combined or be provided in isolation. Combined: java:groovy will search for all questions that have both tags. java,groovy will search for questions that are tagged java and questions that are tagged groovy. Both styles can be combined: java:groovy,oracle,mysql searches for all questions with both java and groovy tags simultaneously and questions with oracle and questions with mysql."
}
def options = cli.parse(args);
if (!options || options.h) {
System.exit(0);
cli.usage();
}
long pollInterval = 0L;
if (options.r) {
pollInterval = options.r as long;
}
def tags = [];
if (options.t) {
def outer = options.t.split(",");
outer.each {inner -> tags << (inner.split(":") as List)};
}
boolean say = false;
if (options.say) {
say = Boolean.parseBoolean(options.say);
}
boolean growl = false;
if (options.growl) {
growl = Boolean.parseBoolean(options.growl);
}
def notifier = new StackoverflowNotifier();
while (true) {
def existing = notifier.getSeenQuestions();
tags.each {taglist ->
def newlySeen = notifier.findStackOverflowQuestions(taglist, existing);
def newlySeenSortedByDate = newlySeen.sort {a, b -> a.published <=> b.published}
notifier.notify(newlySeenSortedByDate, taglist, say, growl);
notifier.addNewToSeen(newlySeenSortedByDate.collect {it.id});
}
if (pollInterval.equals(0L)) {
break;
}
sleep(pollInterval * 1000);
}
}
void addNewToSeen(List newlySeen) {
def cacheOnDisk = new File(CACHE_FILE_NAME);
newlySeen.each {seen -> cacheOnDisk.append("${seen}\n")};
}
List getSeenQuestions() {
List existing = [];
def cacheOnDisk = new File(CACHE_FILE_NAME);
if (!cacheOnDisk.exists()) {cacheOnDisk.createNewFile();}
cacheOnDisk.eachLine { id -> existing << new Integer(id);};
return existing;
}
void notify(List notifyables, List tags, boolean say, boolean growl) {
def dateFormatString = "yyyy-MM-dd HH:mm:ss";
notifyables.each {notifyable ->
def title = notifyable.title
String message = notifyable.published.format(dateFormatString) + ", ${tags.join('/')}, ${title}";
if (growl) {
executeCommand(["growlnotify", "-m", "$message"]);
}
println message;
}
def now = new Date().format(dateFormatString);
println "last checked ${tags.join(',')} at ${now}";
if (notifyables.size() > 0 && say) {
executeCommand(["say", "${notifyables.size()} : ${tags.join(',')}"]);
}
}
List findStackOverflowQuestions(List tags, List seen) {
def questions = new RESTClient("http://api.stackoverflow.com/1.1/questions?pagesize=30&tagged=${tags.join(';')}&sort=creation");
def results = questions.get (headers: [Accept : 'application/json', "Accept-Encoding" : 'gzip,deflate'])
def unseen = [];
def entries = results.data.questions.collect{
if (!seen.contains(it.question_id)){
unseen << new Entry(id:it.question_id as String, published:new Date((it.creation_date as Long) * 1000), title:it.title)
}
}
return unseen;
}
void executeCommand(List message) {
def prec = message.execute();
prec.waitFor();
}
}
class Entry {
String id
Date published
String title
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment