get_charset_collate(); $prefix = $wpdb->prefix; $schemas = array(); $schemas[] = "CREATE TABLE {$prefix}mybot_messages ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL, content LONGTEXT NOT NULL, attachments LONGTEXT NOT NULL, lang VARCHAR(8) NOT NULL DEFAULT '', mode VARCHAR(16) NOT NULL DEFAULT 'quick', provider VARCHAR(32) NOT NULL DEFAULT '', model VARCHAR(64) NOT NULL DEFAULT '', latency_ms INT UNSIGNED NOT NULL DEFAULT 0, tokens_in INT UNSIGNED NOT NULL DEFAULT 0, tokens_out INT UNSIGNED NOT NULL DEFAULT 0, cache_hit TINYINT(1) NOT NULL DEFAULT 0, rating TINYINT(1) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY session_created (session_id, created_at), KEY created_at (created_at), KEY lang (lang), KEY rating (rating) ) {$charset};"; $schemas[] = "CREATE TABLE {$prefix}mybot_ratelimit ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, bucket_key VARCHAR(64) NOT NULL, window_start BIGINT(20) UNSIGNED NOT NULL, hits INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY bucket_window (bucket_key, window_start), KEY window_start (window_start) ) {$charset};"; $schemas[] = "CREATE TABLE {$prefix}mybot_queue ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, job_type VARCHAR(48) NOT NULL, payload LONGTEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'pending', attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0, last_error TEXT NULL, available_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY status_available (status, available_at), KEY job_type (job_type) ) {$charset};"; $schemas[] = "CREATE TABLE {$prefix}mybot_cache ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, cache_key CHAR(64) NOT NULL, cache_value LONGTEXT NOT NULL, expires_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY cache_key (cache_key), KEY expires_at (expires_at) ) {$charset};"; /* * Questions the chatbot could not answer, waiting for a human. * * `question` and `answer` are copied here instead of being looked up in the message log: * the log is purged after the retention period, and a lead that disappears because its * conversation aged out is worse than a duplicated few hundred bytes. */ $schemas[] = "CREATE TABLE {$prefix}mybot_requests ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL DEFAULT '', question LONGTEXT NOT NULL, answer LONGTEXT NOT NULL, name VARCHAR(120) NOT NULL DEFAULT '', email VARCHAR(190) NOT NULL DEFAULT '', phone VARCHAR(40) NOT NULL DEFAULT '', note TEXT NOT NULL, lang VARCHAR(8) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'new', notified TINYINT(1) NOT NULL DEFAULT 0, notify_error TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY status_created (status, created_at), KEY created_at (created_at), KEY session_id (session_id) ) {$charset};"; /* * The visitor's own card (1.17.0). * * ⚠️ **A separate table, NOT a column on `mybot_requests`, and the reason is two different life * cycles:** a request is *one question that needs a human* — it is born, it is answered, it is * closed; a contact is *a person* who may chat on ten different days and leave their number once. * Putting the card on the request would mean the same person exists as many times as they asked * something, and "how many customers do I have" would be unanswerable. That is the same rule * docs/16 muc 3 already applies to `agent` and `role`: **one meaning, one place**. * * **What makes this table different from every other one in the plugin: it holds personal data on * purpose.** So it also carries the two things a lawful collection needs and a spreadsheet column * does not: **when the visitor agreed** (`consent_at`) and **the exact sentence they agreed to** * (`consent_text`) — Nghị định 13/2023 asks the site to *prove* consent, and a boolean proves * nothing on its own. `channel` records which of phone/email they gave, so a screen can say "call * this one" without guessing. */ $schemas[] = "CREATE TABLE {$prefix}mybot_contacts ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL DEFAULT '', name VARCHAR(120) NOT NULL DEFAULT '', email VARCHAR(190) NOT NULL DEFAULT '', phone VARCHAR(40) NOT NULL DEFAULT '', channel VARCHAR(8) NOT NULL DEFAULT '', note TEXT NOT NULL, consent TINYINT(1) NOT NULL DEFAULT 0, consent_text VARCHAR(255) NOT NULL DEFAULT '', consent_at DATETIME NULL DEFAULT NULL, source_url VARCHAR(255) NOT NULL DEFAULT '', lang VARCHAR(8) NOT NULL DEFAULT '', submissions INT UNSIGNED NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY session_id (session_id), KEY email (email), KEY phone (phone), KEY created_at (created_at) ) {$charset};"; foreach ( $schemas as $sql ) { dbDelta( $sql ); } } /** * Writes the default options when they do not exist yet. * * @return void */ protected static function seed_options() { if ( null === get_option( MYBOT_OPTION_KEY, null ) ) { add_option( MYBOT_OPTION_KEY, MYBOT_Settings::defaults() ); } } /** * Schedules the background jobs. * * @return void */ protected static function schedule_events() { if ( ! wp_next_scheduled( MYBOT_Queue::CRON_HOOK ) ) { wp_schedule_event( time() + 60, 'mybot_minute', MYBOT_Queue::CRON_HOOK ); } if ( ! wp_next_scheduled( 'mybot_daily_maintenance' ) ) { wp_schedule_event( time() + 3600, 'daily', 'mybot_daily_maintenance' ); } } /** * Runs the one-time content default upgrade. * * Kept apart from the schema upgrade so it also runs on installs whose database version * did not change — a plugin ZIP replaced over an existing install never re-activates. * * @return void */ public static function maybe_upgrade_content_defaults() { if ( ! class_exists( 'MYBOT_Settings' ) ) { return; } MYBOT_Settings::upgrade_content_defaults(); } /** * Upgrades the schema when the database version changes. * * dbDelta only adds missing columns and indexes and never deletes data, so re-running it is safe. * * @return void */ public static function maybe_upgrade() { if ( MYBOT_DB_VERSION === get_option( MYBOT_DB_VERSION_OPTION ) ) { return; } self::create_tables(); self::seed_options(); self::schedule_events(); update_option( MYBOT_DB_VERSION_OPTION, MYBOT_DB_VERSION, false ); } /** * Registers the every-minute schedule. * * Registered on the front end as well, because WP-Cron on shared hosting only runs when a request arrives. * * @param array $schedules Existing schedules. * @return array */ public static function add_cron_schedule( $schedules ) { if ( ! isset( $schedules['mybot_minute'] ) ) { $schedules['mybot_minute'] = array( 'interval' => 60, 'display' => __( 'Every minute (AI House)', 'aihouse' ), ); } return $schedules; } }