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.3 KiB

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