Google+ History API in Chrome Extensions |
The Google+™ History API is a fairly new addition to the Google+ APIs, which allows writing "moments" to your private history from which you can then share the moments to your circles. This article will give a short introduction to this API and then explain how to use it in combination with Google Chrome™ Extensions.
Important: The Google+ History is at the moment in developer preview, so before you get started playing around with it you will have to sign up for the preview.
To put it simple, the Google+ History is a place (called the Vault in the API) where you can privately save "moments". A moment is defined by a type (CommentActivity, ListenActivity, CreateActivity, ...), a target, which is a publicly reachable URL, and for some types a result, for example the actual comment for a CommentActivity. The biggest limitation of the History API at the moment is that you need a publicly reachable URL for moments, but the reason behind this is that the information about the target is retrieved via a Google bot. So you have for example a website for a song which includes schema.org markup for the title of the song, name of the artist, album art, ... which you can link to from a ListenActivity. You can find a list of currently supported types and the targets they require in the official documentation. You will also find a lot more information about the basic principles behind the History API there, I'm going to focus mainly on the practical part in this article, so it's a good idea to at least have a quick look over the documentation.
The way I see the Google+ History it's a way to keep track of stuff you are doing in other places. This can be something like achievements in computer games, location while you're on mobile or articles you read on a news website. A Chrome extension is a good way to keep track of stuff you do on the web. In the examples below I let the user explicitely write links to the History but this could as well be done automatically triggered by certain actions, like listening to a song. Also using a Chrome extension to write moments using URLs that are already available on the web saves you the trouble of actually having to create the contents for the moments yourself, only focusing on the client-side. If you are interesting in how to construct pages that can be used for moments make sure to check this list of moment types where the necessary schema.org markup is explained.
Because the Google+ History API writes private information on behalf of a user, we need to get the necessary permission from the user via OAuth 2. Very, very simplified the way this is done is to redirect the user to a Google page requesting the permissions we need (in the case of the History API we need to request the scopes https://www.googleapis.com/auth/plus.moments.write and https://www.googleapis.com/auth/plus.me). Once the permission is granted or denied Google will redirect the user back to us with the necessary access token for future requests or an access denied message. Now the specific problem with Chrome extensions is that while we can redirect the user to the authentication page actually getting the callback from Google afterwards is difficult because Chrome extension pages aren't really webpages but only run locally for each user. To work around this problem we are going to use the brilliant OAuth 2.0 Library for Chrome Extensions by Boris Smus. The way Boris solves the problem is to use a public website as the redirect/callback URL which receives the access token from the OAuth workflow. In the Chrome extension we are then using a content script to capture this information and forward the access token to our extension. In the standard library "http://www.google.com/robots.txt" is used as the redirect URL. To prevent interference with other extensions using the library (because the first extension to catch the access token from the URL will keep it) you will want to change this URL to some empty file on your website. For example I just uploaded an empty oauth.txt file to my website and use this for the extension. If you have multiple extensions using the library (in this article we will cover two) you will need a separate redirect URL for each of them.
There's one important change you will have to make in the library to use your own redirect URL:
... | |
redirectURL: function(config) { | |
return "YOUR FULL REDIRECT URL HERE"; | |
}, | |
... |
Another thing you have to do before you can get started is to set-up a project in the Google APIs console.
1. Create a new project
2. In "Services" activate the Google+ API and the Google+ History API.
You won't see this unless you signed up for the developer preview as mentioned above.
3. In "API Access" create a new Client ID and use the Redirect URL of your choice,
the same one you entered in oauth2/adapters/google.js
You can use the same Client ID for both extensions below, simply enter both Redirect URLs you want to use. You will need the Client ID and Client secret later in the code.
The first extension we are going to do will use a browser action to send the current URL we are on to history. Potential use cases could be to keep a private list of read-later, favourite, watched, ... items.
I'm going to explain the important parts of the code inline below. If you have more question about the code don't be shy to ask me. I know that I'm assuming quite a bit of previous knowledge for this article.
manifest.json - all the information about the extension, see the Chrome Extensions documentation for details.
{ | |
"manifest_version": 2, | |
"name": "History Template - Browser Action", | |
"version": "1.0.0", | |
"description": "Google+ History API example", | |
"permissions": [ | |
"tabs", | needed to access URL of current tab |
"notifications", | Desktop notifications for success/error messages |
"https://accounts.google.com/o/oauth2/token", | Necessary for OAuth authentication |
"https://www.googleapis.com/" | needed to access the Google+ History API |
], | |
"icons": { | |
"16": "icon16.png", | |
"48": "icon48.png", | |
"128": "icon128.png" | |
}, | |
"content_scripts": [ | |
{ | |
"matches": ["REDIRECT URL*"], | insert your full redirect URL here, the * is necessary at the end |
"js": ["oauth2/oauth2_inject.js"], | this handles the OAuth callback |
"run_at": "document_start" | |
} | |
], | |
"background": { | |
"page": "history_background.html" | this is where most of the work is done |
}, | |
"browser_action": { | |
"default_icon": "icon19.png", | |
"default_popup": "history_popup.html" | GUI to initiate actions |
}, | |
"web_accessible_resources": [ | |
"icon48.png" | |
] | |
} |
history_background.html - just for loading our scripts
<!doctype html> | |
<html> | |
<head> | |
<title>History Template</title> | |
<script src="oauth2/oauth2.js"></script> | OAuth 2.0 library |
<script src="history_background.js"></script> | our main script |
</head> | |
<body></body> | |
</html> |
history_background.js - all the work is being done here
(function (global) { | |
"use strict"; | |
function HistoryApp() { | |
var google, access_token, types; | |
// initializing the OAuth 2.0 library with the values from the API Console | |
google = new global.OAuth2("google", { | |
client_id: "YOUR_CLIENT_ID", | |
client_secret: "YOUR_CLIENT_SECRET", | |
api_scope: "https://www.googleapis.com/auth/plus.moments.write https://www.googleapis.com/auth/plus.me" | |
}); | |
// defining the activity types we want to allow for our extension | |
// for simplicity we only use types that don't require a "result" | |
types = []; | |
types.push({activityType: "http://schemas.google.com/AddActivity", label: "Add"}); | |
types.push({activityType: "http://schemas.google.com/BuyActivity", label: "Buy"}); | |
types.push({activityType: "http://schemas.google.com/CheckInActivity", label: "Check In"}); | |
types.push({activityType: "http://schemas.google.com/CreateActivity", label: "Create"}); | |
types.push({activityType: "http://schemas.google.com/ListenActivity", label: "Listen"}); | |
types.push({activityType: "http://schemas.google.com/ViewActivity", label: "View"}); | |
// this function will be called from the popup to write a moment with specific type and target (i.e. a URL) | |
this.addItem = function (type, url) { | |
// using the OAuth 2.0 library to make sure we have a fresh and valid access token | |
// this will also start the OAuth 2.0 authentication workflow if necessary | |
google.authorize(function () { | |
// saving the OAuth 2.0 access token for later use | |
access_token = google.getAccessToken(); | |
// calling our function to actually write the described moment | |
writeMoment({ | |
"type": type, | |
"target": { | |
"url": url | |
} | |
}); | |
}); | |
}; | |
// this function does the actual call to the Google+ History API | |
function writeMoment(info) { | |
var xhr, message; | |
xhr = new global.XMLHttpRequest(); | |
xhr.onreadystatechange = function () { | |
// this will be called once the request has finished | |
var response, text; | |
if (xhr.readyState == 4) { | |
if (xhr.status >= 200 && xhr.status <= 204) { | |
// Request has finished successfully | |
global.console.log("Success: " + xhr.responseText); | |
response = JSON.parse(xhr.responseText); | |
text = response.target.name; | |
// creating a desktop notification with the result of the request | |
createNotification(text, undefined, response.target.image); | |
} else { | |
// displaying the error, usual error-codes are: | |
// 400 - target is not a valid public URL or something in the authentication went wrong | |
// 500 - most likely the target didn't include the necessary information for the activity type | |
createNotification("Error " + xhr.status + ": " + xhr.statusText, "Error adding item to Google+ History"); | |
} | |
} | |
}; | |
// this is the URL that request have to be sent to | |
// the ?debug=true makes sure that we actually get a reply with the target information | |
// without it we would get an empty response with a 204 status success code | |
xhr.open("POST", "https://www.googleapis.com/plus/v1moments/people/me/moments/vault?debug=true", true); | |
xhr.setRequestHeader("Content-Type", "application/json"); | |
// add our OAuth access token to authenticate the request | |
xhr.setRequestHeader("Authorization", "OAuth " + access_token); | |
// The moment object needs to be turned into a string to put into the POST request | |
message = JSON.stringify(info); | |
// send request, when finished the onreadystatechange event will be called | |
xhr.send(message); | |
} | |
// this function creates a desktop notification to display success/failure to the user | |
function createNotification(text, title, image) { | |
var notification; | |
notification = global.webkitNotifications.createNotification( | |
image || global.chrome.extension.getURL("icon48.png"), | |
title || "Item added to Google+ history", | |
text | |
); | |
notification.show(); | |
// automatically hide the desktop notification after 5 seconds | |
global.setTimeout(function () { notification.cancel(); }, 5000); | |
} | |
this.getTypes = function () { return types; }; | |
} | |
global.historyApp = new HistoryApp(); | |
}(window)); |
history_popup.html - this will be displayed when clicking on the extension icon
<!doctype html> | |
<html> | |
<head> | |
<title>History Template</title> | |
</head> | |
<body style="min-width: 200px; text-align: center;"> | |
<div id="buttons"></div> | this will include the buttons for adding moments |
<script src="history_popup.js"></script> | script to handle user interaction |
</body> | |
</html> |
history_popup.js - script to handle user interaction
(function (global) { | |
"use strict"; | |
var buttons, background, types, i, l; | |
// access the background page/script | |
background = global.chrome.extension.getBackgroundPage(); | |
buttons = global.document.getElementById("buttons"); | |
// make sure our background script is ready already | |
if (!background.historyApp) { | |
global.document.body.innerHTML = "HistoryApp not loaded yet, please try again."; | |
} else { | |
// read the allowed types from the background script and create buttons for them | |
types = background.historyApp.getTypes(); | |
l = types.length; | |
for (i = 0; i < l; i++) { | |
createButton(types[i]); | |
} | |
} | |
// creates a button for the specified type | |
function createButton(type) { | |
var button; | |
button = global.document.createElement("button"); | |
button.innerHTML = type.label; | |
buttons.appendChild(button); | |
button.onclick = function () { | |
// get the URL of the current tab when the button is clicked | |
global.chrome.tabs.query({active: true, currentWindow: true}, function (tabs) { | |
if (tabs && tabs.length > 0) { | |
// send the information to the background script to be added to History | |
background.historyApp.addItem(type.activityType, tabs[0].url); | |
// close the popup | |
global.close(); | |
} | |
}); | |
}; | |
} | |
}(window)); |
To install the extension you will have to go to chrome://chrome/extensions/ and enable Developer mode. Then you can use "Load unpacked extension..." and navigate to the folder that includes that manifest.json to install it.
Once installed you will have a new icon in the browser bar. When clicking the icon you will get a choice of different actions you can write to history. Important to note is that not all of these actions will work with all pages. For example the listen action will only work for pages describing songs (like some pages on Last.fm) while the view action only works for video pages (like YouTube or Vimeo). I haven't really found a page yet where the "Check In" action actually works. Neither Google+ Local Pages, Google Places or FourSquare locations seem to provide the necessary schema.org markup.
In case adding the moment to history was successful you will get a desktop notification with a summary of what Google has extracted from the target.
You can then also visit plus.google.com/history to see the moment there.
As a second example we want to use a content script to add a new button to Google+ posts which allows us to add this post to our history, maybe as a way to bookmark posts for reading/refering to later.
It's important to note that such an extension is very likely to break at some point because it depends on a fixed class-structure on the Google+ page and this has changed several times in the past.
I'm only going to concentrate on the changes as opposed to the previous extension below.
{ | |
"manifest_version": 2, | |
"name": "History Template - Content Script", | |
"version": "1.0.0", | |
"description": "Google+ History API example", | |
"permissions": [ | |
"notifications", | |
"https://accounts.google.com/o/oauth2/token", | |
"https://www.googleapis.com/" | |
], | |
"icons": { | |
"16": "icon16.png", | |
"48": "icon48.png", | |
"128": "icon128.png" | |
}, | |
"content_scripts": [ | |
{ | |
"matches": ["https://plus.google.com/*"], | We are going to inject our script |
"js": ["history_cs.js"] | on all Google+ pages |
}, | |
{ | |
"matches": ["REDIRECT URL*"], | If you are planning to run both extensions |
this one has to be different from the other one | |
"js": ["oauth2/oauth2_inject.js"], | |
"run_at": "document_start" | |
} | |
], | |
"background": { | |
"page": "history_background.html" | |
}, | |
"web_accessible_resources": [ | |
"icon48.png" | |
] | |
} |
history_cs.js - the content script will be executed in the context of Google+ pages
(function (global) { | |
"use strict"; | |
// in case the Google+ layout changes those class-names will have to be adjusted | |
var | |
postClass = "Tg", | |
actionClass = "mv", | |
menuClass = "a-w", | |
linkClass = "g-M-n", | |
csClass = "history_cs"; | |
// add new menu items for posts | |
function updateDOM() { | |
var divs, i, l; | |
// we are looking for all post divs, i.e. having the postClass | |
// that haven't be handled by our script yet :not(.history_cs) | |
divs = global.document.querySelectorAll("." + postClass + ":not(." + csClass + ")"); | |
if (divs && divs.length > 0) { | |
l = divs.length; | |
for (i = 0; i < l; i++) { | |
addButton(divs[i]); | |
} | |
} | |
} | |
// here we look for the menu-div inside of the post-div and add our new menu item to the end | |
function addButton(div) { | |
var actionDiv, menu, url, link; | |
actionDiv = global.document.querySelector("#" + div.id + " ." + actionClass); | |
// this element includes the link we want to add to history | |
link = global.document.querySelector("#" + div.id + " ." + linkClass); | |
if (actionDiv && link && link.href) { | |
// add our own class to the post-div so we know we already dealt with it | |
div.classList.add(csClass); | |
url = link.href; | |
menu = global.document.createElement("div"); | |
menu.innerHTML = "Add to Google+ History"; | |
menu.classList.add(menuClass); | |
actionDiv.appendChild(menu); | |
menu.onclick = function () { | |
// when our menu item is clicked this will send a message to the background script | |
// including the link of the post to be added to history | |
global.chrome.extension.sendMessage({activityType: "http://schemas.google.com/AddActivity", url: url}); | |
}; | |
} | |
} | |
// this checks if new post-divs have been added dynamically and adds menu items if necessary | |
global.document.getElementById("contentPane").addEventListener("DOMNodeInserted", updateDOM, false); | |
updateDOM(); | |
}(window)); |
(function (global) { | |
"use strict"; | |
function HistoryApp() { | |
var google, access_token, types; | |
google = new global.OAuth2("google", { | |
client_id: "YOUR_CLIENT_ID", | |
client_secret: "YOUR_CLIENT_SECRET", | |
api_scope: "https://www.googleapis.com/auth/plus.moments.write https://www.googleapis.com/auth/plus.me" | |
}); | |
function createNotification(text, title, image) { | |
var notification; | |
notification = global.webkitNotifications.createNotification( | |
image || global.chrome.extension.getURL("icon48.png"), | |
title || "Item added to Google+ history", | |
text | |
); | |
notification.show(); | |
global.setTimeout(function () { notification.cancel(); }, 5000); | |
} | |
function writeMoment(info) { | |
var xhr, message; | |
message = JSON.stringify(info); | |
xhr = new global.XMLHttpRequest(); | |
xhr.onreadystatechange = function () { | |
var response, text; | |
if (xhr.readyState == 4) { | |
if (xhr.status >= 200 && xhr.status <= 204) { | |
global.console.log("Success: " + xhr.responseText); | |
response = JSON.parse(xhr.responseText); | |
text = response.target.name; | |
createNotification(text, undefined, response.target.image); | |
} else { | |
createNotification("Error " + xhr.status + ": " + xhr.statusText, "Error adding item to Google+ History"); | |
} | |
} | |
}; | |
xhr.open("POST", "https://www.googleapis.com/plus/v1moments/people/me/moments/vault?debug=true", true); | |
xhr.setRequestHeader("Content-Type", "application/json"); | |
xhr.setRequestHeader("Authorization", "OAuth " + access_token); | |
xhr.send(message); | |
} | |
// listen for messages coming from the content script | |
global.chrome.extension.onMessage.addListener( | |
function (request, sender) { | |
// make sure our request contains all necessary information | |
if (request && request.url && request.activityType) { | |
// make sure we have a valid access token | |
google.authorize(function () { | |
access_token = google.getAccessToken(); | |
// write moment to history | |
writeMoment({ | |
"type": request.activityType, | |
"target": { | |
"url": request.url | |
} | |
}); | |
}); | |
} | |
} | |
); | |
} | |
global.historyApp = new HistoryApp(); | |
}(window)); |
As above you have to install the extension via chrome://chrome/extensions/
Once installed you can go to your Google+ Stream and when you open the drop-down menu for a post you will see our new menu item there.
In case adding the moment to history was successful you will get a desktop notification with a summary of what Google has extracted from the target.
You can then also visit plus.google.com/history to see the moment there.
I think that Chrome extensions (or browser extensions in general) will be a very important use case for the Google+ History API. Another interesting area for the Google+ History API will probably be mobile so once the API is a bit more stable I might write an article about this as well. Until then you can always have a look at the official documentation linked below and this article should help you get started with building your own Google+ History API applications.
You can find the full sources for this article on GitHub and on Google Code.
Google+ and Google Chrome are trademarks of Google Inc. Use of these trademarks is subject to Google Permissions.
This site is not affiliated with, sponsored by, or endorsed by Google Inc.
This work is licensed under a Creative Commons Attribution 3.0 Unported License.