From 17d218c3b6ce1435d30f896d3144884261ade694 Mon Sep 17 00:00:00 2001 From: WMJ Ismail Date: Fri, 3 Jul 2026 08:49:52 +0200 Subject: [PATCH] Initial commit: Hermes AI Bridge WordPress plugin for cPanel agent runtime --- assets/admin.css | 11 ++ hermes-ai-bridge.php | 32 ++++ includes/ai-provider.php | 112 +++++++++++ includes/class-autoloader.php | 21 +++ includes/core.php | 130 +++++++++++++ includes/installer.php | 60 ++++++ includes/nextcloud-bridge.php | 145 ++++++++++++++ includes/rest-controller.php | 344 ++++++++++++++++++++++++++++++++++ includes/scheduler.php | 126 +++++++++++++ 9 files changed, 981 insertions(+) create mode 100644 assets/admin.css create mode 100644 hermes-ai-bridge.php create mode 100644 includes/ai-provider.php create mode 100644 includes/class-autoloader.php create mode 100644 includes/core.php create mode 100644 includes/installer.php create mode 100644 includes/nextcloud-bridge.php create mode 100644 includes/rest-controller.php create mode 100644 includes/scheduler.php diff --git a/assets/admin.css b/assets/admin.css new file mode 100644 index 0000000..68f4c33 --- /dev/null +++ b/assets/admin.css @@ -0,0 +1,11 @@ +/* Hermes AI Bridge Admin Styles */ +.toplevel_page_hermes-ai-bridge .wrap input[readonly] { + background: #f0f0f1; + cursor: pointer; +} +.toplevel_page_hermes-ai-bridge .wrap h2 { + margin-top: 2em; +} +.toplevel_page_hermes-ai-bridge #hermes-regenerate-key { + margin-top: 8px; +} diff --git a/hermes-ai-bridge.php b/hermes-ai-bridge.php new file mode 100644 index 0000000..8d33376 --- /dev/null +++ b/hermes-ai-bridge.php @@ -0,0 +1,32 @@ +provider = get_option('hermes_ai_provider', 'openai'); + $this->model = get_option('hermes_ai_model', 'gpt-4'); + $this->max_tokens = intval(get_option('hermes_ai_max_tokens', 2048)); + $this->api_key = get_option('hermes_ai_api_key', ''); + $this->base_url = get_option('hermes_ai_base_url', 'https://api.openai.com/v1'); + } + + /** + * Generate content from a prompt. + */ + public function generate($prompt, $context = 'general') { + $system_prompt = $this->get_system_prompt($context); + return $this->call_ai_api($system_prompt, $prompt); + } + + /** + * Call the configured AI provider API. + */ + protected function call_ai_api($system, $user_prompt) { + if (empty($this->api_key)) { + return new \WP_Error('no_api_key', 'AI provider API key not configured. Set it in WordPress Admin → Hermes AI.'); + } + + $body = [ + 'model' => $this->model, + 'max_tokens' => $this->max_tokens, + 'messages' => [ + ['role' => 'system', 'content' => $system], + ['role' => 'user', 'content' => $user_prompt], + ], + ]; + + $response = wp_remote_post($this->base_url . '/chat/completions', [ + 'timeout' => 120, + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->api_key, + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode($body), + ]); + + if (is_wp_error($response)) { + RestController::log('error', 'AI API call failed', ['error' => $response->get_error_message()]); + return $response; + } + + $status = wp_remote_retrieve_response_code($response); + $body = json_decode(wp_remote_retrieve_body($response), true); + + if ($status !== 200) { + $error_msg = $body['error']['message'] ?? 'Unknown API error'; + RestController::log('error', 'AI API error', ['status' => $status, 'error' => $error_msg]); + return new \WP_Error('ai_api_error', "AI API returned $status: $error_msg"); + } + + $content = $body['choices'][0]['message']['content'] ?? ''; + RestController::log('info', 'AI content generated', ['model' => $this->model, 'tokens' => $body['usage']['total_tokens'] ?? 0]); + + return $content; + } + + /** + * Newsletter-specific batch generation. + */ + public function generate_newsletter_content() { + $prompt = "Write a newsletter for Falah Letter — Islamic fintech and community updates. " + . "Include sections: featured article, community news, upcoming events, featured project. " + . "Write in a professional but warm tone. Target length: 1500-2000 words."; + + $content = $this->generate($prompt, 'newsletter'); + if (is_wp_error($content)) return; + + $post_id = wp_insert_post([ + 'post_title' => 'Falah Letter — ' . wp_date('F j, Y'), + 'post_content' => $content, + 'post_type' => 'post', + 'post_status' => 'draft', + 'tags_input' => ['newsletter', 'falah-letter', 'automated'], + ]); + + if ($post_id) { + RestController::log('info', 'Newsletter draft generated', ['post_id' => $post_id]); + } + } + + protected function get_system_prompt($context) { + $prompts = [ + 'post' => 'You are a content writer for FalahOS, an Islamic fintech platform. ' + . 'Write engaging, well-structured content. Use markdown for formatting. ' + . 'Keep paragraphs short and scannable.', + 'newsletter' => 'You are the editor of Falah Letter, a weekly newsletter about ' + . 'Islamic fintech, community updates, and FalahOS platform news. ' + . 'Write in a professional warm tone.', + 'social' => 'You are a social media manager for FalahOS. Create engaging posts ' + . 'optimized for LinkedIn and Twitter. Use emojis sparingly.', + ]; + return $prompts[$context] ?? $prompts['post']; + } +} diff --git a/includes/class-autoloader.php b/includes/class-autoloader.php new file mode 100644 index 0000000..c9b586a --- /dev/null +++ b/includes/class-autoloader.php @@ -0,0 +1,21 @@ +register_routes(); + } + + public static function hourly_tick() { + $scheduler = new Scheduler(); + $scheduler->process_due_tasks(); + } + + public static function daily_tick() { + $ai = new AiProvider(); + $ai->generate_newsletter_content(); + } + + public static function admin_menu() { + add_menu_page( + 'Hermes AI Bridge', + 'Hermes AI', + 'manage_options', + 'hermes-ai-bridge', + [__CLASS__, 'admin_page'], + 'dashicons-rest-api', + 80 + ); + } + + public static function admin_page() { + $api_key = get_option('hermes_api_key'); + ?> +
+

Hermes AI Bridge

+
+

API Key

+

Use this key in the X-API-Key header for REST API calls.

+ +

+ +
+

AI Provider Settings

+
+ + + + + + + + + + + + + + + + + + + + + + +
Provider + +
Model
API Key (for AI provider)
API Base URL
Max Tokens
+ +
+
+ + + $new_key]); + } +} diff --git a/includes/installer.php b/includes/installer.php new file mode 100644 index 0000000..4d78c7c --- /dev/null +++ b/includes/installer.php @@ -0,0 +1,60 @@ +get_charset_collate(); + + $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}hermes_log ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + level VARCHAR(20) NOT NULL DEFAULT 'info', + message TEXT NOT NULL, + context LONGTEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_level (level), + INDEX idx_created (created_at) + ) $charset;"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($sql); + } + + private static function set_default_options() { + if (!get_option('hermes_api_key')) { + update_option('hermes_api_key', wp_hash(wp_generate_password(64, true, true))); + } + add_option('hermes_ai_provider', 'openai'); + add_option('hermes_ai_model', 'gpt-4'); + add_option('hermes_ai_max_tokens', 2048); + add_option('hermes_cron_interval', 'hourly'); + } + + private static function schedule_cron_events() { + if (!wp_next_scheduled('hermes_hourly_tick')) { + wp_schedule_event(time(), 'hourly', 'hermes_hourly_tick'); + } + if (!wp_next_scheduled('hermes_daily_tick')) { + wp_schedule_event(time(), 'daily', 'hermes_daily_tick'); + } + } + + private static function clear_cron_events() { + wp_clear_scheduled_hook('hermes_hourly_tick'); + wp_clear_scheduled_hook('hermes_daily_tick'); + } +} diff --git a/includes/nextcloud-bridge.php b/includes/nextcloud-bridge.php new file mode 100644 index 0000000..f0a5018 --- /dev/null +++ b/includes/nextcloud-bridge.php @@ -0,0 +1,145 @@ +base_url = get_option('hermes_nextcloud_url', ''); + $this->username = get_option('hermes_nextcloud_user', ''); + $this->password = get_option('hermes_nextcloud_pass', ''); + $this->webdav_url = rtrim($this->base_url, '/') . '/remote.php/dav/files/' . $this->username . '/'; + } + + /** + * List files in Nextcloud directory. + */ + public function list_files($path = '/') { + if (!$this->is_configured()) return new \WP_Error('not_configured', 'Nextcloud not configured'); + + $url = $this->webdav_url . ltrim($path, '/'); + $response = $this->propfind($url); + + if (is_wp_error($response)) return $response; + + $files = []; + $dom = new \DOMDocument(); + $dom->loadXML($response); + + foreach ($dom->getElementsByTagNameNS('DAV:', 'response') as $node) { + $href = $node->getElementsByTagNameNS('DAV:', 'href')->item(0)->nodeValue ?? ''; + if ($href === $this->webdav_url) continue; // skip root + + $files[] = [ + 'path' => $href, + 'name' => basename($href), + 'size' => $node->getElementsByTagNameNS('DAV:', 'getcontentlength')->item(0)->nodeValue ?? 0, + 'modified' => $node->getElementsByTagNameNS('DAV:', 'getlastmodified')->item(0)->nodeValue ?? '', + 'type' => $node->getElementsByTagNameNS('DAV:', 'getcontenttype')->item(0)->nodeValue ?? 'directory', + ]; + } + + return $files; + } + + /** + * Upload a file to Nextcloud from WordPress Media Library. + */ + public function upload_from_media($attachment_id, $destination_path = '/') { + if (!$this->is_configured()) return new \WP_Error('not_configured', 'Nextcloud not configured'); + + $file_path = get_attached_file($attachment_id); + if (!$file_path || !file_exists($file_path)) { + return new \WP_Error('file_not_found', 'Attachment file not found on disk'); + } + + $filename = basename($file_path); + $dest_url = $this->webdav_url . ltrim($destination_path, '/') . '/' . $filename; + + $response = wp_remote_request($dest_url, [ + 'method' => 'PUT', + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode($this->username . ':' . $this->password), + 'Content-Type' => get_post_mime_type($attachment_id) ?: 'application/octet-stream', + ], + 'body' => file_get_contents($file_path), + 'timeout' => 60, + ]); + + if (is_wp_error($response)) return $response; + + $status = wp_remote_retrieve_response_code($response); + if ($status < 200 || $status >= 300) { + return new \WP_Error('upload_failed', "WebDAV upload returned $status"); + } + + RestController::log('info', 'File uploaded to Nextcloud', ['file' => $filename, 'dest' => $dest_url]); + return ['success' => true, 'path' => $dest_url]; + } + + /** + * Download a file from Nextcloud into WordPress Media Library. + */ + public function import_to_media($nextcloud_path) { + if (!$this->is_configured()) return new \WP_Error('not_configured', 'Nextcloud not configured'); + + $url = $this->webdav_url . ltrim($nextcloud_path, '/'); + $response = wp_remote_get($url, [ + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode($this->username . ':' . $this->password), + ], + 'timeout' => 60, + ]); + + if (is_wp_error($response)) return $response; + + $body = wp_remote_retrieve_body($response); + $filename = basename($nextcloud_path); + $upload = wp_upload_bits($filename, null, $body); + + if ($upload['error']) { + return new \WP_Error('upload_failed', $upload['error']); + } + + $attachment = [ + 'post_mime_type' => wp_check_filetype($filename)['type'] ?: 'application/octet-stream', + 'post_title' => sanitize_file_name($filename), + 'post_content' => '', + 'post_status' => 'inherit', + 'guid' => $upload['url'], + ]; + + $attach_id = wp_insert_attachment($attachment, $upload['file']); + require_once ABSPATH . 'wp-admin/includes/image.php'; + wp_generate_attachment_metadata($attach_id, $upload['file']); + + RestController::log('info', 'File imported from Nextcloud', ['file' => $filename, 'attachment_id' => $attach_id]); + return ['success' => true, 'attachment_id' => $attach_id, 'url' => $upload['url']]; + } + + protected function propfind($url) { + $response = wp_remote_request($url, [ + 'method' => 'PROPFIND', + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode($this->username . ':' . $this->password), + 'Depth' => '1', + ], + 'timeout' => 30, + ]); + + if (is_wp_error($response)) return $response; + return wp_remote_retrieve_body($response); + } + + protected function is_configured() { + return !empty($this->base_url) && !empty($this->username) && !empty($this->password); + } +} diff --git a/includes/rest-controller.php b/includes/rest-controller.php new file mode 100644 index 0000000..b235366 --- /dev/null +++ b/includes/rest-controller.php @@ -0,0 +1,344 @@ +namespace, '/health', [ + 'methods' => 'GET', + 'callback' => [$this, 'health'], + 'permission_callback' => '__return_true', + ]); + + // Content generation + register_rest_route($this->namespace, '/content/generate', [ + 'methods' => 'POST', + 'callback' => [$this, 'generate_content'], + 'permission_callback' => [$this, 'check_auth'], + 'args' => [ + 'prompt' => ['required' => true, 'type' => 'string'], + 'post_type' => ['default' => 'post', 'type' => 'string'], + 'status' => ['default' => 'draft', 'type' => 'string'], + 'title' => ['type' => 'string'], + ], + ]); + + // Content schedule + register_rest_route($this->namespace, '/content/schedule', [ + 'methods' => 'POST', + 'callback' => [$this, 'schedule_content'], + 'permission_callback' => [$this, 'check_auth'], + 'args' => [ + 'prompt' => ['required' => true, 'type' => 'string'], + 'scheduled_date' => ['required' => true, 'type' => 'string'], + 'post_type' => ['default' => 'post', 'type' => 'string'], + 'title' => ['type' => 'string'], + ], + ]); + + // Posts CRUD + register_rest_route($this->namespace, '/posts', [ + 'methods' => 'GET', + 'callback' => [$this, 'list_posts'], + 'permission_callback' => [$this, 'check_auth'], + ]); + register_rest_route($this->namespace, '/posts/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [$this, 'get_post'], + 'permission_callback' => [$this, 'check_auth'], + ]); + register_rest_route($this->namespace, '/posts/(?P\d+)', [ + 'methods' => ['PUT', 'PATCH'], + 'callback' => [$this, 'update_post'], + 'permission_callback' => [$this, 'check_auth'], + ]); + register_rest_route($this->namespace, '/posts/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [$this, 'delete_post'], + 'permission_callback' => [$this, 'check_auth'], + ]); + + // Users + register_rest_route($this->namespace, '/users', [ + 'methods' => 'GET', + 'callback' => [$this, 'list_users'], + 'permission_callback' => [$this, 'check_auth'], + ]); + register_rest_route($this->namespace, '/users/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [$this, 'get_user'], + 'permission_callback' => [$this, 'check_auth'], + ]); + + // Settings + register_rest_route($this->namespace, '/settings', [ + 'methods' => 'GET', + 'callback' => [$this, 'get_settings'], + 'permission_callback' => [$this, 'check_auth'], + ]); + register_rest_route($this->namespace, '/settings', [ + 'methods' => 'PUT', + 'callback' => [$this, 'update_settings'], + 'permission_callback' => [$this, 'check_auth'], + ]); + + // Schedules + register_rest_route($this->namespace, '/schedules', [ + 'methods' => 'GET', + 'callback' => [$this, 'list_schedules'], + 'permission_callback' => [$this, 'check_auth'], + ]); + + // Logs + register_rest_route($this->namespace, '/logs', [ + 'methods' => 'GET', + 'callback' => [$this, 'get_logs'], + 'permission_callback' => [$this, 'check_auth'], + 'args' => [ + 'level' => ['type' => 'string'], + 'limit' => ['default' => 50, 'type' => 'integer'], + 'offset' => ['default' => 0, 'type' => 'integer'], + ], + ]); + } + + public function check_auth($request) { + $key = $request->get_header('X-API-Key'); + if (!$key) { + // Also check query param for webhook compatibility + $key = $request->get_param('api_key'); + } + $stored = get_option('hermes_api_key'); + return $stored && hash_equals($stored, $key); + } + + public function health() { + return [ + 'status' => 'ok', + 'version' => HERMES_AI_BRIDGE_VERSION, + 'wordpress' => get_bloginfo('version'), + 'php' => PHP_VERSION, + 'site' => get_bloginfo('url'), + ]; + } + + public function generate_content($request) { + $prompt = $request->get_param('prompt'); + $post_type = $request->get_param('post_type'); + $status = $request->get_param('status'); + $title = $request->get_param('title'); + + $ai = new AiProvider(); + $content = $ai->generate($prompt, 'post'); + + if (is_wp_error($content)) { + return new \WP_REST_Response(['error' => $content->get_error_message()], 500); + } + + $post_id = wp_insert_post([ + 'post_title' => $title ?: wp_trim_words($content, 10, ''), + 'post_content' => $content, + 'post_type' => $post_type, + 'post_status' => $status, + ]); + + if (is_wp_error($post_id)) { + return new \WP_REST_Response(['error' => $post_id->get_error_message()], 500); + } + + self::log('info', 'Content generated', ['post_id' => $post_id, 'prompt' => substr($prompt, 0, 100)]); + + return [ + 'success' => true, + 'post_id' => $post_id, + 'edit_url' => get_edit_post_link($post_id, 'raw'), + ]; + } + + public function schedule_content($request) { + $prompt = $request->get_param('prompt'); + $scheduled_date = $request->get_param('scheduled_date'); + $post_type = $request->get_param('post_type'); + $title = $request->get_param('title'); + + $task_id = wp_insert_post([ + 'post_title' => '[AI Task] ' . ($title ?: 'Scheduled: ' . date('Y-m-d H:i')), + 'post_content' => wp_json_encode([ + 'prompt' => $prompt, + 'post_type' => $post_type, + 'scheduled_date' => $scheduled_date, + ]), + 'post_type' => 'hermes_schedule', + 'post_status' => 'publish', + 'meta_input' => [ + '_hermes_scheduled_date' => $scheduled_date, + '_hermes_task_type' => 'content_generation', + '_hermes_prompt' => $prompt, + ], + ]); + + if (is_wp_error($task_id)) { + return new \WP_REST_Response(['error' => $task_id->get_error_message()], 500); + } + + self::log('info', 'Content scheduled', ['task_id' => $task_id, 'date' => $scheduled_date]); + + return [ + 'success' => true, + 'task_id' => $task_id, + 'scheduled_date' => $scheduled_date, + ]; + } + + public function list_posts($request) { + $posts = get_posts([ + 'post_type' => 'post', + 'posts_per_page' => $request->get_param('per_page') ?: 20, + 'post_status' => 'any', + ]); + return array_map(function($p) { + return [ + 'id' => $p->ID, + 'title' => $p->post_title, + 'status' => $p->post_status, + 'date' => $p->post_date, + 'modified' => $p->post_modified, + 'permalink' => get_permalink($p->ID), + ]; + }, $posts); + } + + public function get_post($request) { + $post = get_post($request->get_param('id')); + if (!$post) return new \WP_REST_Response(['error' => 'Post not found'], 404); + return [ + 'id' => $post->ID, + 'title' => $post->post_title, + 'content' => $post->post_content, + 'excerpt' => $post->post_excerpt, + 'status' => $post->post_status, + 'date' => $post->post_date, + 'modified' => $post->post_modified, + 'permalink' => get_permalink($post->ID), + ]; + } + + public function update_post($request) { + $post_id = $request->get_param('id'); + $data = []; + if ($request->has_param('title')) $data['post_title'] = $request->get_param('title'); + if ($request->has_param('content')) $data['post_content'] = $request->get_param('content'); + if ($request->has_param('status')) $data['post_status'] = $request->get_param('status'); + if ($request->has_param('excerpt')) $data['post_excerpt'] = $request->get_param('excerpt'); + + $data['ID'] = $post_id; + $result = wp_update_post($data, true); + + if (is_wp_error($result)) { + return new \WP_REST_Response(['error' => $result->get_error_message()], 500); + } + return ['success' => true, 'post_id' => $result]; + } + + public function delete_post($request) { + $result = wp_delete_post($request->get_param('id'), true); + if (!$result) return new \WP_REST_Response(['error' => 'Delete failed'], 500); + return ['success' => true, 'deleted' => $result->ID]; + } + + public function list_users($request) { + $users = get_users(['fields' => ['ID', 'user_login', 'user_email', 'display_name', 'roles']]); + return $users; + } + + public function get_user($request) { + $user = get_userdata($request->get_param('id')); + if (!$user) return new \WP_REST_Response(['error' => 'User not found'], 404); + return [ + 'id' => $user->ID, + 'login' => $user->user_login, + 'email' => $user->user_email, + 'display_name' => $user->display_name, + 'roles' => array_values($user->roles), + ]; + } + + public function get_settings() { + return [ + 'site_name' => get_bloginfo('name'), + 'site_url' => get_bloginfo('url'), + 'admin_email' => get_bloginfo('admin_email'), + 'language' => get_bloginfo('language'), + 'timezone' => get_option('timezone_string'), + 'date_format' => get_option('date_format'), + 'time_format' => get_option('time_format'), + 'posts_per_page' => get_option('posts_per_page'), + 'ai_provider' => get_option('hermes_ai_provider'), + 'ai_model' => get_option('hermes_ai_model'), + ]; + } + + public function update_settings($request) { + $allowed = ['hermes_ai_provider', 'hermes_ai_model', 'hermes_ai_api_key', 'hermes_ai_base_url', 'hermes_ai_max_tokens', 'hermes_cron_interval']; + foreach ($request->get_params() as $key => $value) { + if (in_array($key, $allowed)) { + update_option($key, sanitize_text_field($value)); + } + } + return ['success' => true]; + } + + public function list_schedules() { + $tasks = get_posts([ + 'post_type' => 'hermes_schedule', + 'posts_per_page' => 50, + 'post_status' => 'any', + ]); + return array_map(function($t) { + return [ + 'id' => $t->ID, + 'type' => get_post_meta($t->ID, '_hermes_task_type', true), + 'scheduled_date' => get_post_meta($t->ID, '_hermes_scheduled_date', true), + 'status' => $t->post_status, + 'created' => $t->post_date, + ]; + }, $tasks); + } + + public function get_logs($request) { + global $wpdb; + $where = '1=1'; + if ($request->get_param('level')) { + $where .= $wpdb->prepare(' AND level = %s', $request->get_param('level')); + } + $limit = intval($request->get_param('limit')); + $offset = intval($request->get_param('offset')); + + $results = $wpdb->get_results( + "SELECT id, level, message, context, created_at FROM {$wpdb->prefix}hermes_log + WHERE $where ORDER BY id DESC LIMIT $offset, $limit" + ); + + return array_map(function($r) { + $r->context = $r->context ? json_decode($r->context) : null; + return $r; + }, $results); + } + + public static function log($level, $message, $context = []) { + global $wpdb; + $wpdb->insert( + $wpdb->prefix . 'hermes_log', + [ + 'level' => $level, + 'message' => $message, + 'context' => wp_json_encode($context), + 'created_at' => current_time('mysql'), + ] + ); + } +} diff --git a/includes/scheduler.php b/includes/scheduler.php new file mode 100644 index 0000000..e728508 --- /dev/null +++ b/includes/scheduler.php @@ -0,0 +1,126 @@ + 'hermes_schedule', + 'posts_per_page' => 10, + 'post_status' => 'publish', + 'meta_query' => [ + [ + 'key' => '_hermes_scheduled_date', + 'value' => current_time('mysql'), + 'compare' => '<=', + 'type' => 'DATETIME', + ], + [ + 'key' => '_hermes_processed', + 'compare' => 'NOT EXISTS', + ], + ], + ]); + + foreach ($tasks as $task) { + $this->execute_task($task); + } + } + + /** + * Execute a single scheduled task. + */ + protected function execute_task($task) { + $task_type = get_post_meta($task->ID, '_hermes_task_type', true); + + switch ($task_type) { + case 'content_generation': + $this->execute_content_task($task); + break; + case 'publish_scheduled': + $this->publish_scheduled_posts(); + break; + default: + RestController::log('warning', 'Unknown task type', ['task_id' => $task->ID, 'type' => $task_type]); + } + + update_post_meta($task->ID, '_hermes_processed', time()); + update_post_meta($task->ID, '_hermes_processed_date', current_time('mysql')); + } + + /** + * Execute a content generation task. + */ + protected function execute_content_task($task) { + $prompt = get_post_meta($task->ID, '_hermes_prompt', true); + $scheduled_date = get_post_meta($task->ID, '_hermes_scheduled_date', true); + + if (!$prompt) { + RestController::log('error', 'Task has no prompt', ['task_id' => $task->ID]); + return; + } + + $ai = new AiProvider(); + $content = $ai->generate($prompt, 'post'); + + if (is_wp_error($content)) { + RestController::log('error', 'Task generation failed', [ + 'task_id' => $task->ID, + 'error' => $content->get_error_message(), + ]); + return; + } + + $post_id = wp_insert_post([ + 'post_title' => $task->post_title, + 'post_content' => $content, + 'post_status' => 'future', + 'post_date' => $scheduled_date, + 'post_type' => 'post', + ]); + + if ($post_id) { + RestController::log('info', 'Task executed: post scheduled', [ + 'task_id' => $task->ID, + 'post_id' => $post_id, + 'scheduled' => $scheduled_date, + ]); + } + } + + /** + * Publish posts that are scheduled but stuck in 'future' status. + */ + protected function publish_scheduled_posts() { + $scheduled = get_posts([ + 'post_status' => 'future', + 'posts_per_page' => 10, + 'post_type' => 'any', + ]); + + foreach ($scheduled as $post) { + if (strtotime($post->post_date) <= time()) { + wp_publish_post($post->ID); + RestController::log('info', 'Post published', ['post_id' => $post->ID, 'title' => $post->post_title]); + } + } + } + + /** + * Register the hermes_schedule custom post type. + */ + public static function register_post_type() { + register_post_type('hermes_schedule', [ + 'labels' => ['name' => 'AI Tasks', 'singular_name' => 'AI Task'], + 'public' => false, + 'show_ui' => true, + 'show_in_menu' => false, + 'supports' => ['title', 'editor', 'custom-fields'], + ]); + } +}