From c6837188dbaab895d425504a191a163da6187203 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Tue, 3 Jan 2017 16:21:03 -0800 Subject: [PATCH] add alexa support probably rough around the edges still --- composer.json | 3 +- composer.lock | 43 ++++- controllers/alexa.php | 257 +++++++++++++++++++++++++++++ controllers/controllers.php | 24 ++- lib/config.template.php | 7 + public/images/teacup-alexa-512.png | Bin 0 -> 27111 bytes public/images/teacup-icon-108.png | Bin 0 -> 4877 bytes public/index.php | 1 + schema/migrations/0002.sql | 3 + schema/schema.sql | 2 + views/alexa-auth.php | 19 +++ views/partials/footer.php | 6 +- views/privacy.php | 8 + views/settings.php | 36 ++++ 14 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 controllers/alexa.php create mode 100644 public/images/teacup-alexa-512.png create mode 100644 public/images/teacup-icon-108.png create mode 100644 schema/migrations/0002.sql create mode 100644 views/alexa-auth.php create mode 100644 views/privacy.php create mode 100644 views/settings.php diff --git a/composer.json b/composer.json index 46caa25..6973539 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "indieauth/client": "0.1.*", "mpratt/relativetime": ">=1.0", "firebase/php-jwt": "^4.0", - "p3k/multipart": "*" + "p3k/multipart": "*", + "minicodemonkey/amazon-alexa-php": "^0.1.5" }, "autoload": { "files": [ diff --git a/composer.lock b/composer.lock index 9412a67..1b534bc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "c399b5d1b32e020f809404e3bfa32275", - "content-hash": "63122eac6f996b58bc73ed1359b12099", + "hash": "ebaa6e4aed4c7cfa5a835cbe2b92e629", + "content-hash": "4b3b194b582b716faa00f4da8ce8859b", "packages": [ { "name": "barnabywalters/mf-cleaner", @@ -329,6 +329,45 @@ ], "time": "2015-07-12 14:10:01" }, + { + "name": "minicodemonkey/amazon-alexa-php", + "version": "0.1.5", + "source": { + "type": "git", + "url": "https://github.com/MiniCodeMonkey/amazon-alexa-php.git", + "reference": "006a1e1e775d8429574cde1a093a8b9a4da6a960" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MiniCodeMonkey/amazon-alexa-php/zipball/006a1e1e775d8429574cde1a093a8b9a4da6a960", + "reference": "006a1e1e775d8429574cde1a093a8b9a4da6a960", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Alexa\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Hansen", + "email": "me@codemonkey.io" + } + ], + "description": "Amazon Alexa interface for PHP", + "time": "2015-11-29 21:50:59" + }, { "name": "mpratt/relativetime", "version": "1.5.4", diff --git a/controllers/alexa.php b/controllers/alexa.php new file mode 100644 index 0000000..503bc11 --- /dev/null +++ b/controllers/alexa.php @@ -0,0 +1,257 @@ +get('/alexa', function() use($app) { + render('alexa', array( + 'title' => 'Teacup for Alexa' + )); +}); + +$app->get('/alexa/auth', function() use($app) { + $req = $app->request(); + $params = $req->params(); + + $required = ['client_id', 'response_type', 'state', 'redirect_uri']; + $params_present = array_keys($params); + + // Validate Alexa OAuth parameters + if(count(array_intersect($required, $params_present)) != count($required)) { + render('auth_error', array( + 'title' => 'Sign In', + 'error' => 'Missing parameters', + 'errorDescription' => 'One or more required parameters were missing', + 'footer' => false + )); + return; + } + + // Check that redirect URI is one that is allowed + if(!in_array($params['redirect_uri'], Config::$alexaRedirectURIs)) { + render('auth_error', array( + 'title' => 'Sign In', + 'error' => 'Invalid redirect URI', + 'errorDescription' => 'Alexa sent an invalid redirect URI', + 'footer' => false + )); + return; + } + + if($params['client_id'] != Config::$alexaClientID) { + render('auth_error', array( + 'title' => 'Sign In', + 'error' => 'Invalid Client ID', + 'errorDescription' => 'Alexa sent an invalid client ID', + 'footer' => false + )); + return; + } + + // Pass through the OAuth parameters + render('alexa-auth', [ + 'title' => 'Teacup for Alexa', + 'client_id' => $params['client_id'], + 'response_type' => $params['response_type'], + 'state' => $params['state'], + 'redirect_uri' => $params['redirect_uri'], + 'footer' => false + ]); +}); + +$app->post('/alexa/login', function() use($app) { + $req = $app->request(); + $params = $req->params(); + + file_put_contents('logs/login.txt', json_encode($params)); + + $required = ['code', 'client_id', 'state', 'redirect_uri']; + $params_present = array_keys($params); + + if(count(array_intersect($required, $params_present)) != count($required)) { + render('auth_error', array( + 'title' => 'Sign In', + 'error' => 'Missing parameters', + 'errorDescription' => 'One or more required parameters were missing', + 'footer' => false + )); + return; + } + + $user = ORM::for_table('users') + ->where('device_code', $params['code']) + ->where_gt('device_code_expires', date('Y-m-d H:i:s'))->find_one(); + + if(!$user) { + render('auth_error', array( + 'title' => 'Sign In', + 'error' => 'Invalid code', + 'errorDescription' => 'The code you entered is invalid or has expired', + 'footer' => false + )); + return; + } + + $code = JWT::encode(array( + 'user_id' => $user->id, + 'iat' => time(), + 'exp' => time()+300, + 'client_id' => $params['client_id'], + 'state' => $params['state'], + 'redirect_uri' => $params['redirect_uri'], + ), Config::$jwtSecret); + + $redirect = $params['redirect_uri'] . '?code=' . $code . '&state=' . $params['state']; + + $app->redirect($redirect, 302); +}); + +$app->post('/alexa/token', function() use($app) { + $req = $app->request(); + $params = $req->params(); + // Alexa requests a token given a code generated above + + // Verify the client ID and secret + if($params['client_id'] != Config::$alexaClientID + || $params['client_secret'] != Config::$alexaClientSecret) { + $app->response->setStatus(400); + $app->response()['Content-type'] = 'application/json'; + $app->response()->body(json_encode([ + 'error' => 'forbidden', + 'error_description' => 'The client ID and secret do not match' + ])); + return; + } + + if(array_key_exists('code', $params)) { + $jwt = $params['code']; + } elseif(array_key_exists('refresh_token', $params)) { + $jwt = $params['refresh_token']; + } else { + $app->response->setStatus(400); + $app->response()['Content-type'] = 'application/json'; + $app->response()->body(json_encode([ + 'error' => 'bad_request', + 'error_description' => 'Must provide either an authorization code or refresh token' + ])); + return; + } + + // Validate the JWT + try { + $user = JWT::decode($jwt, Config::$jwtSecret, ['HS256']); + } catch(Exception $e) { + $app->response->setStatus(400); + $app->response()['Content-type'] = 'application/json'; + $app->response()->body(json_encode([ + 'error' => 'unauthorized', + 'error_description' => 'The authorization code or refresh token was invalid' + ])); + return; + } + + // Generate an access token and refresh token + $access_token = JWT::encode([ + 'user_id' => $user->user_id, + 'client_id' => $user->client_id, + 'iat' => time(), + ], Config::$jwtSecret); + $refresh_token = JWT::encode([ + 'user_id' => $user->user_id, + 'client_id' => $user->client_id, + 'iat' => time(), + ], Config::$jwtSecret); + + + $app->response()['Content-type'] = 'application/json'; + $app->response()->body(json_encode([ + 'access_token' => $access_token, + 'refresh_token' => $refresh_token + ])); +}); + + +$app->post('/alexa/endpoint', function() use($app) { + + $input = file_get_contents('php://input'); + $json = json_decode($input, 'input'); + + $alexaRequest = \Alexa\Request\Request::fromData($json); + + if($alexaRequest instanceof Alexa\Request\IntentRequest) { + # file_put_contents('logs/request.txt', $input); + + # Verify the access token + try { + $data = JWT::decode($alexaRequest->user->accessToken, Config::$jwtSecret, ['HS256']); + } catch(Exception $e) { + $app->response->setStatus(401); + $app->response()['Content-type'] = 'application/json'; + $app->response()->body(json_encode([ + 'error' => 'unauthorized', + 'error_description' => 'The access token was invalid or has expired' + ])); + return; + } + + $user = ORM::for_table('users')->find_one($data->user_id); + + if(!$user) { + $app->response->setStatus(400); + return; + } + + $action = $alexaRequest->slots['Action']; + $food = ucfirst($alexaRequest->slots['Food']); + + + $entry = ORM::for_table('entries')->create(); + $entry->user_id = $user->id; + $entry->type = ($action == 'drank' ? 'drink' : 'eat'); + $entry->content = $food; + $entry->published = date('Y-m-d H:i:s'); + $entry->save(); + + $text_content = 'Just ' . $action . ': ' . $food; + + if($user->micropub_endpoint) { + $mp_request = array( + 'h' => 'entry', + 'published' => date('Y-m-d H:i:s'), + 'summary' => $text_content + ); + if($user->enable_array_micropub) { + $mp_request[$action] = [ + 'type' => 'h-food', + 'properties' => [ + 'name' => $food + ] + ]; + } else { + $mp_request['p3k-food'] = $food; + $mp_request['p3k-type'] = $entry->type; + } + + $r = micropub_post($user->micropub_endpoint, $mp_request, $user->access_token); + $request = $r['request']; + $response = $r['response']; + + $entry->micropub_response = $response; + if($response && preg_match('/Location: (.+)/', $response, $match)) { + $url = $match[1]; + $entry->micropub_success = 1; + $entry->canonical_url = $url; + } else { + $entry->micropub_success = 0; + $url = Config::$base_url . $user->url . '/' . $entry->id; + } + $entry->save(); + } + + + $response = new \Alexa\Response\Response; + $response->respond('Got it!') + ->withCard('You '.$action.': '.$food); + + $app->response()['Content-type'] = 'application/json'; + $app->response()->body(json_encode($response->render())); + } +}); diff --git a/controllers/controllers.php b/controllers/controllers.php index 9e94902..03b3cc9 100644 --- a/controllers/controllers.php +++ b/controllers/controllers.php @@ -76,8 +76,24 @@ $app->get('/new', function() use($app) { $app->get('/settings', function() use($app) { if($user=require_login($app)) { - $html = - $app->response()->body($html); + render('settings', [ + 'title' => 'Settings', + ]); + } +}); + +$app->post('/settings/device-code', function() use($app) { + if($user=require_login($app)) { + $code = mt_rand(100000,999999); + + $user->device_code = $code; + $user->device_code_expires = date('Y-m-d H:i:s', time()+300); + $user->save(); + + $app->response()['Content-Type'] = 'application/json'; + $app->response()->body(json_encode([ + 'code' => $code + ])); } }); @@ -111,6 +127,10 @@ $app->get('/docs', function() use($app) { render('docs', array('title' => 'Documentation')); }); +$app->get('/privacy', function() use($app) { + render('privacy', array('title' => 'Privacy Policy')); +}); + $app->get('/add-to-home', function() use($app) { $params = $app->request()->params(); header("Cache-Control: no-cache, must-revalidate"); diff --git a/lib/config.template.php b/lib/config.template.php index 3f200dc..1c70676 100644 --- a/lib/config.template.php +++ b/lib/config.template.php @@ -12,5 +12,12 @@ class Config { public static $jwtSecret = 'xxx'; public static $mf2Debug = false; + + public static $alexaClientID = ''; + public static $alexaClientSecret = ''; + public static $alexaRedirectURIs = [ + '', + '' + ]; } diff --git a/public/images/teacup-alexa-512.png b/public/images/teacup-alexa-512.png new file mode 100644 index 0000000000000000000000000000000000000000..f3ea1dc077274e31706614bb6b77a3f3418a11cf GIT binary patch literal 27111 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 si7xjH`Ur+`d!1f + + +

Sign in to Teacup

+ +

Go to teacup.p3k.io on your phone or computer and sign in.

+

Then go to the settings page to get a "device code", and enter that code here.

+ +
+
+ +
+ + + + +
+ + \ No newline at end of file diff --git a/views/partials/footer.php b/views/partials/footer.php index 103d524..95418a7 100644 --- a/views/partials/footer.php +++ b/views/partials/footer.php @@ -11,7 +11,7 @@ -

© by Aaron Parecki. +

© by Aaron Parecki. This code is open source. Feel free to send a pull request, or file an issue.

- \ No newline at end of file + diff --git a/views/privacy.php b/views/privacy.php new file mode 100644 index 0000000..3b84b11 --- /dev/null +++ b/views/privacy.php @@ -0,0 +1,8 @@ +
+ + +

Privacy Policy

+ + + +
\ No newline at end of file diff --git a/views/settings.php b/views/settings.php new file mode 100644 index 0000000..0b1a6fb --- /dev/null +++ b/views/settings.php @@ -0,0 +1,36 @@ +
+ +
+ +

Device Code

+

If you are prompted to log in on a device, click the button below to generate a device code.

+ +
+ +
+ +
+ +
+ +