From e9cbc77084cdd7a5eb260a210640ca1a2657a768 Mon Sep 17 00:00:00 2001 From: Philipp Adelt Date: Sat, 2 Jan 2016 23:12:10 +0100 Subject: [PATCH] Add an alternative service for Google Spreadsheets API that uses OAuth2. --- README.md | 73 ++++++++++++++++++++++++++++++ services/gss2.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 services/gss2.py diff --git a/README.md b/README.md index d4137ba8..c7b24565 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. diff --git a/services/gss2.py b/services/gss2.py new file mode 100644 index 00000000..efdf8b5a --- /dev/null +++ b/services/gss2.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = 'Philipp Adelt , 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 \ No newline at end of file