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 '';
}
}