Skip to content

Commit

Permalink
Add an alternative service for Google Spreadsheets API that uses OAuth2.
Browse files Browse the repository at this point in the history
  • Loading branch information
padelt committed Jan 2, 2016
1 parent efe88d0 commit e9cbc77
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 0 deletions.
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ _mqttwarn_ supports a number of services (listed alphabetically below):
* [file](#file)
* [freeswitch](#freeswitch)
* [gss](#gss)
* [gss2](#gss2)
* [http](#http)
* [instapush](#instapush)
* [ionic](#ionic)
Expand Down Expand Up @@ -656,6 +657,78 @@ Requires:
* [gdata-python-client](https://code.google.com/p/gdata-python-client/)
### `gss2`
The `gss2` service interacts directly with a Google Docs Spreadsheet. Each message can be written to a row in a selected worksheet.
Each target has two parameters:
1. The spreadsheet URL. You can copy the URL from your browser that shows the spreadsheet.
2. The worksheet name. Try "Sheet1".
```ini
[config:gss2]
client_secrets_filename = client_secrets.json
oauth2_code =
oauth2_storage_filename = oauth2.store
targets = {
# spreadsheet_url # worksheet_name
'test': [ 'https://docs.google.com/spre...cdA-ik8uk/edit', 'Sheet1']
# This target would be addressed as 'gss2:test'.
}
```
Note: It is important that the top row into your blank spreadsheet has column headings that correspond the values that represent your dictionary keys. If these column headers are not available or different from the dictionary keys, the new rows will be empty.
Note: Google Spreadsheets initially consist of 100 or 1,000 empty rows. The new rows added by `gss2` will be *below*, so you might want to delete those empty rows.
Other than `gss`, `gss2` uses OAuth 2.0 authentication. It is a lot harder to get working - but it does actually work.
Here is an overview how the authentication with Google works:
1. You obtain a `client_secrets.json` file from Google Developers Console.
1. You reference that file in the `client_secrets_filename` field and restart mqttwarn.
1. You grab an URL from the logs and visit that in your webbrowser.
1. You copy the resulting code to `mqttwarn.ini`, field `oauth2_code`
and restart mqttwarn.
1. `gss2` stores the eventual credentials in the file you specified in
field `oauth2_storage_filename`.
1. Everyone lives happily ever after. I hope you reach this point without
severe technology burnout.
1. Technically, you could remove the code from field `oauth2_code`,
but it does not harm to leave it there.
Now to the details of this process:
The contents of the file `client_secrets_filename` needs to be obtained by you as described in the [Google Developers API Client Library for Python docs](https://developers.google.com/api-client-library/python/auth/installed-app) on OAuth 2.0 for an Installed Application.
Unfortunately, [Google prohibits](http://stackoverflow.com/a/28109307/217001) developers to publish their credentials as part of open source software. So you need to get the credentials yourself.
To get them:
1. Log in to the Google Developers website from
[here](https://developers.google.com/).
1. Follow the instructions in section `Creating application credentials` from
the [OAuth 2.0 for Installed Applications](https://developers.google.com/api-client-library/python/auth/installed-app#creatingcred) chapter.
You are looking for an `OAuth client ID`.
1. In the [Credentials screen of the API manager](https://console.developers.google.com/apis/credentials)
there is a download icon next to your new client ID. The downloaded
file should be named something like `client_secret_664...json`.
1. Store that file near e.g. `mqttwarn.ini` and ensure the setting
`client_secrets_filename` has the valid path name of it.
Then you start with the `gss2` service enabled and with the `client_secrets_filename` readable. Once an event is to be published, you will find an error in the logs with a URL that you need to visit with a web browser that is logged into your Google account. Google will offer you to accept access to
Google Docs/Drive. Once you accept, you get to copy a code that you need to paste into field `oauth2_code` and restart mqttwarn.
The file defined in `oauth2_storage_filename` needs to be missing or writable and will be created or overwritten. Once OAuth credentials have been established (using the `oauth2_code`), they are persisted in there.
Requires:
* [google-api-python-client](https://pypi.python.org/pypi/google-api-python-client/)
(`pip install google-api-python-client`)
* [gspread](https://github.com/burnash/gspread)
(`pip install gspread`)
### `http`
The `http` service allows GET and POST requests to an HTTP service.
Expand Down
115 changes: 115 additions & 0 deletions services/gss2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

__author__ = 'Philipp Adelt <autosort-github@philipp.adelt.net>, based on code by Jan Badenhorst'
__copyright__ = 'Copyright 2016 Philipp Adelt, 2014 Jan Badenhorst'
__license__ = """Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)"""

import os

try:
import json
except ImportError:
import simplejson as json

HAVE_GSS = True
try:
import gspread
import oauth2client.client
import oauth2client.file
from oauth2client import clientsecrets
except ImportError:
HAVE_GSS = False

SCOPE="https://spreadsheets.google.com/feeds"

def plugin(srv, item):

srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target)
if not HAVE_GSS:
srv.logging.error("Google Spreadsheet is not installed. Consider 'pip install gdata'.")
return False

try:
spreadsheet_url = item.addrs[0]
worksheet_name = item.addrs[1]
client_secrets_filename = item.config['client_secrets_filename']
oauth2_code = item.config['oauth2_code']
oauth2_storage_filename = item.config['oauth2_storage_filename']
except KeyError as e:
srv.logging.error("Some configuration item is missing: %s" % str(e))
return False

if not os.path.exists(client_secrets_filename):
srv.logging.error(u"Cannot find file '%s'." % client_secrets)
return False

try:
srv.logging.debug("Adding row to spreadsheet %s [%s]..." % (spreadsheet_url, worksheet_name))
if os.path.isfile(oauth2_storage_filename):
# Valid credentials from previously completed authentication?
srv.logging.debug(u"Trying to use credentials from file '%s'." %
oauth2_storage_filename)
storage = oauth2client.file.Storage(oauth2_storage_filename)
credentials = storage.get()
if credentials is None or credentials.invalid:
srv.logging.error(u"Error reading credentials from file '%s'." %
oauth2_storage_filename)
return False
elif oauth2_code is not None and len(oauth2_code) > 0:
# After restart - hopefully with the code coming from the Google webpage.
srv.logging.debug(u"Trying to use client_secrets from '%s' and OAuth code '%s'." %
(client_secrets_filename, oauth2_code))
try:
credentials = oauth2client.client.credentials_from_clientsecrets_and_code(
client_secrets_filename,
scope=SCOPE,
code=oauth2_code,
redirect_uri='urn:ietf:wg:oauth:2.0:oob')
if credentials is None:
raise clientsecrets.InvalidClientSecretsError("Resulting credentials are None!?")
except clientsecrets.InvalidClientSecretsError as e:
srv.logging.error(u"Something went wrong using '%s' and OAuth code '%s': %s" %
(client_secrets_filename, oauth2_code, str(e)))
return False
except oauth2client.client.FlowExchangeError as e:
if 'invalid_grantCode' in e.message:
srv.logging.error(u"It seems you need to start over: Clear the "
"'oauth2_code'-field and restart mqttwarn.")
return False
else:
raise e

# Store credentials for next event.
storage = oauth2client.file.Storage(oauth2_storage_filename)
storage.put(credentials)

else:
# Start a new authentication flow and scream the URL to visit to the logs.
flow = oauth2client.client.flow_from_clientsecrets(
client_secrets_filename,
scope=SCOPE,
redirect_uri='urn:ietf:wg:oauth:2.0:oob')
auth_uri = flow.step1_get_authorize_url()
srv.logging.error(u'NO AUTHENTICATION AVAILABLE: Visit this URL and copy code to '
'mqttwarn.ini -> config:gss2 -> oauth2_code: %s' % auth_uri)
return False

gc = gspread.authorize(credentials)
wks = gc.open_by_url(spreadsheet_url).worksheet(worksheet_name)
col_names = wks.row_values(1)

# Column names found need to be keys in item.data to end up in the new row.
values = []
for col in col_names:
values.append(item.data.get(col, ""))

wks.append_row(values)

srv.logging.debug("Successfully added row to spreadsheet")

except Exception as e:
srv.logging.warn("Error adding row to spreadsheet %s [%s]: %s" % (spreadsheet_key, worksheet_id, str(e)))
return False

return True

0 comments on commit e9cbc77

Please sign in to comment.