2023-05-16 10:50:42 +08:00

1484 lines
52 KiB
PHP

<?php
class imMessage extends model
{
/**
* Get message list.
*
* @param string $cgid
* @param array $idList
* @param object $pager
* @param string $startDate
* @param string $type
* @param bool $format
* @param bool $masterOnly
* @param int $userID
* @access public
* @return array
*/
public function getList($cgid = '', $idList = array(), $pager = null, $startDate = '', $type = '', $format = true, $masterOnly = false, $userID = null)
{
if($masterOnly)
{
$tables = array((object)array('tableName' => TABLE_IM_MESSAGE, 'messages' => $idList));
}
else
{
if(!empty($idList)) $tables = $this->getTableByMessages($idList);
if(!empty($startDate)) $tables = $this->getTablesByDateRange($startDate);
if(empty($idList) && empty($startDate)) $tables = $this->getAllTables();
if(empty($tables)) return array();
}
$queries = array();
foreach($tables as $table)
{
$queries[] = $this->dao->select('*')->from($table->tableName)->where('1')
->beginIF(!empty($cgid))->andWhere('cgid')->eq($cgid)->fi()
->beginIF(!empty($idList))->andWhere('id')->in($table->messages)->fi()
->beginIF(!empty($startDate))->andWhere('date')->ge($startDate)->fi()
->beginIF(!empty($type) && strpos($type, '!') === 0)->andWhere('type')->ne(substr($type, 1))->fi()
->beginIF(!empty($type) && strpos($type, '!') !== 0)->andWhere('type')->eq($type)->fi()
->beginIF($userID != null)->andWhere('user')->eq($userID)->fi()
->get();
}
$query = join(' UNION ALL ', $queries);
$sql = $this->dao->select('*')->from(TABLE_IM_MESSAGE);
$sql->sqlobj->sql = 'SELECT * FROM (' . $query . ') as t';
/* Assemble order by and pager stuff. */
$messages = $sql
->orderBy('id_desc')
->beginIF($pager != null)->page($pager)->fi()
->fetchAll();
$messages = $this->decodeMessages($messages);
return $format ? $this->format($messages) : $messages;
}
/**
* Get message list by indexes.
*
* @param string $cgid
* @param array $indexList
* @param object $pager
* @param string $startDate
* @param string $type
* @param bool $format
* @param bool $masterOnly
* @param int $userID
* @access public
* @return array
*/
public function getListByIndexes($cgid = '', $indexList = array(), $pager = null, $startDate = '', $type = '', $format = true, $masterOnly = false, $userID = null)
{
if($cgid == '') return array();
if($masterOnly)
{
$tables = array((object)array('tableName' => TABLE_IM_MESSAGE, 'messages' => $indexList));
}
else
{
if(!empty($indexList)) $tables = $this->getChatTablesByChatIndexes($cgid, $indexList);
if(!empty($startDate)) $tables = $this->getChatTablesByDateRange($cgid, $startDate);
if(empty($indexList) && empty($startDate)) $tables = $this->getChatAllTables($cgid);
if(empty($tables)) return array();
}
$queries = array();
foreach($tables as $table)
{
$queries[] = $this->dao->select('*')->from($table->tableName)->where('1')
->andWhere('cgid')->eq($cgid)
->beginIF(!empty($indexList))->andWhere('`index`')->in($table->messages)->fi()
->beginIF(!empty($startDate))->andWhere('date')->ge($startDate)->fi()
->beginIF(!empty($type) && strpos($type, '!') === 0)->andWhere('type')->ne(substr($type, 1))->fi()
->beginIF(!empty($type) && strpos($type, '!') !== 0)->andWhere('type')->eq($type)->fi()
->beginIF($userID != null)->andWhere('user')->eq($userID)->fi()
->get();
}
$query = join(' UNION ALL ', $queries);
$sql = $this->dao->select('*')->from(TABLE_IM_MESSAGE);
$sql->sqlobj->sql = 'SELECT * FROM (' . $query . ') as t';
/* Assemble order by and pager stuff. */
$messages = $sql
->orderBy('id_desc')
->beginIF($pager != null)->page($pager)->fi()
->fetchAll();
$messages = $this->decodeMessages($messages);
return $format ? $this->format($messages) : $messages;
}
/**
* Get messages ranged from id for user.
*
* @param string $cgid
* @param int $fromID
* @param bool $reverse
* @param int $limit
* @param int $userID
* @param bool $returnID return id list only.
* @access public
* @return array
*/
public function getListAroundIDForUser($cgid, $fromID, $reverse = false, $limit = 50, $userID = 0, $returnID = false)
{
if($fromID == 0 && $reverse) $fromID = PHP_INT_MAX;
$chats = empty($cgid) ? $this->loadModel('im')->chatGetGidListByUserID($userID, true) : array($cgid);
$tables = $this->getTablesByChats($chats);
if(!empty($tables)) $tables = $this->dao->select('tableName')->from(TABLE_IM_MESSAGE_INDEX)->where(1)
->beginIF($reverse)->andWhere('start')->le($fromID)->fi()
->beginIF(!$reverse)->andWhere('end')->ge($fromID)->fi()
->andWhere('tableName')->in($tables)
->fetchAll();
$tables[] = (object)array('tableName' => TABLE_IM_MESSAGE);
$queries = array();
foreach($tables as $table)
{
$queries[] = $this->dao->select($returnID ? 'id' : '*')->from($table->tableName)->where(1)
->beginIF($reverse)->andWhere('id')->le($fromID)->fi()
->beginIF(!$reverse)->andWhere('id')->ge($fromID)->fi()
->andWhere('cgid')->in($chats)
->get();
}
$query = join(' UNION ALL ', $queries);
$sql = $this->dao->select('*')->from(TABLE_IM_MESSAGE);
$sql->sqlobj->sql = 'SELECT * FROM (' . $query . ') as t';
$messages = $sql
->beginIF($reverse)->orderBy('id_desc')->fi()
->beginIF(!$reverse)->orderBy('id')->fi()
->limit($limit)
->fetchAll();
$messages = $this->decodeMessages($messages);
return $returnID ? array_map(function($obj){return (int)$obj->id;}, $messages) : $this->format($messages);
}
/**
* Get chat messages by indexes.
*
* @param string $cgid
* @param array $indexList
* @param bool $reverse
* @param bool $returnID return id list only.
* @access public
* @return array
*/
public function getListAroundIDByIndexes($cgid, $indexList, $reverse = false, $returnID = false)
{
$tables = $this->getChatTablesByChatIndexes($cgid, $indexList);
$queries = array();
foreach($tables as $table)
{
$queries[] = $this->dao->select($returnID ? 'id' : '*')->from($table->tableName)
->where('cgid')->eq($cgid)
->beginIF(!empty($indexList))->andWhere('`index`')->in($table->messages)->fi()
->get();
}
$query = join(' UNION ALL ', $queries);
$sql = $this->dao->select('*')->from(TABLE_IM_MESSAGE);
$sql->sqlobj->sql = 'SELECT * FROM (' . $query . ') as t';
/* Assemble order by and pager stuff. */
$messages = $sql
->beginIF($reverse)->orderBy('id_desc')->fi()
->beginIF(!$reverse)->orderBy('id')->fi()
->fetchAll();
$messages = $this->decodeMessages($messages);
return $returnID ? array_map(function($obj){return (int)$obj->id;}, $messages) : $this->format($messages);
}
/**
* Get last messages of chats.
*
* @param array $cgids
* @access public
* @return array
*/
public function getLast($cgids)
{
$chatMessages = $this->dao->select('gid,lastMessage')->from(TABLE_IM_CHAT)->where('gid')->in($cgids)->fetchPairs();
return $chatMessages ? $this->getList('', array_values($chatMessages)) : array();
}
/**
* Format messages.
*
* @param mixed $messages object | array
* @access public
* @return object | array
*/
public function format($messages)
{
$isObject = false;
if(is_object($messages))
{
$isObject = true;
$messages = array($messages);
}
$messageList = array();
foreach($messages as $message)
{
$message->id = (int)$message->id;
$message->index = (int)$message->index;
$message->user = (int)$message->user;
$message->date = strtotime($message->date);
$message->deleted = isset($message->deleted) ? (bool)$message->deleted : false;
/* Clear content for retracted messages */
if($message->deleted) $message->content = '';
$messageList[$message->gid] = $message;
}
if($isObject) return reset($messageList);
return $messageList;
}
/**
* Create messages.
*
* @param array $messageList
* @param int $userID
* @access public
* @return array
*/
public function create($messageList = array(), $userID = 0)
{
$idList = array();
$chatMessageID = array();
$chatMessageIndex = array();
$now = helper::now();
foreach($messageList as $message)
{
$message = (object) $message;
$msg = $this->dao->select('*')->from(TABLE_IM_MESSAGE)->where('gid')->eq($message->gid)->fetch();
if($msg)
{
if($msg->contentType == 'image' || $msg->contentType == 'file')
{
$message = $this->encodeMessage($message);
$this->dao->update(TABLE_IM_MESSAGE)->set('content')->eq($message->content)->where('id')->eq($msg->id)->exec();
}
$idList[] = $msg->id;
}
elseif(!$msg)
{
if(!(isset($message->user) && $message->user)) $message->user = $userID;
if(!(isset($message->date) && $message->date)) $message->date = $now;
$msgIndex = $this->dao->select('MAX(`index`) + 1')->from(TABLE_IM_MESSAGE)->where('cgid')->eq($message->cgid)->fetch('MAX(`index`) + 1');
if(empty($msgIndex)) $msgIndex = $this->dao->select('`lastMessageIndex` + 1')->from(TABLE_IM_CHAT)->where('gid')->eq($message->cgid)->fetch('`lastMessageIndex` + 1');
if(empty($msgIndex)) $msgIndex = 1;
$message->index = $msgIndex;
$chatMessageIndex[$message->cgid] = $msgIndex;
$this->dao->insert(TABLE_IM_MESSAGE)->data($this->encodeMessage($message))->exec();
$message->id = $this->dao->lastInsertID();
$idList[] = $message->id;
}
if(isset($message->id)) $chatMessageID[$message->cgid] = $message->id;
}
if(empty($idList)) return array();
foreach(array_keys($chatMessageID) as $cgid)
{
$setData = "lastActiveTime = '$now'";
if(isset($chatMessageIndex[$cgid]))
{
$setData .= ", lastMessage = $chatMessageID[$cgid]";
}
if(isset($chatMessageIndex[$cgid]))
{
$setData .= ", lastMessageIndex = $chatMessageIndex[$cgid]";
}
$this->dao->update(TABLE_IM_CHAT)
->set($setData)
->where('gid')->eq($cgid)
->exec();
}
return $this->getList('', $idList);
}
/**
* set message with new content.
* @param $message
* @return void
*/
public function setMessage($message)
{
$msg = $this->encodeMessage($message);
$this->dao->update(TABLE_IM_MESSAGE)->set('content')->eq($msg->content)->where('id')->eq($message->id)->exec();
}
/**
* Get message list by cgid.
*
* @param string|array $cgid
* @param object $pager
* @param string $startDate
* @access public
* @return array
*/
public function getListByCgid($cgid, $pager = null, $startDate = '')
{
if(empty($startDate)) $tables = $this->getAllTables();
if(!empty($startDate)) $tables = $this->getTablesByDateRange($startDate);
if(empty($tables)) return array();
$queries = array();
foreach($tables as $table)
{
$queries[] = $this->dao->select('*')->from($table->tableName)->where('1')
->beginIF(!is_array($cgid))->andWhere('cgid')->eq($cgid)->fi()
->beginIF(is_array($cgid))->andWhere('cgid')->in($cgid)->fi()
->beginIF(!empty($startDate))->andWhere('date')->ge($startDate)->fi()
->get();
}
$query = join(' UNION ALL ', $queries);
$sql = $this->dao->select('*')->from(TABLE_IM_MESSAGE);
$sql->sqlobj->sql = 'SELECT * FROM (' . $query . ') as t';
/* Assemble order by and pager stuff. */
$messages = $sql
->orderBy('id_desc')
->beginIF($pager != null)->page($pager)->fi()
->fetchAll();
return $this->format($this->decodeMessages($messages));
}
/**
* Fetch chat and message information if chat's last read is greater than user's last known.
*
* @param int $lastKnownMessage
* @param int $userID
* @access public
* @return array of chat => lastReadMessage, empty if all last reads are not greater than last known.
*/
public function getMissedByLastKnown($lastKnownMessage, $userID = 0)
{
$missedMessageChats = $this->dao->select('cgid,lastReadMessage')->from(TABLE_IM_CHATUSER)
->where('user')->eq($userID)
->andWhere('lastReadMessage')->gt($lastKnownMessage)
->fetchPairs();
$messageIDs = array();
foreach($missedMessageChats as $cgid => $lastReadID)
{
$chatMessageIDs = $this->getListAroundIDForUser($cgid, $lastReadID, true, 5, $userID, true);
$messageIDs = array_merge($messageIDs, $chatMessageIDs);
}
$unreadIDs = $this->dao->select('message')->from(TABLE_IM_MESSAGESTATUS)
->where('user')->eq($userID)
->andWhere('status')->eq('waiting')
->fetchAll();
$missedIDs = array_diff($messageIDs, array_keys($unreadIDs));
if(empty($missedIDs)) return array();
return $this->getList('', $missedIDs);
}
/**
* Get offline messages.
*
* @param bool $full
* @param int $userID
* @access public
* @return array
*/
public function getOfflineList($full = false, $userID = 0)
{
$messageIDs = $this->dao->select('message')->from(TABLE_IM_MESSAGESTATUS)
->where('user')->eq($userID)
->andWhere('status')->eq('waiting')
->fetchPairs('message');
if(empty($messageIDs)) return array();
if(!$full)
{
$firstRecordID = $this->dao->select('MIN(id)')->from(TABLE_IM_MESSAGE)->fetch('MIN(id)');
$messageIDs = array_filter(
$messageIDs,
function($id) use ($firstRecordID)
{
return $id >= $firstRecordID;
}
);
}
if(empty($messageIDs)) return array();
$messages = $this->getList('', $messageIDs, null, '', '!notify', false);
$messageIDs = array();
foreach($messages as $message) array_push($messageIDs, $message->id);
$this->dao->delete()->from(TABLE_IM_MESSAGESTATUS)
->where('user')->eq($userID)
->andWhere('message')->in($messageIDs)
->exec();
return $this->format($messages);
}
/**
* Get history list.
*
* @param object $user
* @param string $device
* @access public
* @return array
*/
public function getHistoryList($user, $device = 'desktop')
{
$gids = $this->loadModel('im')->chat->getGidListByUserID($user->id);
$startDate = $this->loadModel('setting')->getItem("owner={$user->account}&module=common&section=lastLogin&key={$device}");
if(!empty($startDate) && !empty($gids)) return $this->getListByCgid($gids, null, $startDate);
}
/**
* Create a output of broadcast.
*
* @param string $type
* @param object $chat
* @param array $onlineUsers
* @param int $userID
* @param array $members
* @param bool $setReminders if true, send members and userID as property "reminders" in $message->data.
* @access public
* @return object
*/
public function createBroadcast($type, $chat, $onlineUsers, $userID, $members = array(), $setReminders = false)
{
$adminUsers = array();
$message = new stdclass();
$message->gid = imModel::createGID();
$message->cgid = $chat->gid;
$message->type = 'broadcast';
$message->contentType = 'text';
$message->content = $this->getBroadcastContent($type, $chat, $userID, $members);
$message->date = helper::now();
$message->user = $userID;
if($setReminders)
{
$membersData = array_merge($members, array($userID));
$message->data = json_encode(array('reminders' => $membersData));
}
/* If quit a chat, only send broadcast to the admins or the created user of chat. */
if($type == 'leaveChat')
{
if($chat->admins)
{
if(is_array($chat->admins))
{
$adminUsers = array_map(function($value){
return trim($value);
}, $chat->admins);
}
if(is_string($chat->admins))
{
$adminUsers = explode(',', trim($chat->admins, ','));
}
}
if(!$adminUsers)
{
$user = $this->loadModel('user')->getByAccount($chat->createdBy);
if($user) $adminUsers = array($user->id);
}
$users = $this->loadModel('im')->user->getList($status = 'online', $adminUsers);
$onlineUsers = array_keys($users);
}
/* Save broadcast to im_message. */
$messages = $this->create(array($message), $userID);
$offlineUsers = $this->loadModel('im')->user->getList($status = 'offline', $chat->members);
$this->saveOfflineList($messages, array_keys($offlineUsers));
$output = new stdclass();
$output->method = 'messagesend';
if(dao::isError())
{
$output->result = 'fail';
$output->message = 'Send message failed.';
}
else
{
$output->result = 'success';
$output->users = $onlineUsers;
$output->data = $messages;
}
return $output;
}
/**
* Get content of broadcast.
*
* @param string $type
* @param object $chat
* @param int $userID
* @param array $members
* @access public
* @return string
*/
public function getBroadcastContent($type, $chat, $userID, $members)
{
$user = $this->loadModel('im')->userGetByID($userID);
$userName = empty($user->realname) ? $user->account : $user->realname;
$userMention = "[@$userName](@#$user->id)";
if(stripos($type, 'changeChatOwnership') === 0)
{
$nameInMarkdown = preg_replace('/([#\\`*_{}\[\]\(\)\+\-\.!])/i', '\\\\$1', $chat->name);
return sprintf($this->lang->im->broadcast->$type, $nameInMarkdown, $chat->gid, $userMention);
}
if($type == 'chatMerged')
{
return sprintf($this->lang->im->broadcast->$type, $chat->name, $chat->intoName);
}
if($type == 'mergeChat')
{
return sprintf($this->lang->im->broadcast->$type, $chat->name);
}
if($type == 'renamePrivate')
{
$nameInMarkdown = preg_replace('/([#\\`*_{}\[\]\(\)\+\-\.!])/i', '\\\\$1', $chat->name);
return sprintf($this->lang->im->broadcast->$type, $nameInMarkdown, $chat->gid);
}
if($type == 'createChat' or $type == 'renameChat')
{
$nameInMarkdown = preg_replace('/([#\\`*_{}\[\]\(\)\+\-\.!])/i', '\\\\$1', $chat->name);
return sprintf($this->lang->im->broadcast->$type, $userMention, $nameInMarkdown, $chat->gid);
}
if(in_array($type, array('inviteUser', 'createConferenceInvitation', 'mergeChatWithMembers')))
{
$memberMentions = array();
$membersList = $this->im->userGetList('', $members, true, true);
foreach($membersList as $member)
{
$memberName = empty($member->realname) ? $member->account : $member->realname;
$memberMentions[] = "[@$memberName](@#$member->id)";
}
$memberMentions = implode($this->lang->im->connector, $memberMentions);
if($type == 'mergeChatWithMembers') return sprintf($this->lang->im->broadcast->mergeChatWithMembers, $chat->name, $memberMentions);
return sprintf($this->lang->im->broadcast->$type, $userMention, $memberMentions);
}
if($type == 'archiveChat' or $type == 'unarchiveChat')
{
$nameInMarkdown = preg_replace('/([#\\`*_{}\[\]\(\)\+\-\.!])/i', '\\\\$1', $chat->name);
return sprintf($this->lang->im->broadcast->$type, $userMention, $nameInMarkdown);
}
return sprintf($this->lang->im->broadcast->$type, $userMention);
}
/**
* Retract one message.
*
* @param string $gid
* @param boolean $byAdmin
* @param int $deletedBy
* @access public
* @return array
*/
public function retract($gid = '', $byAdmin = false, $deletedBy = 0)
{
$message = $this->dao->select('id, gid, cgid, `index`, user, date, data, deleted, type, contentType')->from(TABLE_IM_MESSAGE)->where('gid')->eq($gid)->fetch();
$archiveDate = $this->dao->select('archiveDate')->from(TABLE_IM_CHAT)->where('gid')->eq($message->cgid)->fetch('archiveDate');
if($archiveDate !== '0000-00-00 00:00:00') return array();
$messageLife = (strtotime(helper::now()) - strtotime($message->date)) / 60;
if($messageLife <= $this->config->im->retract->validTime && $message->user == $deletedBy)
{
$message->deleted = 1;
$this->dao->update(TABLE_IM_MESSAGE)->set('deleted')->eq($message->deleted)->where('gid')->eq($gid)->exec();
}
else if($byAdmin)
{
$messageData = empty($message->data) ? new stdClass() : json_decode($message->data);
$bySelf = $message->user == $deletedBy;
if(!$bySelf)
{
$messageData->deletedBy = $deletedBy;
$message->data = $messageData;
}
$message->deleted = 1;
if($bySelf) $this->dao->update(TABLE_IM_MESSAGE)->set('deleted')->eq($message->deleted)->where('gid')->eq($gid)->exec();
else $this->dao->update(TABLE_IM_MESSAGE)->set('deleted')->eq($message->deleted)->set('data')->eq(json_encode($messageData))->where('gid')->eq($gid)->exec();
}
return $this->format(array($message));
}
/**
* Save offline messages.
*
* @param array $messages
* @param array $users
* @access public
* @return bool
*/
public function saveOfflineList($messages = array(), $users = array())
{
/* Prevent deleted users from being stored in TABLE_MESSAGESTATUS. */
$deletedUsers = $this->dao->select('id')->from(TABLE_USER)->where('deleted')->eq('1')->fetchPairs();
$users = array_values(array_diff($users, $deletedUsers));
foreach($messages as $message)
{
$this->saveStatus($users, $message->id, 'waiting');
}
return !dao::isError();
}
/**
* Save message status.
*
* @param array $users
* @param int $message
* @param string $status
* @access public
* @return bool
*/
public function saveStatus($users, $message, $status = 'waiting')
{
if(empty($users) || empty($message)) return false;
$queryData = array_map(
function($user) use ($message, $status)
{
return "($user,$message,'$status')";
},
$users
);
$query = "INSERT INTO " . TABLE_IM_MESSAGESTATUS . "(`user`,`message`,`status`) VALUES " . join(',', $queryData) . "ON DUPLICATE KEY UPDATE `status`=`status`";
$this->dao->query($query);
return !dao::isError();
}
/**
* Get notify.
* @access public
* @return array
*/
public function getNotifyList()
{
$onlineUsers = $this->loadModel('im')->user->getList('online');
if(empty($onlineUsers)) return array();
$onlineUsers = array_keys($onlineUsers);
$messageUserPairsData = $this->dao->select('message,user')->from(TABLE_IM_MESSAGESTATUS)
->where('status')->eq('waiting')
->andWhere('user')->in($onlineUsers)
->fetchAll();
if(empty($messageUserPairsData)) return array();
$messageUserPairs = array();
foreach($messageUserPairsData as $data)
{
if(isset($messageUserPairs[$data->message]))
{
$messageUserPairs[$data->message][] = $data->user;
continue;
}
$messageUserPairs[$data->message] = array($data->user);
}
$notifyMessages = $this->getList('', array_keys($messageUserPairs), null, '', 'notify', false);
if(empty($notifyMessages)) return array();
$messageIDs = array();
foreach($notifyMessages as $message) $messageIDs[] = $message->id;
$messageUserPairs = array_intersect_key($messageUserPairs, array_flip($messageIDs));
$notifications = $this->formatNotify($notifyMessages);
$data = array();
$messages = array();
foreach($notifications as $message)
{
foreach($messageUserPairs[$message->id] as $userID)
{
$messages[$userID][] = $message->id;
$data[$userID][] = $message;
}
}
foreach($messages as $userID => $message)
{
$this->dao->delete()->from(TABLE_IM_MESSAGESTATUS)
->where('message')->in($message)
->andWhere('user')->eq($userID)
->exec();
}
return $data;
}
/**
* Get offline notify.
* @param $userID
* @return array
*/
public function getNotifyByUserID($userID)
{
$messageIDs = $this->dao->select('message')->from(TABLE_IM_MESSAGESTATUS)
->where('user')->eq($userID)
->andWhere('status')->eq('waiting')
->fetchPairs('message');
if(empty($messageIDs)) return array();
$messages = $this->getList('', $messageIDs, null, '', 'notify', false);
if(empty($messages)) return array();
$notifications = $this->formatNotify($messages);
$messages = array();
foreach($notifications as $message) $messages[] = $message->id;
$this->dao->delete()->from(TABLE_IM_MESSAGESTATUS)
->where('message')->in($messages)
->andWhere('user')->eq($userID)
->exec();
return $notifications;
}
/**
* Foramt messages for notify.
* @param object $messages
* @access public
* @return array
*/
public function formatNotify($messages)
{
$notifications = array();
foreach($messages as $message)
{
$data = new stdClass();
$messageData = json_decode($message->data);
$data->id = $message->id;
$data->gid = $message->gid;
$data->cgid = $message->cgid;
$data->type = $message->type;
$data->content = $message->deleted ? '' : $message->content;
$data->date = strtotime($message->date);
$data->contentType = $message->contentType;
$data->title = $messageData->title;
$data->subtitle = $messageData->subtitle;
$data->url = $messageData->url;
$data->actions = $messageData->actions;
$data->sender = $messageData->sender;
$data->users = $messageData->target;
if($data->cgid != 'notification' && !empty($message->index)) $data->index = $message->index;
$notifications[] = $data;
}
return $notifications;
}
/**
* create a bot welcome message.
* @param int $userID
* @param bool $needUpdate
* @return void
*/
public function createXuanbotWelcomeNotify($userID, $needUpdate = false)
{
$sender = new stdclass();
$sender->id = 0;
$sender->name = $this->lang->im->bot->commonName;
$sender->avatar = commonModel::getSysURL() . $this->config->webRoot . 'data/image/xuanbot.png';
if($needUpdate) $this->createNotify(array($userID), $this->lang->im->bot->upgradeWelcome->title, '', $this->lang->im->bot->upgradeWelcome->content, 'text', $this->lang->im->bot->upgradeWelcome->link, array(), $sender);
$this->createNotify(array($userID), $this->lang->im->bot->welcome->title, '', $this->lang->im->bot->welcome->content, 'text', $this->lang->im->bot->welcome->link, array(), $sender);
}
/**
* Insert message for notify.
* @param string $target
* @param string $title
* @param string $subtitle
* @param string $content
* @param string $contentType
* @param string $url
* @param array $actions
* @param int $sender
* @access public
* @return bool
*/
public function createNotify($target = '', $title = '', $subtitle = '', $content = '', $contentType = 'text', $url = '', $actions = array(), $sender = 0)
{
/* Check if target is a chat gid or a list of users. */
if(is_array($target))
{
$cgid = 'notification';
}
else
{
$cgid = $target;
$target = $this->dao->select('user')->from(TABLE_IM_CHATUSER)
->where('cgid')->eq($target)
->andWhere('quit')->eq('0000-00-00 00:00:00')
->fetchPairs('user');
}
$users = $this->loadModel('im')->user->getList('', $target);
$info = array();
$info['title'] = $title;
$info['subtitle'] = $subtitle;
$info['url'] = $url;
$info['actions'] = $actions;
$info['sender'] = $sender;
$info['target'] = array_keys($users);
$notify = new stdClass();
$notify->gid = imModel::createGID();
$notify->cgid = $cgid;
$notify->user = 0;
$notify->date = helper::now();
$notify->type = 'notify';
$notify->content = $content;
$notify->contentType = $contentType;
$notify->data = json_encode($info);
$msgIndex = $this->dao->select('MAX(`index`) + 1')->from(TABLE_IM_MESSAGE)->where('cgid')->eq($cgid)->fetch('MAX(`index`) + 1');
if(empty($msgIndex)) $msgIndex = $this->dao->select('`lastMessageIndex` + 1')->from(TABLE_IM_CHAT)->where('gid')->eq($cgid)->fetch('`lastMessageIndex` + 1');
if(empty($msgIndex)) $msgIndex = 1;
$notify->index = $msgIndex;
$this->dao->insert(TABLE_IM_MESSAGE)->data($this->encodeMessage($notify))->exec();
$message = $this->dao->lastInsertID();
$this->dao->update(TABLE_IM_CHAT)
->set('lastActiveTime')->eq(helper::now())
->set('lastMessage')->eq($message)
->set('lastMessageIndex')->eq($msgIndex)
->where('gid')->eq($cgid)->exec();
$this->saveStatus($info['target'], $message, 'waiting');
return !dao::isError();
}
/**
* Add offline messages according to the gid of messages that failed to be sent.
* @param array $sendfail
* @access public
* @return bool
*/
public function sendFailures($sendfail = array())
{
foreach($sendfail as $userID => $gid)
{
if(empty($gid)) continue;
$idList = $this->dao->select('id')->from(TABLE_IM_MESSAGE)->where('gid')->in($gid)->fetchPairs();
$messages = $this->getList('', $idList);
$this->saveOfflineList($messages, array($userID));
}
return !dao::isError();
}
/**
* Get message count for block.
*
* @access public
* @return object
*/
public function getCountForBlock()
{
$masterTableTotal = $this->dao->select('COUNT(*)')->from(TABLE_IM_MESSAGE)->where('deleted')->eq('0')->fetch('COUNT(*)');
$partitionsTotal = $this->dao->select('SUM(`count`)')->from(TABLE_IM_CHAT_MESSAGE_INDEX)->fetch('SUM(`count`)');
$dayCount = $this->dao->select("SUM(deleted = '0' AND date > '".date('Y-m-d H:i', strtotime('-1 day'))."')")->from(TABLE_IM_MESSAGE)->fetchPairs();
$hourCount = $this->dao->select("SUM(deleted = '0' AND date > '".date('Y-m-d H:i', strtotime('-1 hour'))."')")->from(TABLE_IM_MESSAGE)->fetchPairs();
$count = new stdClass();
$count->total = $masterTableTotal + ($partitionsTotal || 0);
$count->day = reset($dayCount);
$count->hour = reset($hourCount);
if(empty($count->total))
{
$count->total = 0;
$count->day = 0;
$count->hour = 0;
}
return $count;
}
/**
* Get all message tables.
*
* @access public
* @return array
*/
public function getAllTables()
{
$tables = $this->dao->select('tableName')->from(TABLE_IM_MESSAGE_INDEX)->fetchAll();
foreach($tables as $key => $table) $tables[$key]->messages = '';
$master = new stdclass;
$master->tableName = TABLE_IM_MESSAGE;
$master->messages = '';
$tables[] = $master;
return $tables;
}
/**
* Get all message tables with cgid.
*
* @param string $cgid
* @access public
* @return array
*/
public function getChatAllTables($cgid = '')
{
if($cgid == '') return array();
$tables = $this->dao->select('DISTINCT tableName')->from(TABLE_IM_CHAT_MESSAGE_INDEX)->fetchAll();
foreach($tables as $key => $table) $tables[$key]->messages = '';
$master = new stdclass;
$master->tableName = TABLE_IM_MESSAGE;
$master->messages = '';
$tables[] = $master;
return $tables;
}
/**
* Get message table names by message IDs.
*
* @param array $messageIDs
* @access public
* @return array
*/
public function getTableByMessages($messageIDs)
{
$tables = array();
$indices = $this->dao->select('tableName,start,end')->from(TABLE_IM_MESSAGE_INDEX)->fetchAll('tableName');
$processedIDs = array();
foreach($indices as $index)
{
$min = $index->start;
$max = $index->end;
$ids = array_filter(
$messageIDs,
function($id) use ($min, $max)
{
return $id >= $min && $id <= $max;
}
);
if(!empty($ids))
{
$result = new stdclass();
$result->tableName = $index->tableName;
$result->messages = $ids;
$tables[$index->tableName] = $result;
$processedIDs = array_merge($processedIDs, $ids);
}
}
$unindexed = array_diff($messageIDs, $processedIDs);
if(!empty($unindexed))
{
$result = new stdclass();
$result->tableName = TABLE_IM_MESSAGE;
$result->messages = $unindexed;
$tables[TABLE_IM_MESSAGE] = $result;
}
return $tables;
}
/**
* Get chat message table names by message indexes.
*
* @param string $cgid
* @param array $indexes
* @access public
* @return array
*/
public function getChatTablesByChatIndexes($cgid, $indexes)
{
if($cgid == '') return array();
$indexIds = $indexes;
$tables = array();
$indices = $this->dao->select('tableName,startIndex,endIndex')->from(TABLE_IM_CHAT_MESSAGE_INDEX)->where('gid')->eq($cgid)->fetchAll('tableName');
$processedIDs = array();
foreach($indices as $index)
{
$min = $index->startIndex;
$max = $index->endIndex;
$ids = array_filter(
$indexes,
function($id) use ($min, $max)
{
return $id >= $min && $id <= $max;
}
);
$indexes = array_diff($indexes, $ids);
if(!empty($ids))
{
$result = new stdclass();
$result->tableName = $index->tableName;
$result->messages = $ids;
$tables[$index->tableName] = $result;
$processedIDs = array_merge($processedIDs, $ids);
}
}
$unindexed = array_diff($indexIds, $processedIDs);
if(!empty($unindexed))
{
$result = new stdclass();
$result->tableName = TABLE_IM_MESSAGE;
$result->messages = $unindexed;
$tables[TABLE_IM_MESSAGE] = $result;
}
return $tables;
}
/**
* Get tables by start (and / or) end date).
*
* @param string $startDate
* @param string $endDate
* @access public
* @return array
*/
public function getTablesByDateRange($startDate = '', $endDate = '')
{
$tables = $this->dao->select('tableName,startDate,endDate')->from(TABLE_IM_MESSAGE_INDEX)
->where('1')
->beginIF(!empty($startDate))->andWhere('endDate')->ge($startDate)->fi()
->beginIF(!empty($endDate))->andWhere('startDate')->le($endDate)->fi()
->fetchAll('tableName');
if(empty($tables)) $appendMaster = true;
/* If endDate is even later than the max endDate we have in the index, append the master table. */
elseif(!empty($endDate))
{
$maxEndDate = max(array_map(
function($t)
{
return $t->endDate;
},
$tables
));
if($maxEndDate < $endDate) $appendMaster = true;
}
if(isset($appendMaster))
{
$master = new stdclass();
$master->tableName = TABLE_IM_MESSAGE;
$master->startDate = isset($maxEndDate) ? $maxEndDate : '0000-00-00 00:00:00';
$master->endDate = '9999-12-31 23:59:59';
$tables[] = $master;
}
return $tables;
}
/**
* Get chat tables by start (and / or) end date).
*
* @param string $cgid
* @param string $startDate
* @param string $endDate
* @access public
* @return array
*/
public function getChatTablesByDateRange($cgid = '', $startDate = '', $endDate = '')
{
if($cgid == '') return array();
$tables = $this->dao->select('tableName,startDate,endDate')->from(TABLE_IM_CHAT_MESSAGE_INDEX)
->where('gid')->eq($cgid)
->beginIF(!empty($startDate))->andWhere('endDate')->ge($startDate)->fi()
->beginIF(!empty($endDate))->andWhere('startDate')->le($endDate)->fi()
->fetchAll('tableName');
if(empty($tables)) $appendMaster = true;
/* If endDate is even later than the max endDate we have in the index, append the master table. */
elseif(!empty($endDate))
{
$maxEndDate = max(array_map(
function($t)
{
return $t->endDate;
},
$tables
));
if($maxEndDate < $endDate) $appendMaster = true;
}
if(isset($appendMaster))
{
$master = new stdclass();
$master->tableName = TABLE_IM_MESSAGE;
$master->startDate = isset($maxEndDate) ? $maxEndDate : '0000-00-00 00:00:00';
$master->endDate = '9999-12-31 23:59:59';
$tables[] = $master;
}
return $tables;
}
/**
* Get tables by gids of chats.
*
* @param array|string $cgids
* @access public
* @return array
*/
public function getTablesByChats($cgids)
{
return $this->dao->select('DISTINCT tableName')->from(TABLE_IM_CHAT_MESSAGE_INDEX)
->where('gid')->in($cgids)
->fetchPairs();
}
/**
* Mark an ongoing partition opreation.
*
* @param bool $ongoing
* @access public
* @return bool true if successfully marked, otherwise there already is another ongoing opreation.
*/
public function markOngoingPartition($ongoing)
{
$this->loadModel('setting');
$lastStatus = $this->setting->getItem("owner=system&module=common&section=partition&key=ongoing");
if($ongoing != $lastStatus)
{
$this->setting->setItem('system.common.partition.ongoing', $ongoing);
return true;
}
return false;
}
/**
* Check if message count in the master table exceeds twice the partition size.
*
* @access public
* @return bool true if partition is needed.
*/
public function needPartition()
{
$currentID = $this->dao->select('id')->from(TABLE_IM_MESSAGE)->orderBy('id_desc')->limit(1)->fetch('id');
$lastIndexID = $this->dao->select('end')->from(TABLE_IM_MESSAGE_INDEX)->orderBy('end_desc')->limit(1)->fetch('end');
return $currentID > ($lastIndexID + 2 * $this->config->im->partition->messagePerTable);
}
/**
* Backup master message table.
*
* @param int $fromID
* @param int $toID
* @access public
* @return int count of message affected.
*/
public function backupMasterTable($fromID, $toID = 0)
{
$insertStmt = $this->dao->insert(TABLE_IM_MESSAGE_BACKUP)->get();
$selectStmt = $this->dao->select('*')->from(TABLE_IM_MESSAGE)
->where('id')->ge($fromID)
->beginIF(!empty($toID))->andWhere('id')->le($toID)->fi()
->get();
$stmt = substr($insertStmt, 0, -4) . $selectStmt;
return $this->dao->exec($stmt);
}
/**
* Delete a large amount of rows from im_message table
* by selecting the rest into a new table and replace current table with the new one.
*
* @param int $end
* @param int $start
* @access public
* @return bool
*/
public function deleteFromMasterTable($end, $start = 0)
{
$tmpTable = str_replace(array('`', '-'), '', sprintf("%s_%s", TABLE_IM_MESSAGE, 'tmp_' . helper::today()));
$oldTable = str_replace(array('`', '-'), '', sprintf("%s_%s", TABLE_IM_MESSAGE, 'old_' . helper::today()));
$zdb = $this->app->loadClass('zdb');
$fields = $zdb->getTableFields(TABLE_IM_MESSAGE);
$zdb->createTable($tmpTable, $fields);
$insertStmt = $this->dao->insert($tmpTable)->get();
$selectStmt = $this->dao->select('*')->from(TABLE_IM_MESSAGE)
->where('id')->gt($end)
->beginIF($start)->orWhere('id')->lt($start)->fi()
->get();
$stmt = substr($insertStmt, 0, -4) . $selectStmt;
$this->dao->exec($stmt);
$renameQuery = 'RENAME TABLE ' . TABLE_IM_MESSAGE . ' TO ' . $oldTable . '; ';
$renameQuery .= 'RENAME TABLE ' . $tmpTable . ' TO ' . TABLE_IM_MESSAGE . '; ';
$this->dao->exec($renameQuery);
$dropQuery = "DROP TABLE $oldTable;";
$this->dao->exec($dropQuery);
return !dao::isError();
}
/**
* Partition master message table into smaller tables.
*
* @access public
* @return bool
*/
public function partitionTable()
{
/* Check if creation of a new partition is needed. */
if(!$this->needPartition()) return false;
/* Set memory limit to avoid OOMs. */
ini_set('memory_limit', '256M');
/* Start a transaction. */
$this->dao->begin();
/* Create a new message table. */
$zdb = $this->app->loadClass('zdb');
$fields = $zdb->getTableFields(TABLE_IM_MESSAGE);
$fields['id']->extra = ''; // disable auto_increment.
$currentTable = $this->dao->select('id,end')->from(TABLE_IM_MESSAGE_INDEX)->orderBy('id_desc')->limit(1)->fetch();
$newTable = str_replace('`', '', sprintf("%s_%s", TABLE_IM_MESSAGE, ++$currentTable->id));
$zdb->createTable($newTable, $fields);
/* Select messages and insert into the new table. */
$insertStmt = $this->dao->insert($newTable)->get();
$selectStmt = $this->dao->select('*')->from(TABLE_IM_MESSAGE)
->where('id')->gt($currentTable->end)
->andWhere('id')->le($currentTable->end + $this->config->im->partition->messagePerTable)
->get();
$stmt = substr($insertStmt, 0, -4) . $selectStmt;
$this->dao->exec($stmt);
/* Index the new table. */
$this->reindex($newTable);
/* Backup the master table. */
$this->backupMasterTable($currentTable->end + 1, $currentTable->end + $this->config->im->partition->messagePerTable);
/* Delete messages from master table on success. */
$this->deleteFromMasterTable($currentTable->end + $this->config->im->partition->messagePerTable);
/* End the transaction. */
$this->dao->commit();
return !dao::isError();
}
/**
* Index partitions.
*
* @param string $table table to index
* @access public
* @return void
*/
public function reindex($table = '')
{
$MAXID = 'MAX(id)';
$MINID = 'MIN(id)';
$MAXINDEX = 'MAX(`index`)';
$MININDEX = 'MIN(`index`)';
$IDDATE = 'id,date';
$messageMeta = new stdclass();
$firstRecord = $this->dao->select($IDDATE)->from($table)->orderBy('id')->limit(1)->fetch();
$lastRecord = $this->dao->select($IDDATE)->from($table)->orderBy('id_desc')->limit(1)->fetch();
$chats = $this->dao->select('DISTINCT(cgid)')->from($table)->fetchPairs();
$messageMeta->tableName = $table;
$messageMeta->start = $firstRecord->id;
$messageMeta->end = $lastRecord->id;
$messageMeta->startDate = $firstRecord->date;
$messageMeta->endDate = $lastRecord->date;
$messageMeta->chats = ',' . join(',', $chats) . ',';
$this->dao->insert(TABLE_IM_MESSAGE_INDEX)->data($messageMeta)->exec();
$chatsInfo = $this->dao->select('cgid,MAX(id),MIN(id),MAX(`index`),MIN(`index`),count(*)')->from($table)->groupBy('cgid')->fetchAll();
$messages = array();
foreach($chatsInfo as $info)
{
$messages[] = $info->{$MAXID};
$messages[] = $info->{$MINID};
}
$messages = array_unique($messages);
$messageDates = $this->dao->select($IDDATE)->from($table)->where('id')->in($messages)->fetchPairs('id');
$values = array();
foreach($chatsInfo as $info)
{
$meta = array();
$meta[] = $table;
$meta[] = $info->cgid;
$meta[] = $info->{$MINID};
$meta[] = $info->{$MAXID};
$meta[] = $info->{$MININDEX};
$meta[] = $info->{$MAXINDEX};
$meta[] = $messageDates[$info->{$MINID}];
$meta[] = $messageDates[$info->{$MAXID}];
$meta[] = $info->{'count(*)'};
$data = "('" . join("','", $meta) . "')";
$values[] = $data;
}
$insertStmt = $this->dao->insert(TABLE_IM_CHAT_MESSAGE_INDEX)->get();
$insertStmt = substr($insertStmt, 0, -4) . '(`tableName`,`gid`,`start`,`end`,`startIndex`,`endIndex`,`startDate`,`endDate`,`count`) VALUES ' . join(',', $values);
$this->dao->exec($insertStmt);
}
/**
* codec string with rot47.
*
* @access public
* @param string $str string to be rot47
* @return string
*/
public function codecWithRot47($str)
{
return strtr($str, '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~', 'PQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO');
}
/**
* decode text with rot47 & base64.
*
* @access public
* @param string $text text to be rot47
* @return string
*/
public function decodeText($text)
{
$rot47Encoded = $this->codecWithRot47($text);
return base64_decode($rot47Encoded);
}
/**
* decode chat messages.
*
* @access public
* @param array $messages messages to be decode
* @return array
*/
public function decodeMessages($messages)
{
if(isset($this->config->xuanxuan->messageEncrypt) && ($this->config->xuanxuan->messageEncrypt == 'on') && commonModel::isLicensedMethod('im', 'messageEncrypt'))
{
return array_map(function($msg) {
if(isset($msg->content) && isset($this->config->xuanxuan->lastUnEncryptMessageId) && intval($this->config->xuanxuan->lastUnEncryptMessageId) <= $msg->id)
{
$msg->content = $this->decodeText($msg->content);
}
return $msg;
}, $messages);
}
return $messages;
}
/**
* decode one chat message.
*
* @access public
* @param object $message message to be decode
* @return string
*/
public function decodeMessage($message)
{
if(isset($this->config->xuanxuan->messageEncrypt) && ($this->config->xuanxuan->messageEncrypt == 'on') && commonModel::isLicensedMethod('im', 'messageEncrypt') && isset($this->config->xuanxuan->lastUnEncryptMessageId) && intval($this->config->xuanxuan->lastUnEncryptMessageId) <= $message->id)
{
$message->content = $this->decodeText($message->content);
}
return $message;
}
/**
* encode one chat message.
*
* @access public
* @param object $message message to be encode
* @return object
*/
public function encodeMessage($message)
{
if(isset($this->config->xuanxuan->messageEncrypt) && ($this->config->xuanxuan->messageEncrypt == 'on') && commonModel::isLicensedMethod('im', 'messageEncrypt'))
{
$message->content = $this->codecWithRot47(base64_encode($message->content));
}
return $message;
}
/**
* get last message id
*
* @access public
* @return int
*/
public function getLastMessageId()
{
$lastMessageId = $this->dao->select('id')
->from(TABLE_IM_MESSAGE)
->orderBy('id desc')
->limit(1)
->fetch('id');
if(!$lastMessageId)
{
$messagesCount = $this->dao->select('COUNT(*)')->from(TABLE_IM_MESSAGE)->fetch('COUNT(*)');
if($messagesCount == 0)
{
$messagesIdxCount = $this->dao->select('COUNT(*)')->from(TABLE_IM_MESSAGE_INDEX)->fetch('COUNT(*)');
if($messagesIdxCount > 0)
{
$lastMessageId = $this->dao->select('MAX(end)')->from(TABLE_IM_MESSAGE_INDEX)->fetch('MAX(end)');
}
else
{
$lastMessageId = 0;
}
}
}
return $lastMessageId;
}
}