120 ) { self::say( __( 'Voice messages can be up to two minutes long.', 'dealroot' ) ); return; } self::later( self::VOICE_HOOK, array( (string) $voice['file_id'], (int) ( $message['date'] ?? time() ) ) ); return; } // A chat exported from WhatsApp comes as a text file. $document = $message['document'] ?? null; if ( is_array( $document ) && ! empty( $document['file_id'] ) ) { if ( 'text/plain' !== ( $document['mime_type'] ?? '' ) ) { self::say( __( 'Send a voice message, a text, or a chat exported from WhatsApp as a text file.', 'dealroot' ) ); return; } self::later( self::TEXT_HOOK, array( 'file:' . $document['file_id'], 'conversation', '' ) ); return; } $text = trim( (string) ( $message['text'] ?? '' ) ); if ( '' === $text ) { return; } if ( preg_match( '#^/(start|help)\b#', $text ) ) { self::say( self::help() ); return; } $forwarded = $message['forward_origin'] ?? null; if ( is_array( $forwarded ) || self::looks_like_chat( $text ) ) { self::later( self::TEXT_HOOK, array( $text, 'conversation', is_array( $forwarded ) ? self::forwarded_name( $forwarded ) : '' ) ); return; } self::later( self::TEXT_HOOK, array( $text, 'command', '' ) ); } /** * Whether a text is a conversation pasted in rather than an order: lines of a chat with times * and names, as WhatsApp copies them, or an email with its header lines. * * @param string $text Text. * @return bool */ public static function looks_like_chat( $text ) { $chat = preg_match_all( '/^\[?\d{1,2}[.\/]\d{1,2}[.\/]\d{2,4},? \d{1,2}:\d{2}(:\d{2})?\]?\s*(-\s*)?[^:\n]{1,40}:/mu', $text ); $email = preg_match( '/^(from|von|de|da|van|od|от|від):\s.+$/mui', $text ) && preg_match( '/^(subject|betreff|objet|asunto|oggetto|onderwerp|temat|тема):\s.+$/mui', $text ); return $chat >= 1 || $email || mb_strlen( $text ) > 600; } /** * A voice message: written down, then read like a typed order. * * @param string $file_id Telegram file ID. * @param int $sent When it was recorded. */ public static function voice( $file_id, $sent = 0 ) { self::typing(); $audio = Telegram_Bot::download( $file_id, self::MAX_AUDIO ); $text = is_wp_error( $audio ) ? $audio : Ai::transcribe( $audio ); if ( is_wp_error( $text ) ) { self::say( $text->get_error_message() ); return; } if ( '' === $text ) { self::say( __( 'I could not hear anything in this voice message. Please record it again.', 'dealroot' ) ); return; } self::command( $text, $sent ? (int) $sent : time() ); } /** * A text: an order or a conversation passed on. * * @param string $text Text, or "file:" and the Telegram file ID of a text file. * @param string $kind "command" or "conversation". * @param string $name Name of the person a message was forwarded from. */ public static function text( $text, $kind, $name = '' ) { self::typing(); if ( 0 === strpos( (string) $text, 'file:' ) ) { $text = Telegram_Bot::download( substr( $text, 5 ), self::MAX_TEXT ); if ( is_wp_error( $text ) ) { self::say( $text->get_error_message() ); return; } $text = wp_check_invalid_utf8( (string) preg_replace( '/^\xEF\xBB\xBF/', '', $text ) ); } if ( 'conversation' === $kind ) { self::conversation( (string) $text, (string) $name ); return; } self::command( (string) $text, time() ); } /** * Carries out an order typed or spoken on the overview, e.g. "Move Meier to contacted", and * says what was done there instead of in Telegram. * * @param string $text Order. * @return array{action: string, text: string, lead: int} */ public static function on_screen( $text ) { self::$here = array(); self::$here_lead = 0; $action = self::command( $text, time() ); $said = self::$here; self::$here = null; return array( 'action' => $action, 'text' => trim( implode( "\n", $said ) ), 'lead' => self::$here_lead, ); } /** * Reads an order and does it. * * @param string $text Order, typed or written down from speech. * @param int $sent When it was sent. * @return string What was done: the action, or "". */ public static function command( $text, $sent ) { $order = Ai::ask( 'command', array( 'text' => mb_substr( $text, 0, 2000 ), 'today' => wp_date( 'Y-m-d' ), 'people' => self::people(), ), 90 ); if ( is_wp_error( $order ) ) { self::say( $order->get_error_message() ); return ''; } $action = (string) ( $order['action'] ?? 'unknown' ); if ( 'new_lead' === $action ) { self::new_lead( $order, $text, $sent ); return $action; } if ( 'ask' === $action ) { // On the screen the question was already counted as one. if ( is_array( self::$here ) ) { self::say( __( 'I did not see what to change. Say who and what, e.g. “Move Meier to contacted”.', 'dealroot' ) ); return ''; } self::answer_question( '' !== (string) ( $order['question'] ?? '' ) ? (string) $order['question'] : $text ); return $action; } if ( ! in_array( $action, array( 'won', 'lost', 'status', 'follow_up', 'reply', 'note' ), true ) ) { /* translators: %s: what the owner said */ self::say( sprintf( __( 'I did not understand what to do with “%s”.', 'dealroot' ), wp_html_excerpt( $text, 120, '…' ) ) . "\n\n" . self::help() ); return ''; } $lead = self::find( (string) ( $order['person'] ?? '' ) ); if ( ! $lead ) { self::say( '' !== (string) ( $order['person'] ?? '' ) /* translators: %s: name */ ? sprintf( __( 'I found no lead named %s from the last twelve months.', 'dealroot' ), $order['person'] ) : __( 'Whom do you mean? Please say the name of the customer.', 'dealroot' ) ); return ''; } $name = self::name( $lead ); switch ( $action ) { case 'won': $changes = array( 'status' => 'won' ); if ( (float) ( $order['amount'] ?? 0 ) > 0 ) { $changes['value'] = (float) $order['amount']; $changes['currency'] = self::currency( $order['currency'] ?? '' ); } Leads::update( (int) $lead->id, $changes ); $lead = Leads::get( (int) $lead->id ); self::say( null !== $lead->value /* translators: 1: name, 2: amount */ ? sprintf( __( '✓ %1$s: won, %2$s.', 'dealroot' ), $name, Admin\Format::money( (float) $lead->value, (string) $lead->currency ) ) /* translators: %s: name */ : sprintf( __( '✓ %s: won. Send the amount too, e.g. “1,500 euros”, so the revenue is right.', 'dealroot' ), $name ), $lead ); break; case 'lost': // "Müller cancelled, too expensive" says why, too. Leads::update( (int) $lead->id, array( 'status' => 'lost', 'lost_reason' => Lost_Reasons::guess( $text ), ) ); $reason = Lost_Reasons::guess( $text ); self::say( '' !== $reason /* translators: 1: name, 2: reason */ ? sprintf( __( '✓ %1$s: lost – %2$s.', 'dealroot' ), $name, Lost_Reasons::labels()[ $reason ] ) /* translators: %s: name */ : sprintf( __( '✓ %s: lost.', 'dealroot' ), $name ), $lead ); break; case 'status': $stage = in_array( $order['status'] ?? '', array( 'new', 'contacted', 'qualified' ), true ) ? (string) $order['status'] : 'contacted'; Leads::update( (int) $lead->id, array( 'status' => $stage ) ); /* translators: 1: name, 2: status */ self::say( sprintf( __( '✓ %1$s: %2$s.', 'dealroot' ), $name, Leads::status_labels()[ $stage ] ), $lead ); break; case 'follow_up': self::follow_up( $lead, (string) ( $order['date'] ?? '' ), (string) ( $order['time'] ?? '' ), (string) ( $order['message'] ?? '' ) ); break; case 'reply': if ( ! empty( $lead->ai_abusive ) ) { self::say( Ai::abusive_note(), $lead ); break; } // On the screen the assistant writes the answer into the lead, to read and send there. if ( is_array( self::$here ) ) { $draft = Ai::draft( $lead, (string) ( $order['message'] ?? '' ) ); if ( is_string( $draft ) && '' !== $draft ) { Inbox::store_draft( (int) $lead->id, $draft ); /* translators: %s: name */ self::say( sprintf( __( '✓ The answer to %s is written. Open the lead to read it, add photos or documents and send it.', 'dealroot' ), $name ) . "\n\n" . $draft, $lead ); } else { /* translators: %s: name */ self::say( sprintf( __( 'Open %s to write the answer and send it with photos or documents.', 'dealroot' ), $name ), $lead ); } break; } Telegram_Bot::write_draft( (int) $lead->id, (string) ( $order['message'] ?? '' ) ); break; case 'note': $note = trim( (string) ( $order['message'] ?? '' ) ); $line = wp_date( get_option( 'date_format' ) ) . ': ' . ( '' !== $note ? $note : $text ); Leads::update( (int) $lead->id, array( 'note' => trim( (string) $lead->note . "\n" . $line ) ) ); /* translators: %s: name */ self::say( sprintf( __( '✓ Noted for %s.', 'dealroot' ), $name ), $lead ); break; } return $action; } /** * A new lead from what the owner tells about it. * * @param array $order What the assistant read. * @param string $text What the owner said. * @param int $sent When it was said. * @return int Lead ID. */ private static function new_lead( array $order, $text, $sent ) { $how = (string) ( $order['how'] ?? 'other' ); $lead_id = self::create( array( 'name' => (string) ( $order['person'] ?? '' ), 'message' => $text, 'summary' => (string) ( $order['summary'] ?? '' ), 'topic' => (string) ( $order['topic'] ?? '' ), 'budget' => (float) ( $order['amount'] ?? 0 ), 'currency' => (string) ( $order['currency'] ?? '' ), 'how' => $how, 'form' => __( 'Telegram, voice message or note', 'dealroot' ), ) ); if ( ! $lead_id ) { self::say( __( 'Something went wrong. Please reload the page and try again.', 'dealroot' ) ); return 0; } $click = in_array( $how, array( 'phone', 'whatsapp' ), true ) ? self::link_click( $lead_id, 'phone' === $how ? 'phone' : 'whatsapp', $sent ) : null; $lead = Leads::get( $lead_id ); $lines = array( /* translators: %s: name */ sprintf( __( '✓ New lead: %s', 'dealroot' ), self::name( $lead ) ), ); if ( '' !== (string) $lead->ai_summary ) { $lines[] = $lead->ai_summary; } if ( null !== $lead->ai_budget ) { $lines[] = __( 'Budget', 'dealroot' ) . ': ' . Admin\Format::money( (float) $lead->ai_budget, (string) $lead->ai_currency ); } $buttons = array( array( Telegram_Bot::open_button( $lead, __( 'Open lead', 'dealroot' ) ) ) ); if ( $click ) { $lines[] = ''; $lines[] = sprintf( /* translators: 1: time, 2: where the visitor came from */ __( 'Linked to the click on your phone number at %1$s – from %2$s. The call now counts for this source.', 'dealroot' ), wp_date( get_option( 'time_format' ), (int) strtotime( $click->created_at . ' UTC' ) ), Attribution::channel_label( (string) $click->channel ) . ( '' !== (string) $click->campaign ? ', ' . $click->campaign : '' ) ); $buttons[] = array( array( 'text' => __( 'It was not that call', 'dealroot' ), 'callback_data' => 'unlink:' . (int) $lead_id, ), ); } self::say( implode( "\n", $lines ), $lead, array( 'inline_keyboard' => $buttons ) ); return $lead_id; } /** * A conversation passed on, filed as a lead. * * @param string $text Conversation. * @param string $name Name of the person it was forwarded from. * @return int Lead ID. */ public static function conversation( $text, $name = '' ) { $read = self::read( $text ); if ( is_wp_error( $read ) ) { self::say( $read->get_error_message() ); return 0; } if ( '' !== $name ) { $read['name'] = $name; } $lead_id = self::create( $read + array( 'form' => __( 'Telegram, conversation passed on', 'dealroot' ) ) ); if ( ! $lead_id ) { self::say( __( 'Something went wrong. Please reload the page and try again.', 'dealroot' ) ); return 0; } $lead = Leads::get( $lead_id ); $lines = array( /* translators: %s: name */ sprintf( __( '✓ New lead from the conversation: %s', 'dealroot' ), self::name( $lead ) ), ); foreach ( array( $lead->ai_summary, $lead->email, $lead->phone ) as $value ) { if ( '' !== (string) $value ) { $lines[] = (string) $value; } } if ( null !== $lead->ai_budget ) { $lines[] = __( 'Budget', 'dealroot' ) . ': ' . Admin\Format::money( (float) $lead->ai_budget, (string) $lead->ai_currency ); } self::say( implode( "\n", $lines ), $lead, Telegram_Bot::lead_buttons( $lead ) ); return $lead_id; } /** * What a conversation says about the customer: name, email, phone, summary, topic, urgency, * budget and how they got in touch. Email address and phone number are found here and never * sent to the assistant. * * @param string $text Conversation. * @return array|\WP_Error */ public static function read( $text ) { $text = mb_substr( trim( (string) $text ), 0, 20000 ); $read = Ai::ask( 'conversation', array( 'text' => mb_substr( $text, 0, 8000 ) ), 90 ); if ( is_wp_error( $read ) ) { return $read; } $how = (string) ( $read['how'] ?? 'other' ); return array( 'name' => (string) ( $read['name'] ?? '' ), 'email' => self::find_email( $text ), 'phone' => self::find_phone( $text ), 'message' => $text, 'summary' => (string) ( $read['summary'] ?? '' ), 'topic' => (string) ( $read['topic'] ?? '' ), 'urgent' => ! empty( $read['urgent'] ), 'budget' => (float) ( $read['budget'] ?? 0 ), 'currency' => self::currency( $read['currency'] ?? '' ), 'how' => $how, 'channel' => self::way( $how )[0], 'source' => self::way( $how )[1], ); } /** * Channel and source for how someone got in touch. * * @param string $how phone, whatsapp, email, telegram, sms, visit, recommendation or other. * @return array{0: string, 1: string} */ public static function way( $how ) { $ways = array( 'phone' => array( 'phone_call', '' ), 'visit' => array( 'walk_in', '' ), 'recommendation' => array( 'word_of_mouth', '' ), 'whatsapp' => array( 'direct', 'WhatsApp' ), 'telegram' => array( 'direct', 'Telegram' ), 'email' => array( 'email', 'Email' ), 'sms' => array( 'direct', 'SMS' ), ); return $ways[ $how ] ?? array( '', '' ); } /** * Stores a lead the owner told about or passed on. * * @param array $values name, email, phone, message, summary, topic, urgent, budget, currency, how, form. * @return int Lead ID. */ private static function create( array $values ) { global $wpdb; $values = wp_parse_args( $values, array( 'name' => '', 'email' => '', 'phone' => '', 'message' => '', 'summary' => '', 'topic' => '', 'urgent' => false, 'budget' => 0, 'currency' => '', 'how' => 'other', 'form' => '', ) ); $way = self::way( (string) $values['how'] ); $fields = array(); foreach ( array( 'name' => array( __( 'Name', 'dealroot' ), 'text' ), 'email' => array( __( 'Email', 'dealroot' ), 'email' ), 'phone' => array( __( 'Phone', 'dealroot' ), 'tel' ), 'message' => array( __( 'Message', 'dealroot' ), 'textarea' ), ) as $key => $field ) { if ( '' !== trim( (string) $values[ $key ] ) ) { $fields[] = array( 'label' => $field[0], 'name' => $key, 'type' => $field[1], 'value' => trim( (string) $values[ $key ] ), ); } } $budget = (float) $values['budget'] > 0 ? round( (float) $values['budget'], 2 ) : null; $data = array( 'form_plugin' => 'manual', 'form_name' => $values['form'], 'name' => mb_substr( sanitize_text_field( $values['name'] ), 0, 191 ), 'email' => is_email( $values['email'] ) ? $values['email'] : '', 'phone' => mb_substr( sanitize_text_field( $values['phone'] ), 0, 50 ), 'fields' => wp_json_encode( $fields ), 'channel' => $way[0], 'last_channel' => $way[0], 'source' => $way[1], 'last_source' => $way[1], 'currency' => self::currency( $values['currency'] ), 'value' => $budget, ); $lead_id = Leads::insert( $data ); if ( ! $lead_id ) { return 0; } $wpdb->update( Installer::table(), array( 'ai_summary' => mb_substr( sanitize_text_field( $values['summary'] ), 0, 255 ), 'ai_topics' => mb_substr( sanitize_text_field( $values['topic'] ), 0, 40 ), 'ai_urgent' => $values['urgent'] ? 1 : 0, 'ai_kind' => 'enquiry', 'ai_state' => 'done', 'ai_budget' => $budget, 'ai_currency' => null !== $budget ? self::currency( $values['currency'] ) : '', ), array( 'id' => (int) $lead_id ) ); /** This action is documented in includes/class-capture.php */ do_action( 'dealroot_lead_created', $lead_id, $data ); return (int) $lead_id; } /** * Gives a lead the source of the last click on the phone number or WhatsApp before the call, * when it is recent and not taken by another lead. * * @param int $lead_id Lead ID. * @param string $type "phone" or "whatsapp". * @param int $before Time of the voice message. * @return object|null The click. */ public static function link_click( $lead_id, $type, $before ) { global $wpdb; $click = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE type = %s AND lead_id = 0 AND created_at >= %s AND created_at <= %s ORDER BY created_at DESC LIMIT 1', Installer::events_table(), $type, gmdate( 'Y-m-d H:i:s', $before - self::CALL_HOURS * HOUR_IN_SECONDS ), gmdate( 'Y-m-d H:i:s', $before ) ) ); if ( ! $click || '' === (string) $click->channel ) { return null; } $columns = array(); foreach ( array( 'channel', 'source', 'medium', 'campaign', 'last_channel', 'last_source', 'last_medium', 'last_campaign', 'landing_page', 'gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid' ) as $column ) { $columns[ $column ] = (string) ( $click->$column ?? '' ); } $wpdb->update( Installer::table(), $columns, array( 'id' => (int) $lead_id ) ); $wpdb->update( Installer::events_table(), array( 'lead_id' => (int) $lead_id ), array( 'id' => (int) $click->id ) ); return $click; } /** * Takes the source of a click away from a lead again, when the call was another one. * * @param int $lead_id Lead ID. * @return bool */ public static function unlink_click( $lead_id ) { global $wpdb; $lead = Leads::get( (int) $lead_id ); if ( ! $lead || 'manual' !== $lead->form_plugin ) { return false; } $wpdb->update( Installer::events_table(), array( 'lead_id' => 0 ), array( 'lead_id' => (int) $lead_id ) ); $wpdb->update( Installer::table(), array( 'channel' => 'phone_call', 'last_channel' => 'phone_call', 'source' => '', 'last_source' => '', 'medium' => '', 'last_medium' => '', 'campaign' => '', 'last_campaign' => '', 'landing_page' => '', 'gclid' => '', 'gbraid' => '', 'wbraid' => '', 'fbclid' => '', 'msclkid' => '', ), array( 'id' => (int) $lead_id ) ); return true; } /** * A reminder of a lead, in Telegram and, with Dealroot Pro, as its follow-up date. * * @param object $lead Lead. * @param string $date Day, YYYY-MM-DD. * @param string $time Time, HH:MM, optional. * @param string $note What it is about, optional. */ private static function follow_up( $lead, $date, $time, $note ) { $when = '' !== $date ? date_create_immutable( $date . ' ' . ( '' !== $time ? $time : '09:00' ), wp_timezone() ) : false; if ( ! $when || $when->getTimestamp() <= time() ) { self::say( __( 'When should I remind you? Please say a day, e.g. “on Friday” or “tomorrow at 10”.', 'dealroot' ), $lead ); return; } wp_schedule_single_event( $when->getTimestamp(), self::REMIND_HOOK, array( (int) $lead->id, mb_substr( sanitize_text_field( $note ), 0, 300 ) ) ); /** * Fires when the owner asked to be reminded of a lead, e.g. to keep it as follow-up date. * * @param int $lead_id Lead ID. * @param string $time UTC time, Y-m-d H:i:s. */ do_action( 'dealroot_follow_up_set', (int) $lead->id, gmdate( 'Y-m-d H:i:s', $when->getTimestamp() ) ); self::say( sprintf( /* translators: 1: name, 2: date and time */ __( '✓ I will remind you of %1$s on %2$s.', 'dealroot' ), self::name( $lead ), wp_date( 'l, ' . get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $when->getTimestamp() ) ), $lead ); } /** * Sends a reminder that is due. * * @param int $lead_id Lead ID. * @param string $note What it is about. */ public static function remind( $lead_id, $note = '' ) { $lead = Leads::get( (int) $lead_id ); if ( ! $lead || in_array( $lead->status, array( 'won', 'lost', 'spam' ), true ) ) { return; } $lines = array( /* translators: %s: name */ '⏰ ' . sprintf( __( 'Reminder: %s', 'dealroot' ), self::name( $lead ) ), ); foreach ( array( $note, $lead->ai_summary, $lead->phone ) as $value ) { if ( '' !== (string) $value ) { $lines[] = (string) $value; } } // Without Telegram the reminder comes by email. if ( ! Notifier::telegram_ready() ) { Ai::in_locale( Ai::team_locale(), fn() => Mail::owner( Digest::recipients(), /* translators: %s: name */ sprintf( __( 'Reminder: %s', 'dealroot' ), self::name( $lead ) ), array( /* translators: %s: name */ 'title' => sprintf( __( 'Reminder: %s', 'dealroot' ), self::name( $lead ) ), 'text' => implode( "\n", array_slice( $lines, 1 ) ), 'button' => array( 'url' => admin_url( 'admin.php?page=dealroot&lead=' . (int) $lead->id ), 'label' => __( 'Open the lead', 'dealroot' ), ), ) ) ); return; } self::say( implode( "\n", $lines ), $lead, Telegram_Bot::lead_buttons( $lead ) ); } /** * Answers a question about the own enquiries with the numbers. * * @param string $question Question. */ private static function answer_question( $question ) { $answer = Ai_Ask::answer( $question ); if ( is_wp_error( $answer ) ) { self::say( $answer->get_error_message() ); return; } $lines = array(); if ( '' !== (string) ( $answer['title'] ?? '' ) ) { $lines[] = $answer['title'] . ( '' !== (string) ( $answer['period'] ?? '' ) ? ' (' . $answer['period'] . ')' : '' ); } foreach ( array_slice( (array) ( $answer['rows'] ?? array() ), 0, 12 ) as $row ) { $lines[] = '• ' . $row[0] . ': ' . $row[1]; } if ( '' !== (string) ( $answer['total'] ?? '' ) ) { $lines[] = ( ! empty( $answer['rows'] ) ? __( 'In all', 'dealroot' ) . ': ' : '' ) . $answer['total']; } if ( '' !== (string) ( $answer['text'] ?? '' ) ) { $lines[] = $answer['text']; } self::say( implode( "\n", $lines ) ); } /** * The lead a name means: the most recent one with that name, open ones first. * * @param string $person Name as said. * @return object|null */ public static function find( $person ) { global $wpdb; $person = trim( (string) $person ); if ( mb_strlen( $person ) < 2 ) { return null; } $words = array_filter( preg_split( '/\s+/u', $person ), static function ( $word ) { return mb_strlen( $word ) >= 3 && ! preg_match( '/^(herr|frau|mr|mrs|ms|monsieur|madame|señor|señora|signor|signora|pan|pani|meneer|mevrouw|пан|пані|господин|госпожа)\.?$/iu', $word ); } ); // The whole name first, then each of its words, the surname usually last. foreach ( array_merge( array( $person ), array_reverse( $words ) ) as $term ) { $lead = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM %i WHERE status <> 'spam' AND created_at >= %s AND ( name LIKE %s OR email LIKE %s ) ORDER BY status IN ('new', 'contacted', 'qualified') DESC, created_at DESC LIMIT 1", Installer::table(), gmdate( 'Y-m-d H:i:s', time() - YEAR_IN_SECONDS ), '%' . $wpdb->esc_like( $term ) . '%', $wpdb->esc_like( $term ) . '%' ) ); if ( $lead ) { return $lead; } } return null; } /** * Names of recent leads, so the assistant writes them as they are stored. * * @return string[] */ private static function people() { global $wpdb; $names = $wpdb->get_col( $wpdb->prepare( "SELECT name FROM %i WHERE name <> '' AND status <> 'spam' AND created_at >= %s GROUP BY name ORDER BY MAX(created_at) DESC LIMIT 40", Installer::table(), gmdate( 'Y-m-d H:i:s', time() - 90 * DAY_IN_SECONDS ) ) ); return array_values( array_map( 'strval', (array) $names ) ); } /** * Name of a person a message was forwarded from. * * @param array $origin forward_origin of a message. * @return string */ private static function forwarded_name( array $origin ) { if ( isset( $origin['sender_user'] ) ) { return trim( ( $origin['sender_user']['first_name'] ?? '' ) . ' ' . ( $origin['sender_user']['last_name'] ?? '' ) ); } return (string) ( $origin['sender_user_name'] ?? ( $origin['chat']['title'] ?? '' ) ); } /** * The first email address in a text that is not the one of the site. * * @param string $text Text. * @return string */ private static function find_email( $text ) { preg_match_all( '/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', $text, $matches ); $own = array( strtolower( (string) get_option( 'admin_email' ) ) ); foreach ( $matches[0] as $email ) { if ( is_email( $email ) && ! in_array( strtolower( $email ), $own, true ) ) { return $email; } } return ''; } /** * The first phone number in a text. * * @param string $text Text. * @return string */ private static function find_phone( $text ) { return preg_match( '/(?name ) ? (string) $lead->name : ( '' !== (string) $lead->email ? (string) $lead->email : '#' . (int) $lead->id ); } /** * What the bot understands. * * @return string */ public static function help() { return implode( "\n", array( __( 'Send me a voice message or a text, for example:', 'dealroot' ), '• ' . __( '“Müller called about the roof, wants an estimate, about 2,000 euros”', 'dealroot' ), '• ' . __( '“Müller won, 1,500 euros” or “Müller cancelled”', 'dealroot' ), '• ' . __( '“Remind me on Friday about Schmidt”', 'dealroot' ), '• ' . __( '“Answer Mrs Schmidt that I will call back at 2 pm”', 'dealroot' ), '• ' . __( '“How many enquiries came from Google this month?”', 'dealroot' ), '', __( 'Paste or forward a conversation, e.g. copied from WhatsApp, and it becomes a lead.', 'dealroot' ), ) ); } /** * Runs a step right after Telegram got its answer. * * @param string $hook Hook. * @param array $args Arguments. */ private static function later( $hook, array $args ) { wp_schedule_single_event( time(), $hook, $args ); spawn_cron(); } /** * Shows "typing…" in the chat while the assistant works. */ private static function typing() { Telegram_Bot::call( 'sendChatAction', array( 'chat_id' => Settings::get( 'telegram_chat_id' ), 'action' => 'typing', ) ); } /** * Answers in the chat. * * @param string $text Plain text. * @param object|null $lead Lead to open with a button. * @param array|null $buttons Buttons instead. */ private static function say( $text, $lead = null, $buttons = null ) { if ( is_array( self::$here ) ) { self::$here[] = (string) $text; if ( $lead ) { self::$here_lead = (int) $lead->id; } return; } if ( null === $buttons && $lead ) { $buttons = array( 'inline_keyboard' => array( array( Telegram_Bot::open_button( $lead, __( 'Open lead', 'dealroot' ) ) ) ) ); } Notifier::send_telegram( Settings::get( 'telegram_token' ), Settings::get( 'telegram_chat_id' ), htmlspecialchars( (string) $text, ENT_NOQUOTES, 'UTF-8' ), $buttons ); } }