'threadid', 'lastRevision' => 'lrevision', 'state' => 'istate', 'lastToken' => 'ltoken', 'nextAgent' => 'nextagent', 'groupId' => 'groupid', 'shownMessageId' => 'shownmessageid', 'messageCount' => 'messageCount', 'created' => 'dtmcreated', 'modified' => 'dtmmodified', 'chatStarted' => 'dtmchatstarted', 'agentId' => 'agentId', 'agentName' => 'agentName', 'agentTyping' => 'agentTyping', 'lastPingAgent' => 'lastpingagent', 'locale' => 'locale', 'userId' => 'userid', 'userName' => 'userName', 'userTyping' => 'userTyping', 'lastPingUser' => 'lastpinguser', 'remote' => 'remote', 'referer' => 'referer', 'userAgent' => 'userAgent' ); /** * Contain loaded from database information about thread * * Do not use this property manually! * @var array */ protected $threadInfo; /** * List of modified fields. * * Do not use this property manually! * @var array */ protected $updatedFields = array(); /** * Forbid create instance from outside of the class */ protected function __construct() {} /** * Create new empty thread in database * * @return boolean|Thread Returns an object of the Thread class or boolean * false on failure */ public static function create() { // Get database object $db = Database::getInstance(); // Create new empty thread $thread = new self(); // Create thread $db->query("insert into {chatthread} (threadid) values (NULL)"); // Set thread Id // In this case Thread::$threadInfo array use because id of a thread // should not be update $thread->threadInfo['threadid'] = $db->insertedId(); // Check if something went wrong if (empty($thread->id)) { return false; } // Set initial values $thread->lastToken = self::nextToken(); $thread->created = time(); return $thread; } /** * Create thread object from database info. * * @param array $thread_info Associative array of Thread info from database. * It must contains ALL thread table's * FIELDS from the database. * @return boolean|Thread Returns an object of the Thread class or boolean * false on failure */ public static function createFromDbInfo($thread_info) { // Create new empty thread $thread = new self(); // Check thread fields $obligatory_fields = array_values($thread->propertyMap); foreach($obligatory_fields as $field) { if (!array_key_exists($field, $thread_info)) { // Obligatory field is missing unset($thread); return false; } // Copy field to Thread object $thread->threadInfo[$field] = $thread_info[$field]; } return $thread; } /** * Load thread from database * * @param int $id ID of the thread to load * @return boolean|Thread Returns an object of the Thread class or boolean * false on failure */ public static function load($id, $last_token = null) { // Check $id if (empty($id)) { return false; } // Get database object $db = Database::getInstance(); // Create new empty thread $thread = new self(); // Load thread $thread_info = $db->query( "select * from {chatthread} where threadid = :threadid", array( ':threadid' => $id ), array('return_rows' => Database::RETURN_ONE_ROW) ); // There is no thread with such id in database if (! $thread_info) { return; } // Store thread properties $thread->threadInfo = $thread_info; // Check if something went wrong if ($thread->id != $id) { return false; } // Check last token if (! is_null($last_token)) { if ($thread->lastToken != $last_token) { return false; } } return $thread; } /** * Reopen thread and send message about it * * @return boolean|Thread Boolean FALSE on failure or thread object on success */ public static function reopen($id) { // Load thread $thread = self::load($id); // Check if user and agent gone if (Settings::get('thread_lifetime') != 0 && abs($thread->lastPingUser - time()) > Settings::get('thread_lifetime') && abs($thread->lastPingAgent - time()) > Settings::get('thread_lifetime')) { unset($thread); return false; } // Check if thread closed if ($thread->state == self::STATE_CLOSED || $thread->state == self::STATE_LEFT) { unset($thread); return false; } // Reopen thread if ($thread->state == self::STATE_WAITING) { $thread->nextAgent = 0; $thread->save(); } // Send message $thread->postMessage(self::KIND_EVENTS, getstring_("chat.status.user.reopenedthread", $thread->locale)); return $thread; } /** * Close all old threads that were not closed by some reasons */ public static function closeOldThreads() { if (Settings::get('thread_lifetime') == 0) { return; } $db = Database::getInstance(); $query = "update {chatthread} set lrevision = :next_revision, " . "dtmmodified = :now, istate = :state_closed " . "where istate <> :state_closed and istate <> :state_left " . "and ((lastpingagent <> 0 and lastpinguser <> 0 and " . "(ABS(:now - lastpinguser) > :thread_lifetime and " . "ABS(:now - lastpingagent) > :thread_lifetime)) or " . "(lastpingagent = 0 and lastpinguser <> 0 and " . "ABS(:now - lastpinguser) > :thread_lifetime))"; $db->query( $query, array( ':next_revision' => self::nextRevision(), ':now' => time(), ':state_closed' => self::STATE_CLOSED, ':state_left' => self::STATE_LEFT, ':thread_lifetime' => Settings::get('thread_lifetime') ) ); } /** * Check if connection limit reached * * @param string $remote User IP * @return boolean TRUE if connection limit reached and FALSE otherwise */ public static function connectionLimitReached($remote) { if (Settings::get('max_connections_from_one_host') == 0) { return false; } $db = Database::getInstance(); $result = $db->query( "select count(*) as opened from {chatthread} " . "where remote = ? AND istate <> ? AND istate <> ?", array($remote, Thread::STATE_CLOSED, Thread::STATE_LEFT), array('return_rows' => Database::RETURN_ONE_ROW) ); if ($result && isset($result['opened'])) { return $result['opened'] >= Settings::get('max_connections_from_one_host'); } return false; } /** * Return next revision number (last revision number plus one) * * @return int revision number */ protected static function nextRevision() { $db = Database::getInstance(); $db->query("update {chatrevision} set id=LAST_INSERT_ID(id+1)"); $val = $db->insertedId(); return $val; } /** * Create thread token * * @return int Thread token */ protected static function nextToken() { return rand(99999, 99999999); } /** * Implementation of the magic __get method * * Check if variable with name $name exists in the Thread::$propertyMap array. * If it does not exist triggers an error with E_USER_NOTICE level and returns false. * * @param string $name property name * @return mixed * @see Thread::$propertyMap */ public function __get($name) { // Check property existance if (! array_key_exists($name, $this->propertyMap)) { trigger_error("Undefined property '{$name}'", E_USER_NOTICE); return NULL; } $field_name = $this->propertyMap[$name]; return $this->threadInfo[$field_name]; } /** * Implementation of the magic __set method * * Check if variable with name $name exists in the Thread::$propertyMap array before setting. * If it does not exist triggers an error with E_USER_NOTICE level and value will NOT set. * * @param string $name Property name * @param mixed $value Property value * @return mixed * @see Thread::$propertyMap */ public function __set($name, $value) { if (empty($this->propertyMap[$name])) { trigger_error("Undefined property '{$name}'", E_USER_NOTICE); return; } $field_name = $this->propertyMap[$name]; $this->threadInfo[$field_name] = $value; if (! in_array($field_name, $this->updatedFields)) { $this->updatedFields[] = $field_name; } } /** * Implementation of the magic __isset method * * Check if variable with $name exists. * * param string $name Variable name * return boolean True if variable exists and false otherwise */ public function __isset($name) { if (!array_key_exists($name, $this->propertyMap)) { return false; } $property_name = $this->propertyMap[$name]; return isset($this->threadInfo[$property_name]); } /** * Remove thread from database */ public function delete() { $db = Database::getInstance(); $db->query( "DELETE FROM {chatthread} WHERE threadid = :id LIMIT 1", array(':id' => $this->id) ); } /** * Ping the thread. * * Updates ping time for conversation members and sends messages about connection problems. * * @param boolean $is_user Indicates user or operator pings thread. Boolean true for user and boolean false * otherwise. * @param boolean $is_typing Indicates if user or operator is typing a message. */ public function ping($is_user, $is_typing) { // Last ping time of other side $last_ping_other_side = 0; // Update last ping time if ($is_user) { $last_ping_other_side = $this->lastPingAgent; $this->lastPingUser = time(); $this->userTyping = $is_typing ? "1" : "0"; } else { $last_ping_other_side = $this->lastPingUser; $this->lastPingAgent = time(); $this->agentTyping = $is_typing ? "1" : "0"; } // Update thread state for the first user ping if ($this->state == self::STATE_LOADING && $is_user) { $this->state = self::STATE_QUEUE; $this->save(); return; } // Check if other side of the conversation have connection problems if ($last_ping_other_side > 0 && abs(time() - $last_ping_other_side) > self::CONNECTION_TIMEOUT) { // Connection problems detected if ($is_user) { // _Other_ side is operator // Update operator's last ping time $this->lastPingAgent = 0; // Check if user chatting at the moment if ($this->state == self::STATE_CHATTING) { // Send message to user $message_to_post = getstring_("chat.status.operator.dead", $this->locale); $this->postMessage( self::KIND_CONN, $message_to_post, null, null, $last_ping_other_side + self::CONNECTION_TIMEOUT ); // And update thread $this->state = self::STATE_WAITING; $this->nextAgent = 0; } } else { // _Other_ side is user // Update user's last ping time $this->lastPingUser = 0; // And send a message to operator $message_to_post = getstring_("chat.status.user.dead", $this->locale); $this->postMessage( self::KIND_FOR_AGENT, $message_to_post, null, null, $last_ping_other_side + self::CONNECTION_TIMEOUT ); } } $this->save(false); } /** * Save the thread to the database * * @param boolean $update_revision Indicates if last modified time and last revision should be updated */ public function save($update_revision = true){ $db = Database::getInstance(); // Update modified time and last revision if need if ($update_revision) { $this->lastRevision = $this->nextRevision(); $this->modified = time(); } $values = array(); $set_clause = array(); foreach ($this->updatedFields as $field_name) { $set_clause[] = "{$field_name} = ?"; $values[] = $this->threadInfo[$field_name]; } $query = "update {chatthread} t set " . implode(', ', $set_clause) . " where threadid = ?"; $values[] = $this->id; $db->query($query, $values); } /** * Check if thread is reassigned for another operator * * Updates thread info, send events messages and avatar message to user * @global string $home_locale * @param array $operator Operator for test */ public function checkForReassign($operator) { global $home_locale; $operator_name = ($this->locale == $home_locale) ? $operator['vclocalename'] : $operator['vccommonname']; if ($this->state == self::STATE_WAITING && ($this->nextAgent == $operator['operatorid'] || $this->agentId == $operator['operatorid'])) { // Prepare message if ($this->nextAgent == $operator['operatorid']) { $message_to_post = getstring2_( "chat.status.operator.changed", array($operator_name, $this->agentName), $this->locale ); } else { $message_to_post = getstring2_("chat.status.operator.returned", array($operator_name), $this->locale); } // Update thread info $this->state = self::STATE_CHATTING; $this->nextAgent = 0; $this->agentId = $operator['operatorid']; $this->agentName = $operator_name; $this->save(); // Send messages $this->postMessage(self::KIND_EVENTS, $message_to_post); $this->postMessage(self::KIND_AVATAR, $operator['vcavatar'] ? $operator['vcavatar'] : ""); } } /** * Load messages from database corresponding to the thread those ID's more than $lastid * * @global $webim_encoding * @param boolean $is_user Boolean TRUE if messages loads for user and boolean FALSE if they loads for operator. * @param int $lastid ID of the last loaded message. * @return array Array of messages * @see Thread::postMessage() */ public function getMessages($is_user, &$last_id) { global $webim_encoding; $db = Database::getInstance(); // Load messages $messages = $db->query( "select messageid as id, ikind as kind, dtmcreated as created, tname as name, tmessage as message " . "from {chatmessage} " . "where threadid = :threadid and messageid > :lastid " . ($is_user ? "and ikind <> " . self::KIND_FOR_AGENT : "") . " order by messageid", array( ':threadid' => $this->id, ':lastid' => $last_id ), array('return_rows' => Database::RETURN_ALL_ROWS) ); foreach ($messages as $key => $msg) { // Change message fields encoding $messages[$key]['name'] = myiconv($webim_encoding, "utf-8", $msg['name']); $messages[$key]['message'] = myiconv($webim_encoding, "utf-8", $msg['message']); // Get last message ID if ($msg['id'] > $last_id) { $last_id = $msg['id']; } } return $messages; } /** * Send the messsage * * @param int $kind Message kind. One of the Thread::KIND_* * @param string $message Message body * @param string|null $from Sender name * @param int|null $opid operator id. Use NULL for system messages * @param int|null $time unix timestamp of the send time. Use NULL for current time. * @return int Message ID * * @see Thread::KIND_USER * @see Thread::KIND_AGENT * @see Thread::KIND_FOR_AGENT * @see Thread::KIND_INFO * @see Thread::KIND_CONN * @see Thread::KIND_EVENTS * @see Thread::KIND_AVATAR * @see Thread::getMessages() */ public function postMessage($kind, $message, $from = null, $opid = null, $time = null) { $db = Database::getInstance(); $query = "INSERT INTO {chatmessage} " . "(threadid,ikind,tmessage,tname,agentId,dtmcreated) " . "VALUES (:threadid,:kind,:message,:name,:agentid,:created)"; $values = array( ':threadid' => $this->id, ':kind' => $kind, ':message' => $message, ':name' => ($from ? $from : NULL), ':agentid' => ($opid ? $opid : 0), ':created' => ($time ? $time : time()) ); $db->query($query, $values); return $db->insertedId(); } /** * Close thread and send closing messages to the conversation members * * @param boolean $is_user Boolean TRUE if user initiate thread closing or boolean FALSE otherwise */ public function close($is_user) { $db = Database::getInstance(); // Get messages count list($message_count) = $db->query( "SELECT COUNT(*) FROM {chatmessage} WHERE {chatmessage}.threadid = :threadid AND ikind = :kind_user", array( ':threadid' => $this->id, ':kind_user' => Thread::KIND_USER ), array( 'return_rows' => Database::RETURN_ONE_ROW, 'fetch_type' => Database::FETCH_NUM ) ); // Close thread if it's not already closed if ($this->state != self::STATE_CLOSED) { $this->state = self::STATE_CLOSED; $this->messageCount = $message_count; $this->save(); } // Send message about closing $message = ''; if ($is_user) { $message = getstring2_("chat.status.user.left", array($this->userName), $this->locale); } else { $message = getstring2_("chat.status.operator.left", array($this->agentName), $this->locale); } $this->postMessage(self::KIND_EVENTS, $message); } /** * Assign operator to thread * * @global string $home_locale * @param array $operator Operator who try to take thread * @return boolean Boolean TRUE on success or FALSE on failure */ public function take($operator) { global $home_locale; $take_thread = false; $message = ''; $operator_name = ($this->locale == $home_locale) ? $operator['vclocalename'] : $operator['vccommonname']; if ($this->state == self::STATE_QUEUE || $this->state == self::STATE_WAITING || $this->state == self::STATE_LOADING) { // User waiting $take_thread = true; if ($this->state == self::STATE_WAITING) { if ($operator['operatorid'] != $this->agentId) { $message = getstring2_( "chat.status.operator.changed", array($operator_name, $this->agentName), $this->locale ); } else { $message = getstring2_("chat.status.operator.returned", array($operator_name), $this->locale); } } else { $message = getstring2_("chat.status.operator.joined", array($operator_name), $this->locale); } } elseif ($this->state == self::STATE_CHATTING) { // User chatting if ($operator['operatorid'] != $this->agentId) { $take_thread = true; $message = getstring2_( "chat.status.operator.changed", array($operator_name, $this->agentName), $this->locale ); } } else { // Thread closed return false; } // Change operator and update chat info if ($take_thread) { $this->state = self::STATE_CHATTING; $this->nextAgent = 0; $this->agentId = $operator['operatorid']; $this->agentName = $operator_name; if (empty($this->chatStarted)) { $this->chatStarted = time(); } $this->save(); } // Send message if ($message) { $this->postMessage(self::KIND_EVENTS, $message); $this->postMessage(self::KIND_AVATAR, $operator['vcavatar'] ? $operator['vcavatar'] : ""); } return true; } /** * Change user name in the conversation * * @param string $new_name New user name */ public function renameUser($new_name) { // Rename only if a new name is realy new if ($this->userName != $new_name) { // Save old name $old_name = $this->userName; // Rename user $this->userName = $new_name; $this->save(); // Send message about renaming $message = getstring2_( "chat.status.user.changedname", array($old_name, $new_name), $this->locale ); $this->postMessage(self::KIND_EVENTS, $message); } } } ?>