Browse Source

add alexa support

probably rough around the edges still
pull/10/head
Aaron Parecki 2 years ago
parent
commit
c6837188db
No known key found for this signature in database

+ 2
- 1
composer.json View File

@@ -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": [

+ 41
- 2
composer.lock View File

@@ -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",

+ 257
- 0
controllers/alexa.php View File

@@ -0,0 +1,257 @@
<?php
use \Firebase\JWT\JWT;

$app->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()));
}
});

+ 22
- 2
controllers/controllers.php View File

@@ -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");

+ 7
- 0
lib/config.template.php View File

@@ -12,5 +12,12 @@ class Config {
public static $jwtSecret = 'xxx';

public static $mf2Debug = false;

public static $alexaClientID = '';
public static $alexaClientSecret = '';
public static $alexaRedirectURIs = [
'',
''
];
}


BIN
public/images/teacup-alexa-512.png View File


BIN
public/images/teacup-icon-108.png View File


+ 1
- 0
public/index.php View File

@@ -14,6 +14,7 @@ $app = new \Slim\Slim(array(
require 'controllers/auth.php';
require 'controllers/controllers.php';
require 'controllers/pebble.php';
require 'controllers/alexa.php';

session_name('teacup');
session_set_cookie_params(86400*30);

+ 3
- 0
schema/migrations/0002.sql View File

@@ -0,0 +1,3 @@
ALTER TABLE users
ADD COLUMN `device_code` varchar(10) DEFAULT NULL,
ADD COLUMN `device_code_expires` datetime DEFAULT NULL;

+ 2
- 0
schema/schema.sql View File

@@ -16,6 +16,8 @@ CREATE TABLE `users` (
`date_created` datetime DEFAULT NULL,
`last_login` datetime DEFAULT NULL,
`enable_array_micropub` tinyint(4) NOT NULL DEFAULT '1',
`device_code` varchar(10) DEFAULT NULL,
`device_code_expires` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


+ 19
- 0
views/alexa-auth.php View File

@@ -0,0 +1,19 @@
<div class="narrow">
<?= partial('partials/header') ?>

<h2>Sign in to Teacup</h2>

<p>Go to teacup.p3k.io on your phone or computer and sign in.</p>
<p>Then go to the settings page to get a "device code", and enter that code here.</p>

<form action="/alexa/login" method="post" class="form-inline">
<div class="form-group">
<input type="number" name="code" value="" class="form-control">
</div>
<input type="submit" value="Sign In" class="btn btn-primary">
<input type="hidden" name="redirect_uri" value="<?= $this->redirect_uri ?>">
<input type="hidden" name="client_id" value="<?= $this->client_id ?>">
<input type="hidden" name="state" value="<?= $this->state ?>">
</form>

</div>

+ 3
- 3
views/partials/footer.php View File

@@ -11,7 +11,7 @@
<ul class="nav navbar-nav navbar-right">
<? if(session('me')) { ?>
<li><a href="/add-to-home?start">Add to Home Screen</a></li>
<li><span class="navbar-text"><?= preg_replace('/https?:\/\//','',session('me')) ?></span></li>
<li><a href="/settings"><?= preg_replace(['/https?:\/\//','/\/$/'],'',session('me')) ?></a></li>
<li><a href="/signout">Sign Out</a></li>
<? } else if(property_exists($this, 'authorizing')) { ?>
<li class="navbar-text"><?= $this->authorizing ?></li>
@@ -25,7 +25,7 @@
</ul>
</div>

<p class="credits">&copy; <?=date('Y')?> by <a href="http://aaronparecki.com">Aaron Parecki</a>.
<p class="credits">&copy; <?=date('Y')?> by <a href="https://aaronparecki.com">Aaron Parecki</a>.
This code is <a href="https://github.com/aaronpk/Teacup">open source</a>.
Feel free to send a pull request, or <a href="https://github.com/aaronpk/Teacup/issues">file an issue</a>.</p>
</div>
</div>

+ 8
- 0
views/privacy.php View File

@@ -0,0 +1,8 @@
<div class="narrow">
<?= partial('partials/header') ?>

<h2 id="introduction">Privacy Policy</h2>



</div>

+ 36
- 0
views/settings.php View File

@@ -0,0 +1,36 @@
<div class="narrow">

<div class="jumbotron">

<h3>Device Code</h3>
<p id="device-message">If you are prompted to log in on a device, click the button below to generate a device code.</p>

<div id="device-code">
<input type="button" class="btn btn-primary" value="Generate Device Code" id="generate-code">
</div>

</div>

</div>
<style type="text/css">
.screenshot {
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
}
</style>
<script>
$(function(){
$("#generate-code").click(function(){
$.post("/settings/device-code", {
generate: 1
}, function(response){
$("#device-code").html('<h3>'+response.code+'</h3>');
$("#device-message").html('Enter the code below on the device in order to sign in. This code is valid for 5 minutes.');
setTimeout(function(){
window.location.reload();
}, 1000*300);
});
});
})
</script>

Loading…
Cancel
Save