v1.0.0.1
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/includes/config.php';
- require_once __DIR__ . '/includes/database.php';
- require_once __DIR__ . '/includes/auth.php';
- require_once __DIR__ . '/includes/csrf.php';
- require_once __DIR__ . '/includes/ai.php';
-
- const AI_MAX_BODY_BYTES = 600000;
-
- function aiRespond(int $status, array $payload)
- {
- http_response_code($status);
- header('Content-Type: application/json; charset=utf-8');
- header('Cache-Control: no-store');
- header('X-Content-Type-Options: nosniff');
- echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
- exit;
- }
-
- if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
- header('Allow: POST');
- aiRespond(405, ['ok' => false, 'error' => 'Something went wrong. Please refresh the page.']);
- }
-
- Auth::boot();
-
- if (!Auth::check()) {
- aiRespond(401, ['ok' => false, 'error' => 'You were logged out. Refresh the page and log in again.']);
- }
-
- if (!Csrf::verify($_SERVER['HTTP_X_CSRF_TOKEN'] ?? null)) {
- aiRespond(403, ['ok' => false, 'error' => 'Your session expired. Refresh the page and try again.']);
- }
-
- if (!Ai::availableFor(Auth::role())) {
- aiRespond(403, ['ok' => false, 'error' => 'The AI helper is not available for your account.']);
- }
-
- $declaredLength = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
- if ($declaredLength > AI_MAX_BODY_BYTES) {
- aiRespond(413, ['ok' => false, 'error' => 'That\'s too much text at once. Try a shorter message or a smaller part.']);
- }
-
- $rawBody = (string) file_get_contents('php://input', false, null, 0, AI_MAX_BODY_BYTES + 1);
- if (strlen($rawBody) > AI_MAX_BODY_BYTES) {
- aiRespond(413, ['ok' => false, 'error' => 'That\'s too much text at once. Try a shorter message or a smaller part.']);
- }
-
- $input = json_decode($rawBody, true);
- if (!is_array($input)) {
- aiRespond(400, ['ok' => false, 'error' => 'Something went wrong. Please refresh the page and try again.']);
- }
-
- $message = is_string($input['message'] ?? null) ? trim($input['message']) : '';
- if ($message === '' || mb_strlen($message) > 4000) {
- aiRespond(400, ['ok' => false, 'error' => 'Please keep your message under 4000 characters.']);
- }
-
- $rawBlocks = is_array($input['blocks'] ?? null) ? array_slice($input['blocks'], 0, 300) : [];
- $blocks = Ai::normalizeBlocks($rawBlocks);
- $fields = Ai::normalizeFields(is_array($input['fields'] ?? null) ? $input['fields'] : []);
- if ($blocks === [] && $fields === []) {
- aiRespond(400, ['ok' => false, 'error' => 'There\'s nothing written yet for me to work with.']);
- }
-
- $scope = ($input['scope'] ?? 'article') === 'block' ? 'block' : 'article';
- $focusId = is_string($input['focus_id'] ?? null) ? substr($input['focus_id'], 0, 40) : '';
- $history = is_array($input['history'] ?? null) ? array_slice($input['history'], -10) : [];
-
- $user = Auth::user();
- $userId = (int) ($user['id'] ?? 0);
-
- try {
- if (Ai::rateLimited($userId)) {
- aiRespond(429, ['ok' => false, 'error' => 'You\'ve used up this hour\'s AI messages. Try again a bit later.']);
- }
- Ai::recordRequest($userId);
- } catch (Throwable $exception) {
- aiRespond(500, ['ok' => false, 'error' => 'The AI helper is temporarily unavailable.']);
- }
-
- session_write_close();
- set_time_limit(130);
- ignore_user_abort(false);
-
- try {
- $result = Ai::chat($blocks, $message, $history, $scope, $focusId, $scope === 'block' ? [] : $fields);
- aiRespond(200, ['ok' => true] + $result);
- } catch (AiException $exception) {
- aiRespond(502, ['ok' => false, 'error' => $exception->getMessage()]);
- } catch (Throwable $exception) {
- error_log('StocketBase AI: ' . $exception->getMessage());
- aiRespond(500, ['ok' => false, 'error' => 'The AI helper failed unexpectedly.']);
- }
-