self::learn( $lead_id, $text ) ) ); } /** * Keeps an answer that was sent. * * @param int $lead_id Lead it answered. * @param string $text Text sent. * @return bool Whether it was kept. */ public static function learn( $lead_id, $text ) { $lead = Leads::get( (int) $lead_id ); $text = trim( str_replace( "\r", '', (string) $text ) ); if ( ! $lead || ! self::enabled() || mb_strlen( $text ) < self::MIN_LENGTH || self::is_template( $text, $lead ) ) { return false; } $text = self::anonymize( $text, $lead ); $answers = array_values( array_filter( self::all(), static function ( $answer ) use ( $text ) { return $answer['text'] !== $text; } ) ); array_unshift( $answers, array( 'text' => mb_substr( $text, 0, 1500 ), 'lang' => (string) $lead->ai_lang, 'time' => time(), ) ); return update_option( self::OPTION, array_slice( $answers, 0, self::KEEP ), false ); } /** * The answers kept, newest first. * * @return array */ public static function all() { $answers = get_option( self::OPTION, array() ); return is_array( $answers ) ? array_values( array_filter( $answers, 'is_array' ) ) : array(); } /** * Answers for a draft: the newest in the language of the reply first, then the others. * * @param string $lang Language of the reply, or "". * @return string[] */ public static function examples( $lang = '' ) { if ( ! self::enabled() ) { return array(); } $same = array(); $other = array(); foreach ( self::all() as $answer ) { if ( '' !== $lang && $answer['lang'] === $lang ) { $same[] = $answer['text']; } else { $other[] = $answer['text']; } } // Answers in another language still show greeting and length, but only when none fit. return array_slice( $same ? $same : $other, 0, self::USE ); } /** * How many answers the drafts learn from. * * @return int */ public static function count() { return count( self::all() ); } /** * Forgets all answers. */ public static function forget() { delete_option( self::OPTION ); } /** * Whether a text is one of the quick replies as written in the settings, not the owner's own. * * @param string $text Text. * @param object $lead Lead. * @return bool */ private static function is_template( $text, $lead ) { foreach ( Replies::all() as $reply ) { if ( trim( Replies::fill( $reply['body'], $lead ) ) === $text ) { return true; } } return false; } /** * The text without the name, email address and phone number of the person. * * @param string $text Text. * @param object $lead Lead. * @return string */ private static function anonymize( $text, $lead ) { $text = (string) preg_replace( '/[^\s@<>]+@[^\s@<>]+\.[a-z]{2,}/iu', '[email]', $text ); $text = (string) preg_replace_callback( '/(?= 7 ? '[phone]' : $found[0]; }, $text ); $names = array_filter( preg_split( '/\s+/u', trim( (string) $lead->name ) ), static function ( $part ) { return mb_strlen( $part ) >= 2; } ); // The longest first, so "Anna-Lena" goes before "Anna". usort( $names, static function ( $a, $b ) { return mb_strlen( $b ) <=> mb_strlen( $a ); } ); foreach ( $names as $part ) { $text = (string) preg_replace( '/(?