Last active
December 15, 2015 15:49
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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