Skip to content

Instantly share code, notes, and snippets.

@mislav
Last active September 13, 2016 00:25
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mislav/8ee3b0944aa0f4018d3f to your computer and use it in GitHub Desktop.
Save mislav/8ee3b0944aa0f4018d3f to your computer and use it in GitHub Desktop.
My simpler alternative to OctoGAS labler; more aggressive caching to avoid Gmail rate limits
/* Scans Gmail inbox for new GitHub notifications and:
*
* - labels threads that @-mention me with "Direct Mention";
* - labels threads for issues/PRs that I've opened with "Direct Mention";
* - labels threads that @-mention my teams with "Team Mention".
*
* To install: visit https://script.google.com/intro to enable Apps Script.
* Then copy this script over, edit the first few regular expressions. You'll
* have to allow Apps Script access to your Gmail. Finally, use the time icon
* in the menubar to create a trigger that will run `processInbox` at scheduled
* intervals, such as once every hour.
*/
var directRE = /@mislav\b/
var teamRE = /@github\/(js|josh|repos-flex|user-growth)\b/
function forEach(a, fn) {
for (var i = 0; i < a.length; i++) if (fn(a[i], i) === false) break
}
function log(message) {
var args = arguments, argIndex = 1
Logger.log(message.replace(/%[ds]/g, function(){ return args[argIndex++] }))
}
cache = (function(){
var userCache = CacheService.getUserCache()
, cacheKey = "processedThreads:v2"
, cacheMax = 1000
, expiresIn = 60 * 60 * 2
, processed = userCache.get(cacheKey)
, threadMessageCountMap = {}
if (processed) processed = JSON.parse(processed)
else processed = []
forEach(processed, function(idWithCount) {
var pair = idWithCount.split(":", 2)
threadMessageCountMap[pair[0]] = Number(pair[1])
})
var threadKey = function(thread) {
return thread.getId() + ":" + thread.getMessageCount()
}
return {
isThreadProcessed: function(thread) {
return processed.indexOf(threadKey(thread)) > -1
}
, markThreadsAsProcessed: function(threads) {
log("caching %d threads as processed", threads.length)
forEach(threads, function(thread){
var key = threadKey(thread)
if (processed.indexOf(key) == -1) processed.unshift(key)
threadMessageCountMap[String(thread.getId())] = thread.getMessageCount()
})
processed = processed.slice(0, cacheMax)
userCache.put(cacheKey, JSON.stringify(processed), expiresIn)
}
, getStartingMessageIndex: function(thread) {
return threadMessageCountMap[String(thread.getId())] || 0;
}
, getLastRun: function() {
return userCache.get("lastRunAt")
}
, recordLastRun: function(date) {
userCache.put("lastRunAt", parseInt(date / 1000), expiresIn)
}
}
})()
getLabel = (function(){
var labelIndex = null
return function(name) {
if (!labelIndex) {
labelIndex = {}
forEach(GmailApp.getUserLabels(), function(label){
labelIndex[label.getName().toLowerCase()] = label
})
}
return labelIndex[name.toLowerCase()] || GmailApp.createLabel(name)
}
})()
// NOTE: doesn't handle multiline values
function parseHeaders(message) {
var match
, headers = {}
, rawHeaders = message.getRawContent().split("\r\n\r\n", 2)[0]
forEach(rawHeaders.split("\r\n"), function(line) {
var match = line.match(/^(\S+):\s*(.*)/)
if (match) headers[match[1].toLowerCase()] = match[2]
})
return headers
}
function processThreads(threads) {
var doneThreads = []
, todoThreads = []
forEach(threads, function(thread){
if (!cache.isThreadProcessed(thread)) todoThreads.push(thread)
})
if (todoThreads.length == 0) return
try {
log("fetching messages for %d threads", todoThreads.length)
forEach(GmailApp.getMessagesForThreads(todoThreads), function(messages, i){
var message
, body
, thread = todoThreads[i]
, i = cache.getStartingMessageIndex(thread)
log("fetching body for %d messages starting from index %d", messages.length - i, i)
for (; i < messages.length; i++) {
message = messages[i]
if (i == 0 && parseHeaders(message)["x-github-reason"] == "author") {
thread.addLabel(getLabel("Direct Mention"))
break
}
body = message.getPlainBody()
if (directRE.test(body)) {
thread.addLabel(getLabel("Direct Mention"))
break
} else if (teamRE.test(body)) {
thread.addLabel(getLabel("Team Mention"))
break
}
}
doneThreads.push(thread)
})
} finally {
cache.markThreadsAsProcessed(doneThreads)
}
}
function processInbox() {
var threads
, perPage = 30
, offset = 0
, query = 'in:inbox AND ( from:"notifications@github.com" OR from:"notifications@support.github.com" OR from:"noreply@github.com" )'
, lastRunAt = cache.getLastRun()
, newLastRun = new Date()
if (lastRunAt) {
query += " after:" + lastRunAt
}
cache.recordLastRun(newLastRun)
log("performing a search for `%s`", query)
do {
threads = GmailApp.search(query, offset, perPage)
log("processing %d threads starting at index %d", threads.length, offset)
processThreads(threads)
offset += perPage
} while (threads.length == perPage)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment