You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

257 lines
7.3KB

  1. <?php
  2. use \Firebase\JWT\JWT;
  3. $app->get('/alexa', function() use($app) {
  4. render('alexa', array(
  5. 'title' => 'Teacup for Alexa'
  6. ));
  7. });
  8. $app->get('/alexa/auth', function() use($app) {
  9. $req = $app->request();
  10. $params = $req->params();
  11. $required = ['client_id', 'response_type', 'state', 'redirect_uri'];
  12. $params_present = array_keys($params);
  13. // Validate Alexa OAuth parameters
  14. if(count(array_intersect($required, $params_present)) != count($required)) {
  15. render('auth_error', array(
  16. 'title' => 'Sign In',
  17. 'error' => 'Missing parameters',
  18. 'errorDescription' => 'One or more required parameters were missing',
  19. 'footer' => false
  20. ));
  21. return;
  22. }
  23. // Check that redirect URI is one that is allowed
  24. if(!in_array($params['redirect_uri'], Config::$alexaRedirectURIs)) {
  25. render('auth_error', array(
  26. 'title' => 'Sign In',
  27. 'error' => 'Invalid redirect URI',
  28. 'errorDescription' => 'Alexa sent an invalid redirect URI',
  29. 'footer' => false
  30. ));
  31. return;
  32. }
  33. if($params['client_id'] != Config::$alexaClientID) {
  34. render('auth_error', array(
  35. 'title' => 'Sign In',
  36. 'error' => 'Invalid Client ID',
  37. 'errorDescription' => 'Alexa sent an invalid client ID',
  38. 'footer' => false
  39. ));
  40. return;
  41. }
  42. // Pass through the OAuth parameters
  43. render('alexa-auth', [
  44. 'title' => 'Teacup for Alexa',
  45. 'client_id' => $params['client_id'],
  46. 'response_type' => $params['response_type'],
  47. 'state' => $params['state'],
  48. 'redirect_uri' => $params['redirect_uri'],
  49. 'footer' => false
  50. ]);
  51. });
  52. $app->post('/alexa/login', function() use($app) {
  53. $req = $app->request();
  54. $params = $req->params();
  55. $required = ['code', 'client_id', 'state', 'redirect_uri'];
  56. $params_present = array_keys($params);
  57. if(count(array_intersect($required, $params_present)) != count($required)) {
  58. render('auth_error', array(
  59. 'title' => 'Sign In',
  60. 'error' => 'Missing parameters',
  61. 'errorDescription' => 'One or more required parameters were missing',
  62. 'footer' => false
  63. ));
  64. return;
  65. }
  66. $user = ORM::for_table('users')
  67. ->where('device_code', $params['code'])
  68. ->where_gt('device_code_expires', date('Y-m-d H:i:s'))->find_one();
  69. if(!$user) {
  70. render('auth_error', array(
  71. 'title' => 'Sign In',
  72. 'error' => 'Invalid code',
  73. 'errorDescription' => 'The code you entered is invalid or has expired',
  74. 'footer' => false
  75. ));
  76. return;
  77. }
  78. $code = JWT::encode(array(
  79. 'user_id' => $user->id,
  80. 'iat' => time(),
  81. 'exp' => time()+300,
  82. 'client_id' => $params['client_id'],
  83. 'state' => $params['state'],
  84. 'redirect_uri' => $params['redirect_uri'],
  85. ), Config::$jwtSecret);
  86. $redirect = $params['redirect_uri'] . '?code=' . $code . '&state=' . $params['state'];
  87. $app->redirect($redirect, 302);
  88. });
  89. $app->post('/alexa/token', function() use($app) {
  90. $req = $app->request();
  91. $params = $req->params();
  92. // Alexa requests a token given a code generated above
  93. // Verify the client ID and secret
  94. if($params['client_id'] != Config::$alexaClientID
  95. || $params['client_secret'] != Config::$alexaClientSecret) {
  96. $app->response->setStatus(400);
  97. $app->response()['Content-type'] = 'application/json';
  98. $app->response()->body(json_encode([
  99. 'error' => 'forbidden',
  100. 'error_description' => 'The client ID and secret do not match'
  101. ]));
  102. return;
  103. }
  104. if(array_key_exists('code', $params)) {
  105. $jwt = $params['code'];
  106. } elseif(array_key_exists('refresh_token', $params)) {
  107. $jwt = $params['refresh_token'];
  108. } else {
  109. $app->response->setStatus(400);
  110. $app->response()['Content-type'] = 'application/json';
  111. $app->response()->body(json_encode([
  112. 'error' => 'bad_request',
  113. 'error_description' => 'Must provide either an authorization code or refresh token'
  114. ]));
  115. return;
  116. }
  117. // Validate the JWT
  118. try {
  119. $user = JWT::decode($jwt, Config::$jwtSecret, ['HS256']);
  120. } catch(Exception $e) {
  121. $app->response->setStatus(400);
  122. $app->response()['Content-type'] = 'application/json';
  123. $app->response()->body(json_encode([
  124. 'error' => 'unauthorized',
  125. 'error_description' => 'The authorization code or refresh token was invalid'
  126. ]));
  127. return;
  128. }
  129. // Generate an access token and refresh token
  130. $access_token = JWT::encode([
  131. 'user_id' => $user->user_id,
  132. 'client_id' => $user->client_id,
  133. 'iat' => time(),
  134. ], Config::$jwtSecret);
  135. $refresh_token = JWT::encode([
  136. 'user_id' => $user->user_id,
  137. 'client_id' => $user->client_id,
  138. 'iat' => time(),
  139. ], Config::$jwtSecret);
  140. $app->response()['Content-type'] = 'application/json';
  141. $app->response()->body(json_encode([
  142. 'access_token' => $access_token,
  143. 'refresh_token' => $refresh_token
  144. ]));
  145. });
  146. $app->post('/alexa/endpoint', function() use($app) {
  147. $input = file_get_contents('php://input');
  148. $json = json_decode($input, 'input');
  149. $alexaRequest = \Alexa\Request\Request::fromData($json);
  150. if($alexaRequest instanceof Alexa\Request\IntentRequest) {
  151. # Verify the access token
  152. try {
  153. $data = JWT::decode($alexaRequest->user->accessToken, Config::$jwtSecret, ['HS256']);
  154. } catch(Exception $e) {
  155. $app->response->setStatus(401);
  156. $app->response()['Content-type'] = 'application/json';
  157. $app->response()->body(json_encode([
  158. 'error' => 'unauthorized',
  159. 'error_description' => 'The access token was invalid or has expired'
  160. ]));
  161. return;
  162. }
  163. $user = ORM::for_table('users')->find_one($data->user_id);
  164. if(!$user) {
  165. $app->response->setStatus(400);
  166. return;
  167. }
  168. $action = $alexaRequest->slots['Action'];
  169. $action = ($action == 'drank' ? 'drank' : 'ate');
  170. $food = ucfirst($alexaRequest->slots['Food']);
  171. $entry = ORM::for_table('entries')->create();
  172. $entry->user_id = $user->id;
  173. $entry->type = ($action == 'drank' ? 'drink' : 'eat');
  174. $entry->content = $food;
  175. $entry->published = date('Y-m-d H:i:s');
  176. $entry->save();
  177. $text_content = 'Just ' . $action . ': ' . $food;
  178. if($user->micropub_endpoint) {
  179. $mp_request = array(
  180. 'h' => 'entry',
  181. 'published' => date('Y-m-d H:i:s'),
  182. 'summary' => $text_content
  183. );
  184. if($user->enable_array_micropub) {
  185. $mp_request[$action] = [
  186. 'type' => 'h-food',
  187. 'properties' => [
  188. 'name' => $food
  189. ]
  190. ];
  191. } else {
  192. $mp_request['p3k-food'] = $food;
  193. $mp_request['p3k-type'] = $entry->type;
  194. }
  195. $r = micropub_post($user->micropub_endpoint, $mp_request, $user->access_token);
  196. $request = $r['request'];
  197. $response = $r['response'];
  198. $entry->micropub_response = $response;
  199. if($response && preg_match('/Location: (.+)/', $response, $match)) {
  200. $url = $match[1];
  201. $entry->micropub_success = 1;
  202. $entry->canonical_url = $url;
  203. } else {
  204. $entry->micropub_success = 0;
  205. $url = Config::$base_url . $user->url . '/' . $entry->id;
  206. }
  207. $entry->save();
  208. }
  209. $response = new \Alexa\Response\Response;
  210. $response->respond('Got it!')
  211. ->withCard('You '.$action.': '.$food, $url)
  212. ->endSession(true);
  213. $app->response()['Content-type'] = 'application/json';
  214. $app->response()->body(json_encode($response->render()));
  215. }
  216. });