From 834b700d5f0e06bcee7968474741b0202fa6e7a7 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 17 Jan 2018 03:16:59 +0100 Subject: [PATCH] Added GMail actions support, solves #49 --- platypush/message/request/__init__.py | 21 +++++-- platypush/plugins/google/__init__.py | 36 +++++++++++ platypush/plugins/google/credentials.py | 82 +++++++++++++++++++++++++ platypush/plugins/google/mail.py | 77 +++++++++++++++++++++++ platypush/procedure/__init__.py | 7 ++- requirements.txt | 3 + setup.py | 1 + 7 files changed, 219 insertions(+), 8 deletions(-) create mode 100644 platypush/plugins/google/__init__.py create mode 100644 platypush/plugins/google/credentials.py create mode 100644 platypush/plugins/google/mail.py diff --git a/platypush/message/request/__init__.py b/platypush/message/request/__init__.py index fb2325d92..d1c7da99e 100644 --- a/platypush/message/request/__init__.py +++ b/platypush/message/request/__init__.py @@ -113,14 +113,19 @@ class Request(Message): if context_argname in context: try: - context_value = eval("context['{}']{}".format( - context_argname, path if path else '')) + try: + context_value = eval("context['{}']{}".format( + context_argname, path if path else '')) + except: + context_value = eval(inner_expr) if callable(context_value): context_value = context_value() if isinstance(context_value, datetime.date): context_value = context_value.isoformat() - except: context_value = expr + except Exception as e: + logging.exception(e) + context_value = expr parsed_value += prefix + ( json.dumps(context_value) @@ -128,7 +133,12 @@ class Request(Message): or isinstance(context_value, dict) else str(context_value)) - else: parsed_value += prefix + expr + else: + try: + parsed_value += prefix + eval(inner_expr) + except Exception as e: + logging.exception(e) + parsed_value += prefix + expr else: parsed_value += value value = '' @@ -162,7 +172,8 @@ class Request(Message): def _thread_func(n_tries): if self.action.startswith('procedure.'): - response = self._execute_procedure(n_tries=n_tries) + context['n_tries'] = n_tries + response = self._execute_procedure(**context) self._send_response(response) return response else: diff --git a/platypush/plugins/google/__init__.py b/platypush/plugins/google/__init__.py new file mode 100644 index 000000000..a2761bd80 --- /dev/null +++ b/platypush/plugins/google/__init__.py @@ -0,0 +1,36 @@ +import os + +from platypush.plugins import Plugin +from platypush.plugins.google.credentials import get_credentials + + +class GooglePlugin(Plugin): + """ + Executes calls to the Google APIs using the google-api-python-client. + In order to use Google services (like GMail, Maps, Calendar etc.) with + your account you need to: + + 1. Create your Google application, if you don't have one already, on + the developers console, https://console.developers.google.com + + 2. Click on "Credentials", then "Create credentials" -> "OAuth client ID" + + 3 Select "Other", enter whichever description you like, and create + + 4. Click on the "Download JSON" icon next to your newly created client ID + + 5. Generate a credentials file for the needed scope: + + $ python -m platypush.plugins.google.credentials 'https://www.googleapis.com/auth/gmail.compose' ~/client_secret.json + """ + + def __init__(self, scopes, *args, **kwargs): + super().__init__(*args, **kwargs) + self.credentials = {} + + for scope in scopes: + self.credentials[scope] = get_credentials(scope) + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/plugins/google/credentials.py b/platypush/plugins/google/credentials.py new file mode 100644 index 000000000..8086d1953 --- /dev/null +++ b/platypush/plugins/google/credentials.py @@ -0,0 +1,82 @@ +import argparse +import httplib2 +import os +import sys + +from oauth2client import client +from oauth2client import tools +from oauth2client.file import Storage + + +def get_credentials_filename(scope): + from platypush.config import Config + + scope_name = scope.split('/')[-1] + credentials_dir = os.path.join( + Config.get('workdir'), 'credentials', 'google') + + if not os.path.exists(credentials_dir): + os.makedirs(credentials_dir) + + return os.path.join(credentials_dir, scope_name + '.json') + + +def get_credentials(scope): + credentials_file = get_credentials_filename(scope) + if not os.path.exists(credentials_file): + raise RuntimeError('Credentials file {} not found. Generate it through:\n' + + '\tpython -m platypush.google.credentials "{}"' + + '\n' + + '\t\t[--auth_host_name AUTH_HOST_NAME]\n' + + '\t\t[--noauth_local_webserver]\n' + + '\t\t[--auth_host_port [AUTH_HOST_PORT [AUTH_HOST_PORT ...]]]\n' + + '\t\t[--logging_level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]\n'. + format(credentials_file, scope_name)) + + store = Storage(credentials_file) + credentials = store.get() + + if not credentials or credentials.invalid: + credentials.refresh(httplib2.Http()) + + return credentials + + +def generate_credentials(client_secret_path, scope): + credentials_file = get_credentials_filename(scope) + store = Storage(credentials_file) + + flow = client.flow_from_clientsecrets(client_secret_path, scope) + flow.user_agent = 'Platypush' + flow.access_type = 'offline' + flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args() + credentials = tools.run_flow(flow, store, flags) + print('Storing credentials to ' + credentials_file) + + +def main(): + """ + Generates a Google API credentials file given client secret JSON and scopes. + Usage: + python -m platypush.plugins.google.credentials [client_secret.json location] [comma-separated list of scopes] + """ + + scope = sys.argv.pop(1) if len(sys.argv) > 1 \ + else input('Comma separated list of OAuth scopes: ') + + client_secret_path = os.path.expanduser( + sys.argv.pop(1) if len(sys.argv) > 1 + else input('Google credentials JSON file location: ')) + + # Uncomment to force headless (no browser spawned) authentication + # sys.argv.append('--noauth_local_webserver') + + generate_credentials(client_secret_path, scope) + + +if __name__ == '__main__': + main() + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/plugins/google/mail.py b/platypush/plugins/google/mail.py new file mode 100644 index 000000000..1240912ad --- /dev/null +++ b/platypush/plugins/google/mail.py @@ -0,0 +1,77 @@ +import base64 +import httplib2 +import mimetypes + +from apiclient import discovery + +from email.mime.audio import MIMEAudio +from email.mime.base import MIMEBase +from email.mime.image import MIMEImage +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +from platypush.message.response import Response +from platypush.plugins.google import GooglePlugin + + +class GoogleMailPlugin(GooglePlugin): + scopes = ['https://www.googleapis.com/auth/gmail.modify'] + + def __init__(self, *args, **kwargs): + super().__init__(scopes=self.scopes, *args, **kwargs) + + + def compose(self, sender, to, subject, body, file=None): + message = MIMEMultipart() if file else MIMEText(body) + message['to'] = to + message['from'] = sender + message['subject'] = subject + + if file: + msg = MIMEText(body) + message.attach(msg) + content_type, encoding = mimetypes.guess_type(file) + + if content_type is None or encoding is not None: + content_type = 'application/octet-stream' + main_type, sub_type = content_type.split('/', 1) + + with open(file, 'rb') as fp: + if main_type == 'text': + msg = mimetypes.MIMEText(fp.read(), _subtype=sub_type) + elif main_type == 'image': + msg = MIMEImage(fp.read(), _subtype=sub_type) + elif main_type == 'audio': + msg = MIMEAudio(fp.read(), _subtype=sub_type) + else: + msg = MIMEBase(main_type, sub_type) + msg.set_payload(fp.read()) + + filename = os.path.basename(file) + msg.add_header('Content-Disposition', 'attachment', filename=filename) + message.attach(msg) + + service = self._get_service() + body = { 'raw': base64.urlsafe_b64encode(message.as_bytes()).decode() } + message = (service.users().messages().send( + userId='me', body=body).execute()) + + return Response(output=message) + + + def get_labels(self): + service = self._get_service() + results = service.users().labels().list(userId='me').execute() + labels = results.get('labels', []) + return Response(output=labels) + + + def _get_service(self): + scope = self.scopes[0] + credentials = self.credentials[scope] + http = credentials.authorize(httplib2.Http()) + return discovery.build('gmail', 'v1', http=http, cache_discovery=False) + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/procedure/__init__.py b/platypush/procedure/__init__.py index 0f5b49305..958235edf 100644 --- a/platypush/procedure/__init__.py +++ b/platypush/procedure/__init__.py @@ -78,7 +78,8 @@ class Procedure(object): response = Response() for request in self.requests: - response = request.execute(n_tries, async=self.async, **context) + context['async'] = self.async; context['n_tries'] = n_tries + response = request.execute(**context) if not self.async: if isinstance(response.output, dict): @@ -125,13 +126,13 @@ class LoopProcedure(Procedure): self.requests = requests - def execute(self, n_tries=1, async=None, **context): + def execute(self, async=None, **context): iterable = Request.expand_value_from_context(self.iterable, **context) response = Response() for item in iterable: context[self.iterator_name] = item - response = super().execute(n_tries, **context) + response = super().execute(**context) return response diff --git a/requirements.txt b/requirements.txt index 0f233f03e..c34125bad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,9 @@ ouimeaux google-assistant-sdk[samples] google-assistant-library +# Google APIs general layer support +google-api-python-client + # Last.FM scrobbler plugin support pylast diff --git a/setup.py b/setup.py index 38ccf1c7c..9f3ade911 100755 --- a/setup.py +++ b/setup.py @@ -73,6 +73,7 @@ setup( 'Support for OMXPlayer plugin': ['omxplayer'], 'Support for YouTube in the OMXPlayer plugin': ['youtube-dl'], 'Support for Google Assistant': ['google-assistant-library'], + 'Support for the Google APIs': ['google-api-python-client'], 'Support for most of the HTTP poll backends': ['python-dateutil'], 'Support for Last.FM scrobbler plugin': ['pylast'], # 'Support for Flic buttons': ['git+ssh://git@github.com/50ButtonsEach/fliclib-linux-hci']