├── .gitignore ├── README.md ├── app-engine ├── app.yaml └── handlers.py └── apps-script └── endpoint.gs /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Automated Blink Intent Tracker 2 | ============================== 3 | 4 | A set of services that automatically populate the ["Blink Intents" Google Spreadsheet](https://docs.google.com/a/chromium.org/spreadsheet/ccc?key=0AjGgk26K1Cc-dHJKNGtlLVlmSGRIYVR3LVRGYnVCRVE) with a list of the ["intent" emails](http://www.chromium.org/blink#TOC-Web-Platform-Changes:-Process) sent to the [blink-dev@chromium.org mailing list](https://groups.google.com/a/chromium.org/forum/#!forum/blink-dev). 5 | 6 | Not everything is automated; [some work still needs to be done manually](https://docs.google.com/a/chromium.org/document/d/1p2g3hkjTGBIhGebn0ZvAwM88XksWIFD9tBb3hnefnOI/edit). 7 | 8 | Overview 9 | -------- 10 | 11 | * `jochen@chromium.org` has a [Superfeedr](https://superfeedr.com/) Pubsubhubbub profile configured to send a POST request to `https://blink-intent-tracker-193211.appspot.com/rss-handler` whenever the [blink-dev 'topics' RSS feed](https://groups.google.com/a/chromium.org/forum/feed/blink-dev/topics/rss.xml?num=15) is updated. 12 | * The blink-intent-tracker App Engine app (owned by `jochen@chromium.org`) processes the updates and sends a stripped down version to the [Apps Script web app](https://developers.google.com/apps-script/execution_web_apps) (also owned by `jochen@chromium.org`). 13 | * The Apps Script web app adds the relevant content to the first empty row in the ["Blink Intents" Google Spreadsheet](https://docs.google.com/a/chromium.org/spreadsheet/ccc?key=0AjGgk26K1Cc-dHJKNGtlLVlmSGRIYVR3LVRGYnVCRVE). 14 | -------------------------------------------------------------------------------- /app-engine/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 The Chromium Authors. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | runtime: python27 6 | api_version: 1 7 | threadsafe: true 8 | 9 | handlers: 10 | - url: /.* 11 | script: handlers.application 12 | -------------------------------------------------------------------------------- /app-engine/handlers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 The Chromium Authors. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | import json 6 | import logging 7 | import re 8 | import urllib 9 | import webapp2 10 | from google.appengine.api import urlfetch 11 | 12 | 13 | APPS_SCRIPT_ENDPOINT = 'https://script.google.com/a/chromium.org/macros/s/AKfycby-kGgowUQ0Ol_KxgL9VyWSRrz7ZrwsuoyM8JwPxvhG4x7VAlQy/exec' 14 | 15 | RSS_FEED = 'https://groups.google.com/a/chromium.org/forum/feed/blink-dev/topics/rss.xml?num=15' 16 | 17 | 18 | def sendUpdateToAppsScript(sender, subject, link): 19 | raw_data = {'sender': sender.encode('utf-8'), 20 | 'subject': subject.encode('utf-8'), 21 | 'link': link.encode('utf-8')} 22 | form_data = urllib.urlencode(raw_data) 23 | logging.info(form_data) 24 | urlfetch.fetch(url=APPS_SCRIPT_ENDPOINT, 25 | payload=form_data, 26 | method=urlfetch.POST, 27 | headers={'Content-Type': 'application/x-www-form-urlencoded'}) 28 | 29 | 30 | class ProcessRssTopic(webapp2.RequestHandler): 31 | 32 | def isIntent(self, subject): 33 | subject = subject.encode('utf-8').lower() 34 | return re.match(r'.*intent to .*:.*$', subject) and not re.match(r'.*(was|re):.*', subject) 35 | 36 | def post(self): 37 | rssUpdate = json.loads(self.request.body) 38 | logging.info(rssUpdate) 39 | for item in rssUpdate['items']: 40 | logging.info(item['permalinkUrl']) 41 | logging.info(item['title']) 42 | if (self.isIntent(rssUpdate['items'][0]['title'])): 43 | logging.info("It's an intent!") 44 | sendUpdateToAppsScript( 45 | item['actor']['displayName'], 46 | item['title'], 47 | item['permalinkUrl']) 48 | 49 | def get(self): 50 | if self.request.GET['hub.topic'] != RSS_FEED: 51 | self.error(400) 52 | return 53 | 54 | if self.request.GET['hub.mode'] != 'subscribe': 55 | self.error(400) 56 | return 57 | 58 | self.response.headers['Content-Type'] = 'text/plain' 59 | self.response.out.write(self.request.GET['hub.challenge']) 60 | return 61 | 62 | # TODO(meh): Test if API owners LGTMed in their replies. 63 | 64 | 65 | application = webapp2.WSGIApplication([ 66 | ('/rss-handler', ProcessRssTopic) 67 | ], debug=True) 68 | -------------------------------------------------------------------------------- /apps-script/endpoint.gs: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE: to update the running production instance of this code: 3 | 1) Go to File => Manage versions and add a new version. 4 | 2) Then go to Publish, select the new version number, and click update. 5 | */ 6 | 7 | var INTENTS = [" implement"," deprecate"," ship"," remove"]; 8 | 9 | function doPost(request) { 10 | var ss = SpreadsheetApp.openById("0AjGgk26K1Cc-dHJKNGtlLVlmSGRIYVR3LVRGYnVCRVE"); 11 | var sheet = ss.getSheetByName("DATA"); 12 | var lastRow = sheet.getLastRow(); 13 | var currentRow = lastRow + 1; 14 | Logger.log("Intent received!"); 15 | Logger.log(new Date()); 16 | sheet.getRange(currentRow, 1, 1).setValue(request.parameter); // Put raw data in first column on next available row. 17 | sheet.getRange(currentRow, 2, 1).setValue(Utilities.formatDate(new Date(), "PST", "MM/dd/yyyy")); 18 | sheet.getRange(currentRow, 3, 1).setValue(request.parameter.sender); // Sender 19 | sheet.getRange(currentRow, 4, 1).setValue(getIntentType(request.parameter.subject)); 20 | sheet.getRange(currentRow, 5, 1).setValue("=HYPERLINK(\"" + request.parameter.link + "\", \"" + getSubject(request.parameter.subject) + "\")"); 21 | } 22 | 23 | function getIntentType(subject) { 24 | var lowerCaseSubject = subject.toLowerCase(); 25 | var intentType = ""; 26 | for (var i = 0; i < INTENTS.length; i++) 27 | if (lowerCaseSubject.indexOf(INTENTS[i]) >= 0) 28 | intentType += (intentType.length == 0) ? 29 | capitalizeFirstLetter(INTENTS[i].trim()) : " and " + INTENTS[i].trim(); 30 | return intentType; 31 | } 32 | 33 | function getSubject(subject) { 34 | var lastIntentTypeEndIndex = -1; 35 | var curIntentTypeEndIndex = -1; 36 | var lowerCaseSubject = subject.toLowerCase(); 37 | for (var i = 0; i < INTENTS.length; i++) { 38 | curIntentTypeEndIndex = lowerCaseSubject.indexOf(INTENTS[i]) + INTENTS[i].length; 39 | if (curIntentTypeEndIndex >= lastIntentTypeEndIndex) 40 | lastIntentTypeEndIndex = curIntentTypeEndIndex; 41 | } 42 | if (lastIntentTypeEndIndex == -1) Logger.log("Intent type string not found."); 43 | // If there's a colon, the "+1" should get rid of it. 44 | var trimmedSubject = subject.substring(lastIntentTypeEndIndex+1).trim(); 45 | // Double quotes don't play nice with Google Spreadsheets. 46 | return trimmedSubject.replace(/"/g, "'"); 47 | } 48 | 49 | function capitalizeFirstLetter(string) { 50 | return string.charAt(0).toUpperCase() + string.slice(1); 51 | } 52 | --------------------------------------------------------------------------------