Skip to content

Commit

Permalink
Merge branch 'release/0.4.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
treethought committed Oct 11, 2019
2 parents 6bb46ed + 256df60 commit 03703cf
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 11 deletions.
20 changes: 19 additions & 1 deletion Pipfile
Expand Up @@ -7,9 +7,27 @@ verify_ssl = true
pytest = "*"
pytest-cov = "*"
tox = "*"
black = "*"
flask-assistant = {editable = true,path = "."}

[packages]
flask-assistant = {editable = true,path = "."}
aniso8601 = "==4.0.1"
certifi = "==2018.11.29"
chardet = "==3.0.4"
click = "==7.0"
idna = "==2.8"
itsdangerous = "==1.1.0"
requests = "==2.21.0"
urllib3 = "==1.24.1"
Flask = "==1.0.2"
Jinja2 = "==2.10"
MarkupSafe = "==1.1.0"
"ruamel.yaml" = "==0.15.81"
Werkzeug = "==0.14.1"
google-auth = "*"

[requires]
python_version = "3.7"

[pipenv]
allow_prereleases = true
3 changes: 2 additions & 1 deletion flask_assistant/__init__.py
Expand Up @@ -23,9 +23,10 @@
storage,
session_id,
context_in,
profile
)

from flask_assistant.response import ask, tell, event, build_item, permission
from flask_assistant.response import ask, tell, event, build_item, permission, sign_in
from flask_assistant.manager import Context

import flask_assistant.utils
Expand Down
35 changes: 34 additions & 1 deletion flask_assistant/core.py
Expand Up @@ -40,6 +40,7 @@ def find_assistant(): # Taken from Flask-ask courtesy of @voutilad
session_id = LocalProxy(lambda: find_assistant().session_id)
user = LocalProxy(lambda: find_assistant().user)
storage = LocalProxy(lambda: find_assistant().storage)
profile = LocalProxy(lambda: find_assistant().profile)

