bind_anti_eav_hooks(); } /** Adapter slug (matches wp.org plugin slug). */ public function plugin_slug(): string { return 'hivepress-messages'; } /** Class probed for addon presence. */ public function detection_class(): string { return 'HivePress\\Messages\\Plugin'; } /** Version constant probed for addon presence. */ public function detection_const(): string { return 'HIVEPRESS_MESSAGES_VERSION'; } /** Minimum addon version supported. */ public function minimum_addon_version(): string { return '1.4.0'; } /** * Declare the shadow table to the Custom_Table_Registry. * * @param TMDO_Custom_Table_Registry $registry Registry singleton. */ public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { $this->register_custom_table( $registry, array( 'table_name' => self::TABLE, 'primary_key' => 'comment_id', 'post_type_link' => null, 'expected_columns' => array( 'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY', 'sender_id' => 'BIGINT UNSIGNED NOT NULL', 'recipient_id' => 'BIGINT UNSIGNED NOT NULL', 'listing_id' => 'BIGINT UNSIGNED NOT NULL', 'is_read' => 'TINYINT(1) NOT NULL DEFAULT 0', 'sent_at' => 'DATETIME NOT NULL', ), 'indexes' => array( 'idx_recipient_unread' => '(recipient_id, is_read)', 'idx_thread' => '(listing_id, sent_at)', ), 'doctor_callback' => array( $this, 'doctor_check' ), ) ); } /** * Bind sync hooks on hp_message comment insert / update / delete. * * The recipient_id is stored in comment_karma by HivePress; we mirror * it into a properly-named indexed column. is_read tracks comment_approved * (HP uses 0=unread / 1=read) — same column, mirrored. */ public function on_register_event_hooks(): void { add_action( 'wp_insert_comment', array( $this, 'mirror_insert' ), 10, 2 ); add_action( 'wp_set_comment_status', array( $this, 'mirror_status_change' ), 10, 2 ); add_action( 'deleted_comment', array( $this, 'mirror_delete' ), 10, 2 ); } /** * Mirror a new hp_message into the shadow table. * * @param int $comment_id Comment ID. * @param object|\WP_Comment $comment Comment object. */ public function mirror_insert( int $comment_id, $comment ): void { if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { return; } global $wpdb; $sender_id = (int) ( $comment->user_id ?? 0 ); $recipient_id = (int) ( $comment->comment_karma ?? 0 ); $listing_id = (int) ( $comment->comment_post_ID ?? 0 ); $is_read = (int) ( $comment->comment_approved ?? 0 ); if ( $sender_id <= 0 || $recipient_id <= 0 ) { // Malformed message — nothing useful to mirror. return; } $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->prepare( 'INSERT INTO `' . $wpdb->prefix . self::TABLE . '` (comment_id, sender_id, recipient_id, listing_id, is_read, sent_at) VALUES (%d, %d, %d, %d, %d, %s) ON DUPLICATE KEY UPDATE is_read = VALUES(is_read)', $comment_id, $sender_id, $recipient_id, $listing_id, $is_read, $comment->comment_date ?? current_time( 'mysql' ) ) ); } /** * Update is_read when a message is marked read (comment_approved flip). * * @param int $comment_id Comment ID. * @param string $status New status ('approve', 'hold', 'spam', 'trash'). */ public function mirror_status_change( int $comment_id, string $status ): void { global $wpdb; // Look up the comment to confirm it's hp_message before touching. if ( ! function_exists( 'get_comment' ) ) { return; } $comment = get_comment( $comment_id ); if ( ! $comment || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { return; } $is_read = ( 'approve' === $status ) ? 1 : 0; $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prefix . self::TABLE, array( 'is_read' => $is_read ), array( 'comment_id' => $comment_id ), array( '%d' ), array( '%d' ) ); } /** * Remove shadow row on hp_message deletion. * * @param int $comment_id Comment ID. * @param object|\WP_Comment $comment Comment object. */ public function mirror_delete( int $comment_id, $comment ): void { if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { return; } global $wpdb; $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prefix . self::TABLE, array( 'comment_id' => $comment_id ), array( '%d' ) ); } /** * Doctor probe — verify shadow table. * * @return array{ok:bool, message:string, details?:array} */ public function doctor_check(): array { global $wpdb; if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { return array( 'ok' => false, 'message' => 'wpdb global not available', ); } $table = $wpdb->prefix . self::TABLE; try { $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); if ( ! $exists ) { return array( 'ok' => false, 'message' => sprintf( 'hivepress-messages: shadow table %s missing', $table ), 'details' => array( 'table' => $table ), ); } $rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared return array( 'ok' => true, 'message' => sprintf( 'hivepress-messages: %s rows=%d', $table, $rows ), 'details' => array( 'table' => $table, 'rows' => $rows, ), ); } catch ( \Throwable $e ) { return array( 'ok' => false, 'message' => sprintf( 'hivepress-messages doctor failed: %s', $e->getMessage() ), ); } } /** * 8-D self-score. * * D1: 1.0 — adapter never calls *_meta() * D2: 1.0 — uses wpdb prefix exclusively * D3: 1.0 — shadow table covers recipient/unread access * D4: 1.0 — expected_columns + indexes registered * D5: 1.0 — listens to wp_insert_comment + status change for sync * D6: 1.0 — zero options/transients * D7: 1.0 — only queries own shadow table * D8: 1.0 — idx_recipient_unread covers the hot dashboard query */ public function suitability_score(): array { return $this->compose_score( array() ); } /** * FSM modules this adapter contributes. * * @return array */ public function migrations(): array { return array( 'comment_hp_message' ); } } } // end if ( ! class_exists )