settings->get( 'support_webhook_url', '' ) ); if ( '' !== $webhook ) { $sent = self::post_webhook( $webhook, $row ); if ( is_wp_error( $sent ) ) { $problems[] = sprintf( /* translators: %s: error message returned by the webhook */ __( 'The webhook could not be called: %s', 'aihouse' ), $sent->get_error_message() ); } } if ( mybot()->settings->get( 'support_notify_email', 0 ) ) { $sent = self::send_email( $row ); if ( is_wp_error( $sent ) ) { $problems[] = $sent->get_error_message(); } } /* * Other channels — an add-on registers a callable and this runs it. * * A channel is a name, a label and a callable that receives the saved request and returns true or a * WP_Error. Without this seam every new channel (SMS, Zalo ZNS, a CRM) would be another `if` block * inside this method, which means the core would contain the paid code — the first invariant of * docs/13 muc 1. With it, the core knows how to deliver to a channel and the add-on knows what the * channel is. * * Errors are collected the same way as the two built-in channels: one failing channel never stops * the others, and whatever went wrong is recorded on the request itself (`record_problem`) so the * administrator sees it on the requests screen rather than guessing. */ $channels = apply_filters( 'mybot_register_channels', array() ); $channels = is_array( $channels ) ? $channels : array(); foreach ( $channels as $channel_id => $channel ) { $callable = is_array( $channel ) && isset( $channel['send'] ) ? $channel['send'] : $channel; if ( ! is_callable( $callable ) ) { continue; } $sent = call_user_func( $callable, $row ); if ( is_wp_error( $sent ) ) { $problems[] = sprintf( /* translators: 1: channel id, 2: error message */ __( 'The "%1$s" channel could not deliver this request: %2$s', 'aihouse' ), sanitize_key( (string) $channel_id ), $sent->get_error_message() ); } } if ( empty( $problems ) ) { MYBOT_Requests::mark_notified( (int) $id ); return; } self::record_problem( (int) $id, implode( ' ', $problems ) ); } /** * Loads one request. * * @param int $id Request ID. * @return array|null */ protected static function row( $id ) { return MYBOT_Requests::find( $id ); } /** * Posts the request to a webhook, in the shape an automation tool expects. * * @param string $url Webhook URL. * @param array $row Request row. * @return true|WP_Error */ protected static function post_webhook( $url, $row ) { $payload = array( 'id' => (int) $row['id'], 'created_at' => (string) $row['created_at'], 'name' => (string) $row['name'], 'email' => (string) $row['email'], 'phone' => (string) $row['phone'], 'question' => (string) $row['question'], 'bot_answer' => (string) $row['answer'], 'note' => (string) $row['note'], 'lang' => (string) $row['lang'], 'session_id' => (string) $row['session_id'], 'site' => home_url(), 'site_name' => get_bloginfo( 'name' ), 'admin_url' => admin_url( 'admin.php?page=mybot&view=settings#mybot-support-requests' ), 'source' => 'mybot', ); $response = MYBOT_HTTP::post_json( $url, $payload, array(), self::TIMEOUT, 1 ); return is_wp_error( $response ) ? $response : true; } /** * Emails the administrator. * * @param array $row Request row. * @return true|WP_Error */ protected static function send_email( $row ) { $to = self::recipients(); if ( empty( $to ) ) { return new WP_Error( 'mybot_support_no_recipient', __( 'No notification address is configured, so no email was sent.', 'aihouse' ) ); } $subject = sprintf( /* translators: 1: site name, 2: visitor name or email */ __( '[%1$s] A question is waiting for a human: %2$s', 'aihouse' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), '' !== (string) $row['name'] ? (string) $row['name'] : (string) $row['email'] ); $lines = array( sprintf( /* translators: %s: question the visitor asked */ __( 'Question: %s', 'aihouse' ), (string) $row['question'] ), '', sprintf( /* translators: %s: the reply the bot gave */ __( 'The bot answered: %s', 'aihouse' ), '' !== (string) $row['answer'] ? (string) $row['answer'] : __( '(nothing)', 'aihouse' ) ), ); if ( '' !== (string) $row['note'] ) { $lines[] = ''; $lines[] = sprintf( /* translators: %s: extra information the visitor typed */ __( 'Extra information: %s', 'aihouse' ), (string) $row['note'] ); } $lines[] = ''; $lines[] = sprintf( /* translators: 1: name, 2: email, 3: phone */ __( 'Name: %1$s · Email: %2$s · Phone: %3$s', 'aihouse' ), '' !== (string) $row['name'] ? (string) $row['name'] : '—', (string) $row['email'], '' !== (string) $row['phone'] ? (string) $row['phone'] : '—' ); $lines[] = sprintf( /* translators: %s: URL of the settings screen */ __( 'Open the request list: %s', 'aihouse' ), admin_url( 'admin.php?page=mybot&view=settings#mybot-support-requests' ) ); $sent = wp_mail( $to, $subject, implode( "\n", $lines ) ); if ( ! $sent ) { return new WP_Error( 'mybot_support_mail', __( 'WordPress could not send the notification email. On shared hosting this is common: either set up an SMTP plugin, or use the webhook instead.', 'aihouse' ) ); } return true; } /** * Addresses the notification goes to. * * @return array */ protected static function recipients() { $configured = trim( (string) mybot()->settings->get( 'support_notify_to', '' ) ); $to = array(); if ( '' !== $configured ) { foreach ( preg_split( '/[,;\s]+/', $configured ) as $candidate ) { $candidate = sanitize_email( $candidate ); if ( '' !== $candidate && is_email( $candidate ) ) { $to[] = $candidate; } } } if ( empty( $to ) ) { $fallback = sanitize_email( (string) get_option( 'admin_email' ) ); if ( '' !== $fallback ) { $to[] = $fallback; } } /** Filters the addresses a support request notification is sent to. */ return (array) apply_filters( 'mybot_support_notify_recipients', $to ); } /** * Records a notification problem where the administrator will see it. * * @param int $id Request ID. * @param string $message Problem description. * @return void */ protected static function record_problem( $id, $message ) { global $wpdb; $table = MYBOT_Requests::table(); // The message goes in its own column. Writing it into `note` would overwrite what the // visitor typed — losing their words to store our error is a bad trade. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->update( $table, array( 'notified' => 0, 'notify_error' => $message, 'updated_at' => current_time( 'mysql' ), ), array( 'id' => (int) $id ), array( '%d', '%s', '%s' ), array( '%d' ) ); } }