Life, the Universe and Everything
Back to mainpage

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.

Google+ History a.k.a. Moments

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.


Why a Chrome Extension?

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.


Authentication

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:


oauth2/adapters/google.js
...
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

API Console 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.

API Console Services

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

API Console Access

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.

Using a Browser Action

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.

Browser Action 01

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));

Running the extension

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.

Installation

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.

Browser Action 01

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.

Browser Action 02

You can then also visit plus.google.com/history to see the moment there.

Browser Action 03

Using a Content Script

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.

Content Script 01

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.json
{
"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));


history_background.js
(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));


Running the extension

As above you have to install the extension via chrome://chrome/extensions/

Installation

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.

Content Script 01

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.

Content Script 02

You can then also visit plus.google.com/history to see the moment there.

Content Script 03

Conclusion

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.


References

History API Documentation: developers.google.com/+/history/
OAuth 2.0 Library for Chrome Extensions: github.com/borismus/oauth2-extensions/
Several code samples by myself: github.com/Scarygami/gplus-experiments or code.google.com/p/gplus-experiments/



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.


Back to mainpage

Creative Commons License This work is licensed under a Creative Commons Attribution 3.0 Unported License.