Added GMail actions support, solves #49
This commit is contained in:
parent
d629fc897f
commit
834b700d5f
7 changed files with 219 additions and 8 deletions
|
@ -113,14 +113,19 @@ class Request(Message):
|
||||||
|
|
||||||
if context_argname in context:
|
if context_argname in context:
|
||||||
try:
|
try:
|
||||||
context_value = eval("context['{}']{}".format(
|
try:
|
||||||
context_argname, path if path else ''))
|
context_value = eval("context['{}']{}".format(
|
||||||
|
context_argname, path if path else ''))
|
||||||
|
except:
|
||||||
|
context_value = eval(inner_expr)
|
||||||
|
|
||||||
if callable(context_value):
|
if callable(context_value):
|
||||||
context_value = context_value()
|
context_value = context_value()
|
||||||
if isinstance(context_value, datetime.date):
|
if isinstance(context_value, datetime.date):
|
||||||
context_value = context_value.isoformat()
|
context_value = context_value.isoformat()
|
||||||
except: context_value = expr
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
context_value = expr
|
||||||
|
|
||||||
parsed_value += prefix + (
|
parsed_value += prefix + (
|
||||||
json.dumps(context_value)
|
json.dumps(context_value)
|
||||||
|
@ -128,7 +133,12 @@ class Request(Message):
|
||||||
or isinstance(context_value, dict)
|
or isinstance(context_value, dict)
|
||||||
else str(context_value))
|
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:
|
else:
|
||||||
parsed_value += value
|
parsed_value += value
|
||||||
value = ''
|
value = ''
|
||||||
|
@ -162,7 +172,8 @@ class Request(Message):
|
||||||
|
|
||||||
def _thread_func(n_tries):
|
def _thread_func(n_tries):
|
||||||
if self.action.startswith('procedure.'):
|
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)
|
self._send_response(response)
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
|
|
36
platypush/plugins/google/__init__.py
Normal file
36
platypush/plugins/google/__init__.py
Normal file
|
@ -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:
|
||||||
|
|
82
platypush/plugins/google/credentials.py
Normal file
82
platypush/plugins/google/credentials.py
Normal file
|
@ -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 "{}"' +
|
||||||
|
'<path to client_secret.json>\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:
|
||||||
|
|
77
platypush/plugins/google/mail.py
Normal file
77
platypush/plugins/google/mail.py
Normal file
|
@ -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:
|
||||||
|
|
|
@ -78,7 +78,8 @@ class Procedure(object):
|
||||||
response = Response()
|
response = Response()
|
||||||
|
|
||||||
for request in self.requests:
|
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 not self.async:
|
||||||
if isinstance(response.output, dict):
|
if isinstance(response.output, dict):
|
||||||
|
@ -125,13 +126,13 @@ class LoopProcedure(Procedure):
|
||||||
self.requests = requests
|
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)
|
iterable = Request.expand_value_from_context(self.iterable, **context)
|
||||||
response = Response()
|
response = Response()
|
||||||
|
|
||||||
for item in iterable:
|
for item in iterable:
|
||||||
context[self.iterator_name] = item
|
context[self.iterator_name] = item
|
||||||
response = super().execute(n_tries, **context)
|
response = super().execute(**context)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,9 @@ ouimeaux
|
||||||
google-assistant-sdk[samples]
|
google-assistant-sdk[samples]
|
||||||
google-assistant-library
|
google-assistant-library
|
||||||
|
|
||||||
|
# Google APIs general layer support
|
||||||
|
google-api-python-client
|
||||||
|
|
||||||
# Last.FM scrobbler plugin support
|
# Last.FM scrobbler plugin support
|
||||||
pylast
|
pylast
|
||||||
|
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -73,6 +73,7 @@ setup(
|
||||||
'Support for OMXPlayer plugin': ['omxplayer'],
|
'Support for OMXPlayer plugin': ['omxplayer'],
|
||||||
'Support for YouTube in the OMXPlayer plugin': ['youtube-dl'],
|
'Support for YouTube in the OMXPlayer plugin': ['youtube-dl'],
|
||||||
'Support for Google Assistant': ['google-assistant-library'],
|
'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 most of the HTTP poll backends': ['python-dateutil'],
|
||||||
'Support for Last.FM scrobbler plugin': ['pylast'],
|
'Support for Last.FM scrobbler plugin': ['pylast'],
|
||||||
# 'Support for Flic buttons': ['git+ssh://git@github.com/50ButtonsEach/fliclib-linux-hci']
|
# 'Support for Flic buttons': ['git+ssh://git@github.com/50ButtonsEach/fliclib-linux-hci']
|
||||||
|
|
Loading…
Reference in a new issue