`) using the shared * secret configured in the plugin settings (and pasted into the chatbot's * "Post-actions" on app.agentsia.fr). * * On a valid, signed request it fires the `agentsia_chatbot_conversation_ended` * action so any plugin/theme can react (CRM, email, automation, etc.). */ final class Webhook { const REST_NAMESPACE = 'agentsia/v1'; const REST_ROUTE = '/chatbot-ended'; private static ?self $instance = null; public static function instance(): self { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } public function register(): void { add_action( 'rest_api_init', [ $this, 'register_routes' ] ); } public static function endpoint_url(): string { return rest_url( self::REST_NAMESPACE . self::REST_ROUTE ); } public function register_routes(): void { register_rest_route( self::REST_NAMESPACE, self::REST_ROUTE, [ 'methods' => 'POST', 'callback' => [ $this, 'handle' ], 'permission_callback' => [ $this, 'verify_signature' ], ] ); } /** * Permission callback — the request is accepted only when the webhook is * enabled and the body carries a valid HMAC-SHA256 signature. * * @return true|\WP_Error */ public function verify_signature( \WP_REST_Request $request ) { $settings = Plugin::get_settings(); if ( empty( $settings['webhook_enabled'] ) ) { return new \WP_Error( 'agentsia_webhook_disabled', __( 'Webhook désactivé.', 'agentsia-agents' ), [ 'status' => 404 ] ); } $secret = (string) ( $settings['webhook_secret'] ?? '' ); if ( $secret === '' ) { return new \WP_Error( 'agentsia_webhook_no_secret', __( 'Aucun secret configuré.', 'agentsia-agents' ), [ 'status' => 403 ] ); } $provided = trim( (string) $request->get_header( 'X-Agentsia-Signature' ) ); if ( $provided === '' ) { return new \WP_Error( 'agentsia_webhook_no_signature', __( 'Signature manquante.', 'agentsia-agents' ), [ 'status' => 401 ] ); } // Accept both "sha256=" and a bare "". $provided = (string) preg_replace( '/^sha256=/i', '', $provided ); $expected = hash_hmac( 'sha256', (string) $request->get_body(), $secret ); if ( ! hash_equals( $expected, $provided ) ) { return new \WP_Error( 'agentsia_webhook_bad_signature', __( 'Signature invalide.', 'agentsia-agents' ), [ 'status' => 401 ] ); } return true; } public function handle( \WP_REST_Request $request ): \WP_REST_Response { $payload = $request->get_json_params(); if ( ! is_array( $payload ) ) { $payload = []; } /** * Fires when a chatbot conversation has ended and the signed webhook from * the Agentsia backend was received and verified. * * @param array $payload Conversation analysis: outcome, sentiment, * summary, keyPoints, actionItems, * followUpRequired, collectedData, date… * @param \WP_REST_Request $request The full REST request. */ do_action( 'agentsia_chatbot_conversation_ended', $payload, $request ); return new \WP_REST_Response( [ 'received' => true ], 200 ); } /** * Render the "Webhook de fin de conversation" settings section. Called from * the settings page, inside the main options form. * * @param array $settings Current plugin settings. */ public function render_settings_fields( array $settings ): void { $enabled = ! empty( $settings['webhook_enabled'] ); $secret = (string) ( $settings['webhook_secret'] ?? '' ); $opt = Plugin::OPTION_KEY; ?>