# Converter shorthands for commonly used system entities
_converter_shorthands = {
Expand All @@ -65,6 +66,7 @@ class Assistant(object):
blueprint {Flask Blueprint} -- Flask Blueprint instance to initialize (Default: {None})
route {str} -- entry point to which initial Alexa Requests are forwarded (default: {None})
project_id {str} -- Google Cloud Project ID, required to manage contexts from flask-assistant
client_id {Str} -- Actions on Google client ID used for account linking
dev_token {str} - Dialogflow dev access token used to register and retrieve agent resources
client_token {str} - Dialogflow client access token required for querying agent
"""
Expand All @@ -77,12 +79,14 @@ def __init__(
project_id=None,
dev_token=None,
client_token=None,
client_id=None,
):

self.app = app
self.blueprint = blueprint
self._route = route
self.project_id = project_id
self.client_id = client_id
self._intent_action_funcs = {}
self._intent_mappings = {}
self._intent_converts = {}
Expand All @@ -108,6 +112,9 @@ def __init__(
"Assistant object must be intialized with either an app or blueprint"
)

if self.client_id is None:
self.client_id = self.app.config.get("AOG_CLIENT_ID")

if project_id is None:
import warnings

Expand Down Expand Up @@ -243,11 +250,19 @@ def storage(self):

@storage.setter
def storage(self, value):
if not isintance(value, dict):
if not isinstance(value, dict):
raise TypeError("Storage must be a dictionary")

self.user["userStorage"] = value

@property
def profile(self):
return getattr(_app_ctx_stack.top, "_assist_profile", None)

@profile.setter
def profile(self, value):
_app_ctx_stack.top._assist_profile = value

def _register_context_to_func(self, intent_name, context=[]):
required = self._required_contexts.get(intent_name)
if required:
Expand Down Expand Up @@ -372,6 +387,23 @@ def _dump_result(self, view_func, result):
def _parse_session_id(self):
return self.request["session"].split("/sessions/")[1]

def _set_user_profile(self):
if self.client_id is None:
return

if self.user.get("idToken") is not None:
from flask_assistant.utils import decode_token

token = self.user["idToken"]
profile_payload = decode_token(token, self.client_id)
for k in ["sub", "iss", "aud", "iat", "exp"]:
profile_payload.pop(k)

self.profile = profile_payload




def _flask_assitant_view_func(self, nlp_result=None, *args, **kwargs):
if nlp_result: # pass API query result directly
self.request = nlp_result
Expand Down Expand Up @@ -401,6 +433,7 @@ def _flask_assitant_view_func(self, nlp_result=None, *args, **kwargs):
payload = original_request.get("payload")
if payload and payload.get("user"):
self.user = original_request["payload"]["user"]
self._set_user_profile()

# Get access token from request
if original_request and original_request.get("user"):
Expand Down
31 changes: 29 additions & 2 deletions flask_assistant/response.py
Expand Up @@ -209,8 +209,11 @@ def build_item(
}

if img_url:
img_payload = {"imageUri": img_url, "accessibilityText": alt_text or "{} img".format(title)}
item["image"] = img_payload
img_payload = {
"imageUri": img_url,
"accessibilityText": alt_text or "{} img".format(title),
}
item["image"] = img_payload

return item

Expand Down Expand Up @@ -355,3 +358,27 @@ def __init__(self, permissions, context=None, update_intent=None):
},
}


class sign_in(_Response):
"""Initiates the authentication flow for Account Linking
After the user authorizes the action to access their profile, a Google ID token
will be received and validated by the flask-assistant and expose user profile information
with the `user.profile` local
In order to complete the sign in process, you will need to create an intent with
the `actions_intent_SIGN)IN` event
"""

def __init__(self, reason=None):
super(sign_in, self).__init__(speech=None)

self._messages[:] = []
self._response["payload"]["google"]["systemIntent"] = {
"intent": "actions.intent.SIGN_IN",
"data": {
"optContext": reason,
"@type": "type.googleapis.com/google.actions.v2.SignInValueSpec",
},
}

22 changes: 18 additions & 4 deletions flask_assistant/utils.py
@@ -1,16 +1,25 @@
from __future__ import absolute_import
from typing import Dict, Any
import os
import sys
import logging
from google.auth import jwt
from flask_assistant.core import Assistant
from . import logger


logger.setLevel(logging.INFO)

GOOGLE_PUBLIC_KEY = {
"ee4dbd06c06683cb48dddca6b88c3e473b6915b9": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIXlp0tU/OdR8wDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0xOTEwMDMxNDQ5MzRaFw0xOTEwMjAwMzA0MzRaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC41NLGPK9PRi0KjFTIQ9qEirje2IrmWSwZ\n7lmgTzwA4mpc4tqDn7AfUTHmuyhDrbweGq2wQeYJDbBPT5uX86XcQgAcu4IzSuZG\nJZ68ASYOWWlKV0vYjf6W/9v73sGJFxbkoAB8X7QH/fN80QYoXvSX+IwNnePnoikM\nnAsNiZrkLoqHuv5+ahOgpBN5qyvKglasNiXGpv8EL96CKb+nmMudzpypjbQHJUp2\nmfDvOiTX6IuSXyeYRkyzOeX7wqpV1l+TU3A8orMylNe8e+oL/2mAYVzCC9Wk1nq2\nGT4vRRmzrr2GW4eKr9525JQe7BKBKkC2WWhKE+EmqPm2ZFnQ/frlAgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQA+mwyWb+SZMLOiQS1lUgN6iXO2JZm+\nq6rmSYnpLP5nCtwKjHbGSw0DoUchem2g0AYsB/HqNl5zLvJb5CXlP79sTO6Ot7sX\nHI3Mtqw4Fe1QFCC5QVhudpMNKNQYq8P45SrfwMW1qSYYsMXbpmNkIPbFvxib1L0l\niUfnjCXFB4XiDa80Cb73cxmU7a24/j03o42kjcRX2BtXs6jhP7z8BxnDCjybjlLT\no5gBombtlPKgOTdcF+eKdaO1LLQ+9LmueiZH/HCsvAmmxLT9g1XZCEFg5zttdetT\nVV+03sFBGhoJPbChiOJMdH8IQVEdtpvnAiYyVBYEUSj7CWSZEI50syL2\n-----END CERTIFICATE-----\n",
"8c58e138614bd58742172bd5080d197d2b2dd2f3": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIFqFLh91FQ0kwDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0xOTA5MjUxNDQ5MzRaFw0xOTEwMTIwMzA0MzRaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ/fKZfxfVD68YAuMfl5bnoNBjZ4kyhXMi\nffkizGpJGkMR2gL6ansSYLrd94Gn/W5FH0hMLCWK41gBXXI6alI6y0YNGSeGnmbO\nZz8Si+oMfiPAj9NaawMwNnusdMqrkMUrMBUmWSTzk4ttu3U9TVkIXZ5i0LNvntJO\nuG+Ga4A+CaipE6Y1QoXkFwFDDum8qpXYKMlF0pSbGz/Nb2o1RjINlo9gx+KWgaPK\n2wWw+n6XJDLFtcmhRtQPuMDpxRveY63OlE4CCRhnJLvjSD4ZlxUtqmoDALpRnGUI\n36VdsZpvtz5CsIy8PB+7ZTcBBK8jfy73kxpuuMdlaxZEJuolV8cjAgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQCY7YX8EDcPQGFpklGVtqN7tlfm+Gi8\n0v3E9WoQUiMQJ9Mt34ixd3zPjMeKOtVxj6BZzHrpyR6rM9lB4nzoyCWf9K86HaWS\nuxtAACj7yovKAh2x5pFwEB014qNdG03YYQFy8MvDxegaL4soWqCa2UfEK4vdWyJi\nZKM0iT6s78VY6vOWxK+z1IC/6AYbyskzv57T+dBUwGcwQEf0yBeu89tR7LFSaVV/\n6A+dTGyFlHysR6dddwJvzl7jG83RQs2L58qIISD+6RdRxcD02h388YhhMy9Nrpmo\nZeXouJ7YLsHGFkn3yfWi3KWYVdTGbd/9BQPBjhKzS93SxdolTKKVOI/7\n-----END CERTIFICATE-----\n",
"3db3ed6b9574ee3fcd9f149e59ff0eef4f932153": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIeBPD3wqfL6EwDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0xOTEwMTExNDQ5MzRaFw0xOTEwMjgwMzA0MzRaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDYpym/gLFOh4IoQhfOeGo+DbUyEIA/0Odf\nmzb9R1nVvM5WFHyqKiT8/yPvLxgXYzYlzyvZu18KAkYWWNuS21Vzhe+d4949P6EZ\n/096QjVFSHvKTo94bSQImeZxZiBhfFcvw/RMM0eTeZZPgOXI3YIJyWjAZ9FUslt7\nWoLU0HZFc/JyPRF8M2kinkdYxnzA+MjzCetXlqmhAr+wLPg/QLKwACyRIF2FJHgf\nPsvqaeF7JXo0zHPcGuHUOqXCHon6KiHZF7OC4bzTuTEzVipJTLYy9QUyL4M2L8bQ\nu1ISUSaXhj+i1WT0RDJwqpioOFprVFqqkVvbUW0nXD/x1UA4nvf7AgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQBr5+4ZvfhP436NdJgN0Jn7iwwVArus\nXUn0hfuBbCoj1DhuRkP9wyLCpOo6cQS0T5bURVZzirsKc5sXP4fNYXqbfLaBpc7n\njTUtTOIqoA4LKPU7/FH6Qt/UfZ4DQIsKaD3087KdY3ePatSn/HTxvT8Ghqy/JGjf\nLXZehQnlyyCRaCMqv1gEOMiY/8LG3d1hLL7CMphnb4ASk0YMKrWkKhIoa6NWU2Rd\nqp01F4iG44ABpea+ymXAGmWBVPnep51kr/wIPIzr9WvNFAAZW2Enk3+kUWNupuz+\npdXq9KnegVsCs4G7QcTPqwc/vMu7uGq/pruDEOYVOd9Rm+rr0wlMgkcf\n-----END CERTIFICATE-----\n",
}


def import_with_3(module_name, path):
import importlib.util

spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
Expand All @@ -19,6 +28,7 @@ def import_with_3(module_name, path):

def import_with_2(module_name, path):
import imp

return imp.load_source(module_name, path)


Expand All @@ -28,13 +38,17 @@ def get_assistant(filename):
agent_name = os.path.splitext(filename)[0]

try:
agent_module = import_with_3(
agent_name, os.path.join(os.getcwd(), filename))
agent_module = import_with_3(agent_name, os.path.join(os.getcwd(), filename))

except ImportError:
agent_module = import_with_2(
agent_name, os.path.join(os.getcwd(), filename))
agent_module = import_with_2(agent_name, os.path.join(os.getcwd(), filename))

for name, obj in agent_module.__dict__.items():
if isinstance(obj, Assistant):
return obj


def decode_token(token, client_id):
decoded = jwt.decode(token, certs=GOOGLE_PUBLIC_KEY, verify=True, audience=client_id)
return decoded

6 changes: 6 additions & 0 deletions requirements.txt
@@ -1,13 +1,19 @@
aniso8601==4.0.1
cachetools==3.1.1
certifi==2018.11.29
chardet==3.0.4
click==7.0
flask==1.0.2
google-auth==1.6.3
idna==2.8
itsdangerous==1.1.0
jinja2==2.10
markupsafe==1.1.0
pyasn1-modules==0.2.7
pyasn1==0.4.7
requests==2.21.0
rsa==4.0
ruamel.yaml==0.15.81
six==1.12.0
urllib3==1.24.1
werkzeug==0.14.1
32 changes: 32 additions & 0 deletions samples/account_linking/webhook.py
@@ -0,0 +1,32 @@
from flask import Flask
from flask_assistant import Assistant, ask, profile, sign_in


app = Flask(__name__)

app.config['INTEGRATIONS'] = ['ACTIONS_ON_GOOGLE']
app.config['AOG_CLIENT_ID'] = "CLIENT_ID OBTAINED BY SETTING UP ACCOUNT LINKING IN AOG CONSOLE"


assist = Assistant(app=app, route="/", project_id="YOUR_GCP_PROJECT_ID")

@assist.action("Default Welcome Intent")
def welcome():
if profile:
return ask(f"Welcome back {profile['name']}")

return sign_in("To learn more about you")

# this intent must have the actions_intent_SIGN_IN event
# and will be invoked once the user has
@assist.action("Complete-Sign-In")
def complete_sign_in():
if profile:
return ask(f"Welcome aboard {profile['name']}, thanks for signing up!")
else:
return ask("Hope you sign up soon! Would love to get to know you!")


if __name__ == "__main__":
app.run(debug=True)

4 changes: 2 additions & 2 deletions setup.py
Expand Up @@ -11,7 +11,7 @@

setup(
name="Flask-Assistant",
version="0.3.91",
version="0.4.0",
url="https://github.com/treethought/flask-assistant",
license="Apache 2.0",
author="Cam Sweeney",
Expand All @@ -23,7 +23,7 @@
zip_safe=False,
include_package_data=True,
platforms="any",
install_requires=["Flask", "requests", "ruamel.yaml", "aniso8601"],
install_requires=["Flask", "requests", "ruamel.yaml", "aniso8601", "google-auth"],
setup_requires=["pytest-runner"],
tests_require=["pytest"],
test_suite="tests",
Expand Down

0 comments on commit 03703cf

Please sign in to comment.