[$this, 'turn'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/preview', [ 'callback' => [$this, 'preview'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/run', [ 'callback' => [$this, 'run'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/stage', [ 'callback' => [$this, 'stage'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/stage/apply', [ 'callback' => [$this, 'applyStage'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/stage/clear', [ 'callback' => [$this, 'clearStage'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/stage/schedule', [ 'callback' => [$this, 'scheduleStage'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/stage/request', [ 'callback' => [$this, 'requestStage'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/stage/review', [ 'callback' => [$this, 'reviewStage'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/stage/view', [ 'callback' => [$this, 'viewStage'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/requests', [ 'callback' => [$this, 'requests'], 'methods' => WP_REST_Server::READABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/requests/(?P[a-f0-9]{16})/(?Papply|decline|withdraw)', [ 'callback' => [$this, 'decideRequest'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/scheduled', [ 'callback' => [$this, 'scheduled'], 'methods' => WP_REST_Server::READABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/scheduled/(?P[a-f0-9]{12})', [ 'callback' => [$this, 'cancelScheduled'], 'methods' => WP_REST_Server::DELETABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/undo', [ 'callback' => [$this, 'undo'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/changes', [ 'callback' => [$this, 'changes'], 'methods' => WP_REST_Server::READABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ]); register_rest_route(self::NAMESPACE, '/assistant/conversations', [ [ 'callback' => [$this, 'conversations'], 'methods' => WP_REST_Server::READABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ], [ 'callback' => [$this, 'saveConversation'], 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ], ]); register_rest_route(self::NAMESPACE, '/assistant/conversations/(?P\d+)', [ [ 'callback' => [$this, 'conversation'], 'methods' => WP_REST_Server::READABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ], [ 'callback' => [$this, 'deleteConversation'], 'methods' => WP_REST_Server::DELETABLE, 'permission_callback' => [self::class, 'canUseAssistant'], ], ]); } public static function canUseAssistant(): bool { return is_user_logged_in() && current_user_can(ReviewPage::CAPABILITY); } public function turn(WP_REST_Request $request): WP_REST_Response { self::activateStage($request); $body = $request->get_json_params(); $messages = is_array($body) && is_array($body['messages'] ?? null) ? $body['messages'] : []; $summary = is_array($body) && is_string($body['summary'] ?? null) ? $body['summary'] : ''; $result = Client::turn(get_current_user_id(), $messages, $summary); return new WP_REST_Response($result, isset($result['error']) ? 502 : 200); } public function preview(WP_REST_Request $request): WP_REST_Response { self::activateStage($request); [$ability, $args] = self::call($request); return new WP_REST_Response(Catalog::preview($ability, $args)); } /** A real write, or, with `stage: true`, one laid over the chat's preview. */ public function run(WP_REST_Request $request): WP_REST_Response { self::activateStage($request); [$ability, $args] = self::call($request); $body = $request->get_json_params(); if (is_array($body) && ($body['stage'] ?? false) === true) { $id = is_string($body['id'] ?? null) ? sanitize_text_field($body['id']) : ''; $staged = Stage::add($id, $ability, $args); if (!$staged['ok']) { return new WP_REST_Response($staged); } $output = ['staged' => true, 'summary' => (string) ($staged['summary'] ?? '')]; if (isset($staged['id'])) { $output['id'] = $staged['id']; } return new WP_REST_Response(['ok' => true, 'output' => $output, 'staged' => true] + $staged); } $consent = is_array($body) && ($body['consent'] ?? false) === true; $result = Catalog::run($ability, $args, $consent); if ($result['ok'] && isset($result['output'])) { $result['output'] = Transcript::capOutput($result['output']); } return new WP_REST_Response($result); } /** The browser's staged list replaces the stored one (a reload, or a chat picked up again). */ public function stage(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); $token = is_array($body) && is_string($body['token'] ?? null) ? sanitize_text_field($body['token']) : ''; $changes = is_array($body) && is_array($body['changes'] ?? null) ? $body['changes'] : []; $result = Stage::sync($token, $changes); if ($result['ok']) { $result['previewUrl'] = home_url('/'); } return new WP_REST_Response($result, $result['ok'] ? 200 : 400); } public function applyStage(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); $token = is_array($body) && is_string($body['token'] ?? null) ? sanitize_text_field($body['token']) : ''; if (!Stage::activate($token)) { return new WP_REST_Response(['error' => __('That preview has ended. Reload the page and try again.', 'sarcio'), 'ok' => false], 404); } return new WP_REST_Response(Stage::apply()); } public function scheduleStage(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); $token = is_array($body) && is_string($body['token'] ?? null) ? sanitize_text_field($body['token']) : ''; if (!Stage::activate($token)) { return new WP_REST_Response(['error' => __('That preview has ended. Reload the page and try again.', 'sarcio'), 'ok' => false], 404); } $summaries = []; foreach (is_array($body['summaries'] ?? null) ? $body['summaries'] : [] as $id => $summary) { if (is_string($id) && is_string($summary)) { $summaries[$id] = $summary; } } $result = Scheduled::fromStage( is_string($body['applyAt'] ?? null) ? sanitize_text_field($body['applyAt']) : '', is_string($body['revertAt'] ?? null) ? sanitize_text_field($body['revertAt']) : null, $summaries, is_string($body['repeat'] ?? null) ? sanitize_key($body['repeat']) : 'none', ); return new WP_REST_Response($result, $result['ok'] ? 200 : 400); } public function reviewStage(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); $token = is_array($body) && is_string($body['token'] ?? null) ? sanitize_text_field($body['token']) : ''; if (!Stage::activate($token)) { return new WP_REST_Response(['error' => __('That preview has ended. Reload the page and try again.', 'sarcio'), 'ok' => false], 404); } $result = Review::create(); return new WP_REST_Response($result, $result['ok'] ? 200 : 400); } public function requestStage(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); $token = is_array($body) && is_string($body['token'] ?? null) ? sanitize_text_field($body['token']) : ''; if (!Stage::activate($token)) { return new WP_REST_Response(['error' => __('That preview has ended. Reload the page and try again.', 'sarcio'), 'ok' => false], 404); } $previews = []; foreach (is_array($body['previews'] ?? null) ? $body['previews'] : [] as $id => $preview) { if (is_string($id) && is_array($preview)) { $previews[$id] = $preview; } } $result = Requests::fromStage(is_string($body['title'] ?? null) ? $body['title'] : '', $previews); return new WP_REST_Response($result, $result['ok'] ? 200 : 400); } public function requests(WP_REST_Request $request): WP_REST_Response { return new WP_REST_Response(['canApply' => Requests::canApply(), 'requests' => Requests::list()]); } public function decideRequest(WP_REST_Request $request): WP_REST_Response { $id = sanitize_text_field((string) $request->get_param('id')); $body = $request->get_json_params(); $result = match ((string) $request->get_param('action')) { 'apply' => Requests::apply($id), 'decline' => Requests::decline($id, is_array($body) && is_string($body['note'] ?? null) ? $body['note'] : ''), default => Requests::withdraw($id), }; return new WP_REST_Response($result, $result['ok'] ? 200 : 400); } public function scheduled(WP_REST_Request $request): WP_REST_Response { return new WP_REST_Response(['scheduled' => Scheduled::list()]); } public function cancelScheduled(WP_REST_Request $request): WP_REST_Response { $result = Scheduled::cancel(sanitize_text_field((string) $request->get_param('id'))); return new WP_REST_Response($result, $result['ok'] ? 200 : 400); } public function viewStage(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); $token = is_array($body) && is_string($body['token'] ?? null) ? sanitize_text_field($body['token']) : ''; $view = is_array($body) && ($body['view'] ?? '') === 'live' ? 'live' : 'staged'; if (!Stage::activate($token)) { return new WP_REST_Response(['error' => __('That preview has ended. Reload the page and try again.', 'sarcio'), 'ok' => false], 404); } Stage::setView($view === 'live'); return new WP_REST_Response(['ok' => true, 'view' => $view]); } /** Also reached by a beacon when the chat's tab closes, so the token may come as a query parameter. */ public function clearStage(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); $token = is_array($body) && is_string($body['token'] ?? null) ? $body['token'] : (string) ($request->get_param('token') ?? ''); Stage::clear(sanitize_text_field($token)); return new WP_REST_Response(['ok' => true]); } public function changes(WP_REST_Request $request): WP_REST_Response { return new WP_REST_Response(['changes' => JournalAbilities::list()]); } public function undo(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); $id = is_array($body) ? (int) ($body['id'] ?? 0) : 0; return new WP_REST_Response(Catalog::undo($id)); } public function conversations(WP_REST_Request $request): WP_REST_Response { return new WP_REST_Response(['conversations' => Conversations::listFor(get_current_user_id())]); } public function conversation(WP_REST_Request $request): WP_REST_Response { $found = Conversations::get((int) $request->get_param('id'), get_current_user_id()); if ($found === null) { return new WP_REST_Response(['error' => __('That conversation is no longer here.', 'sarcio'), 'ok' => false], 404); } return new WP_REST_Response(['conversation' => $found, 'ok' => true]); } public function saveConversation(WP_REST_Request $request): WP_REST_Response { $body = $request->get_json_params(); if (!is_array($body) || !is_array($body['messages'] ?? null)) { return new WP_REST_Response(['error' => __('Nothing to save.', 'sarcio'), 'ok' => false], 400); } $saved = Conversations::save( (int) ($body['id'] ?? 0), get_current_user_id(), is_string($body['title'] ?? null) ? $body['title'] : '', is_string($body['summary'] ?? null) ? $body['summary'] : '', $body['messages'], is_array($body['previews'] ?? null) ? $body['previews'] : [], ); if ($saved === null) { return new WP_REST_Response(['error' => __('That conversation is no longer here.', 'sarcio'), 'ok' => false], 404); } return new WP_REST_Response(['ok' => true] + $saved); } public function deleteConversation(WP_REST_Request $request): WP_REST_Response { return new WP_REST_Response(['ok' => Conversations::delete((int) $request->get_param('id'), get_current_user_id())]); } private static function activateStage(WP_REST_Request $request): void { $token = $request->get_header(Stage::HEADER); if (is_string($token) && $token !== '') { Stage::activate(sanitize_text_field($token)); } } /** @return array{0:string,1:array} */ private static function call(WP_REST_Request $request): array { $body = $request->get_json_params(); $ability = is_array($body) && is_string($body['ability'] ?? null) ? sanitize_text_field($body['ability']) : ''; $args = is_array($body) && is_array($body['args'] ?? null) ? $body['args'] : []; return [$ability, $args]; } }