self::tell_message( (int) $lead_id, (int) $message_id ) ); } /** * Sends the notice about a message. * * @param int $lead_id Lead ID. * @param int $message_id Message ID. */ private static function tell_message( $lead_id, $message_id ) { $lead = Leads::get( $lead_id ); $message = Messages::get( $message_id ); if ( ! $lead || ! $message || (int) $message->lead_id !== (int) $lead->id ) { return; } $name = '' !== trim( (string) $lead->name ) ? (string) $lead->name : (string) $lead->email; $url = admin_url( 'admin.php?page=dealroot&lead=' . (int) $lead->id ); /* translators: %s: name of the person */ $title = sprintf( __( '%s wrote again', 'dealroot' ), $name ); $text = wp_html_excerpt( (string) $message->body, 600, '…' ); if ( self::telegram_ready() ) { self::send_telegram( Settings::get( 'telegram_token' ), Settings::get( 'telegram_chat_id' ), '✉️ ' . esc_html( $title ) . '' . "\n\n" . esc_html( $text ) . "\n\n" . '' . esc_html__( 'Open the lead', 'dealroot' ) . '' ); } if ( Settings::get( 'notify_new' ) ) { Mail::owner( Digest::recipients(), /* translators: 1: name of the person, 2: site name */ sprintf( __( '%1$s wrote again – %2$s', 'dealroot' ), $name, wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ), array( 'title' => $title, 'text' => $text, 'button' => array( 'url' => $url, 'label' => __( 'Open the lead', 'dealroot' ), ), 'note' => __( 'You get this email because "Email me each new enquiry" is on in the settings of Dealroot.', 'dealroot' ), ) ); } } /** * Queues the notification so the form response is not delayed by the Telegram API. * Leads entered by hand are not announced. * * @param int $lead_id Lead ID. * @param array $data Stored values. */ public function schedule( $lead_id, $data = array() ) { if ( ! ( self::telegram_ready() || Settings::get( 'notify_new' ) ) || 'manual' === ( $data['form_plugin'] ?? '' ) ) { return; } // With the AI assistant, the message waits for its summary, but not longer than 90 seconds. wp_schedule_single_event( time() + ( Ai::enabled() ? 90 : 0 ), 'dealroot_send_notification', array( (int) $lead_id ) ); } /** * Sends the notification once the assistant read the enquiry. When that happened while the * visitor waits, e.g. for an order read at the checkout, it goes out in the background, so the * customer does not wait for the email or Telegram. * * @param int $lead_id Lead ID. */ public function analyzed( $lead_id ) { if ( wp_doing_cron() ) { $this->notify( (int) $lead_id ); return; } if ( ! ( self::telegram_ready() || Settings::get( 'notify_new' ) ) ) { return; } $args = array( (int) $lead_id ); $next = wp_next_scheduled( 'dealroot_send_notification', $args ); if ( $next && $next <= time() + 5 ) { return; } if ( $next ) { wp_unschedule_event( $next, 'dealroot_send_notification', $args ); } wp_schedule_single_event( time(), 'dealroot_send_notification', $args ); } /** * Sends the queued notification. * * @param int $lead_id Lead ID. */ public function notify( $lead_id ) { Ai::in_locale( Ai::team_locale(), fn() => $this->tell( (int) $lead_id ) ); } /** * Sends the notification in the language of the team. * * @param int $lead_id Lead ID. */ private function tell( $lead_id ) { $lead = Leads::get( $lead_id ); if ( ! $lead || 'spam' === $lead->status || 'manual' === $lead->form_plugin || strtotime( $lead->created_at . ' UTC' ) < time() - DAY_IN_SECONDS ) { return; } // Each sent once, whichever comes first: the summary or the 90 seconds. if ( self::telegram_ready() && ! get_transient( 'dealroot_notified_' . (int) $lead->id ) ) { set_transient( 'dealroot_notified_' . (int) $lead->id, 1, DAY_IN_SECONDS ); Telegram_Bot::maybe_connect(); self::send_telegram( Settings::get( 'telegram_token' ), Settings::get( 'telegram_chat_id' ), self::message( $lead ), Telegram_Bot::lead_buttons( $lead ) ); } if ( Settings::get( 'notify_new' ) && ! in_array( (string) $lead->ai_kind, array( 'spam' ), true ) && ! get_transient( 'dealroot_mailed_' . (int) $lead->id ) ) { set_transient( 'dealroot_mailed_' . (int) $lead->id, 1, DAY_IN_SECONDS ); self::mail( $lead ); } } /** * The email about a new enquiry for the owner. * * @param object $lead Lead. * @return bool */ public static function mail( $lead ) { $name = '' !== trim( (string) $lead->name ) ? (string) $lead->name : (string) $lead->email; $details = Ai::details( $lead ); $facts = array(); $labels = array( 'when' => __( 'When', 'dealroot' ), 'quantity' => __( 'How many', 'dealroot' ), 'place' => __( 'Where', 'dealroot' ), ); foreach ( array( __( 'Email', 'dealroot' ) => (string) $lead->email, __( 'Phone', 'dealroot' ) => (string) $lead->phone, __( 'Form', 'dealroot' ) => (string) $lead->form_name, ) as $label => $value ) { if ( '' !== trim( $value ) ) { $facts[ $label ] = $value; } } if ( null !== $lead->ai_budget ) { $facts[ __( 'Budget', 'dealroot' ) ] = Admin\Format::money( (float) $lead->ai_budget, (string) $lead->ai_currency ); } foreach ( $labels as $key => $label ) { if ( '' !== (string) ( $details[ $key ] ?? '' ) ) { $facts[ $label ] = $details[ $key ]; } } $facts[ __( 'Came from', 'dealroot' ) ] = trim( Attribution::channel_label( (string) $lead->channel ) . ( '' !== (string) $lead->campaign ? ' – ' . $lead->campaign : ( '' !== (string) $lead->source ? ' – ' . $lead->source : '' ) ) ); $signals = Lead_Signals::line( $lead ); if ( '' !== $signals ) { $facts[ __( 'Before writing', 'dealroot' ) ] = $signals; } $replies = Replies::all( $lead ); $subject = '' !== (string) ( $replies[0]['subject'] ?? '' ) ? Replies::fill( $replies[0]['subject'], $lead ) : __( 'Your enquiry', 'dealroot' ); return Mail::owner( Digest::recipients(), /* translators: 1: name of the person, 2: site name */ sprintf( __( 'New enquiry from %1$s – %2$s', 'dealroot' ), $name, wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ), array( /* translators: %s: name of the person */ 'title' => ( ! empty( $lead->ai_urgent ) ? '🔥 ' : '' ) . sprintf( __( 'New enquiry from %s', 'dealroot' ), $name ), 'text' => '' !== (string) $lead->ai_summary ? (string) $lead->ai_summary : wp_html_excerpt( Ai::text( $lead ), 600, '…' ), 'facts' => $facts, 'button' => array( 'url' => admin_url( 'admin.php?page=dealroot&lead=' . (int) $lead->id ), 'label' => __( 'Open the lead', 'dealroot' ), ), 'second' => is_email( $lead->email ) ? array( 'url' => 'mailto:' . rawurlencode( (string) $lead->email ) . '?subject=' . rawurlencode( $subject ), 'label' => __( 'Reply by email', 'dealroot' ), ) : null, 'note' => __( 'You get this email because "Email me each new enquiry" is on in the settings of Dealroot.', 'dealroot' ), ) ); } /** * Whether Telegram is configured. * * @return bool */ public static function telegram_ready() { $settings = Settings::all(); return ! empty( $settings['telegram_enabled'] ) && '' !== $settings['telegram_token'] && '' !== $settings['telegram_chat_id']; } /** * Calls the Telegram Bot API. * * @param string $token Bot token. * @param string $chat_id Chat ID. * @param string $text HTML message. * @param array $buttons Inline keyboard, optional. * @return true|\WP_Error */ public static function send_telegram( $token, $chat_id, $text, $buttons = null ) { if ( ! preg_match( '/^\d+:[A-Za-z0-9_-]+$/', (string) $token ) ) { return new \WP_Error( 'dealroot_telegram', __( 'The bot token format is not valid.', 'dealroot' ) ); } $fields = array( 'chat_id' => $chat_id, 'text' => $text, 'parse_mode' => 'HTML', 'disable_web_page_preview' => 'true', ); if ( is_array( $buttons ) ) { $fields['reply_markup'] = wp_json_encode( $buttons ); } $response = wp_remote_post( 'https://api.telegram.org/bot' . $token . '/sendMessage', array( 'timeout' => 10, 'body' => $fields, ) ); if ( is_wp_error( $response ) ) { return $response; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $body['ok'] ) ) { return new \WP_Error( 'dealroot_telegram', $body['description'] ?? __( 'Unknown Telegram error.', 'dealroot' ) ); } return true; } /** * The chat in which the bot was written to last, from the updates Telegram keeps for a day. * * @param string $token Bot token. * @return array{id: string, title: string}|\WP_Error */ public static function find_chat( $token ) { if ( ! preg_match( '/^\d+:[A-Za-z0-9_-]+$/', (string) $token ) ) { return new \WP_Error( 'dealroot_telegram', __( 'The bot token format is not valid.', 'dealroot' ) ); } // Telegram only hands out updates while it does not send them to the site. if ( false !== get_option( Telegram_Bot::OPTION ) ) { wp_remote_post( 'https://api.telegram.org/bot' . $token . '/deleteWebhook', array( 'timeout' => 10 ) ); delete_option( Telegram_Bot::OPTION ); } $response = wp_remote_get( 'https://api.telegram.org/bot' . $token . '/getUpdates?limit=50', array( 'timeout' => 10 ) ); if ( is_wp_error( $response ) ) { return $response; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $body['ok'] ) ) { return new \WP_Error( 'dealroot_telegram', $body['description'] ?? __( 'Unknown Telegram error.', 'dealroot' ) ); } foreach ( array_reverse( (array) ( $body['result'] ?? array() ) ) as $update ) { foreach ( array( 'message', 'channel_post', 'my_chat_member', 'edited_message' ) as $kind ) { $chat = $update[ $kind ]['chat'] ?? null; if ( is_array( $chat ) && isset( $chat['id'] ) ) { $title = $chat['title'] ?? trim( ( $chat['first_name'] ?? '' ) . ' ' . ( $chat['last_name'] ?? '' ) ); return array( 'id' => (string) $chat['id'], 'title' => sanitize_text_field( '' !== $title ? $title : ( $chat['username'] ?? '' ) ), ); } } } return new \WP_Error( 'dealroot_telegram', __( 'No message to the bot was found. Write any message to your bot in Telegram, then try again.', 'dealroot' ) ); } /** * Message text for a lead. * * @param object $lead Lead row. * @return string */ public static function message( $lead ) { $esc = static function ( $text ) { return htmlspecialchars( (string) $text, ENT_NOQUOTES, 'UTF-8' ); }; $lines = array(); /* translators: %s: site name. */ $lines[] = '' . $esc( sprintf( __( 'New lead on %s', 'dealroot' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ) ) . ''; foreach ( array( $lead->name, $lead->email, $lead->phone ) as $value ) { if ( '' !== $value ) { $lines[] = $esc( $value ); } } if ( '' !== $lead->form_name ) { $lines[] = $esc( __( 'Form', 'dealroot' ) . ': ' . $lead->form_name ); } if ( '' !== (string) ( $lead->ai_summary ?? '' ) ) { $lines[] = ''; $flag = ! empty( $lead->ai_urgent ) ? __( 'Urgent', 'dealroot' ) : ( 'job' === ( $lead->ai_kind ?? '' ) ? __( 'Job application', 'dealroot' ) : '' ); $lines[] = ( '' !== $flag ? '' . $esc( $flag ) . ': ' : '' ) . $esc( $lead->ai_summary ); if ( null !== ( $lead->ai_budget ?? null ) ) { $lines[] = $esc( __( 'Budget', 'dealroot' ) ) . ': ' . $esc( Admin\Format::money( (float) $lead->ai_budget, (string) $lead->ai_currency ) ) . ''; } foreach ( array_intersect_key( Ai::details( $lead ), array( 'when' => 1 ) ) as $value ) { $lines[] = $esc( __( 'When', 'dealroot' ) ) . ': ' . $esc( $value ) . ''; } } $customer = Lead_Insights::customer( $lead ); if ( $customer ) { $lines[] = '' . $esc( Lead_Insights::customer_line( $customer ) ) . ''; } $signals = Lead_Signals::line( $lead ); if ( '' !== $signals ) { $lines[] = ( Lead_Signals::ready( $lead ) ? '🔥 ' : '' ) . $esc( $signals ); } $privacy = Privacy_Requests::line( $lead ); if ( '' !== $privacy ) { $lines[] = '⚠️ ' . $esc( $privacy ) . ''; } if ( ! empty( $lead->ai_abusive ) ) { $lines[] = '⚠️ ' . $esc( __( 'Insults or threatens. No answer was written.', 'dealroot' ) ) . ''; } if ( Ai::is_foreign( $lead, strtolower( substr( get_locale(), 0, 2 ) ) ) ) { /* translators: %s: name of a language */ $lines[] = $esc( sprintf( __( 'Written in %s.', 'dealroot' ), Ai::language_name( (string) $lead->ai_lang ) ) ); } $lines[] = ''; foreach ( Attribution::summary_rows( (array) $lead ) as $label => $value ) { $lines[] = $esc( $label ) . ': ' . $esc( $value ) . ''; } $message = self::first_long_field( $lead ); if ( '' !== $message ) { $lines[] = ''; $lines[] = '' . $esc( mb_strlen( $message ) > 600 ? mb_substr( $message, 0, 599 ) . '…' : $message ) . ''; } $links = array( '' . $esc( __( 'Open lead', 'dealroot' ) ) . '' ); if ( 0 === strpos( (string) $lead->phone, '+' ) ) { $links[] = 'WhatsApp'; } $lines[] = ''; $lines[] = implode( ' · ', $links ); /** * Filters the Telegram message (HTML subset). * * @param string $text Message. * @param object $lead Lead row. */ return apply_filters( 'dealroot_telegram_message', implode( "\n", $lines ), $lead ); } /** * The free-text message, if the form had one. * * @param object $lead Lead row. * @return string */ private static function first_long_field( $lead ) { $fields = json_decode( (string) $lead->fields, true ); foreach ( (array) $fields as $field ) { $value = (string) ( $field['value'] ?? '' ); if ( 'textarea' === ( $field['type'] ?? '' ) || mb_strlen( $value ) > 60 ) { return $value; } } return ''; } }