/** * @args {"description": "Delay: Clean offline client and helpdesk every minute"} */ public function perform() { $accounts = Account::findAll([]); foreach ($accounts as $account) { $accountId = $account->_id; $setting = HelpDeskSetting::findOne(['accountId' => $accountId]); if (!empty($setting)) { $maxWaitTime = $setting->maxWaitTime; // Close timeout conversation $chatConversations = ChatConversation::findAll(['accountId' => $accountId, 'status' => ChatConversation::STATUS_OPEN, 'lastChatTime' => ['$lt' => TimeUtil::msTime(time() - $maxWaitTime * 60)]]); foreach ($chatConversations as $chatConversation) { HelpDesk::disconnect($chatConversation->_id, ['type' => 'brake']); } // Delete timeout pending client $pendingClients = PendingClient::findAll(['accountId' => $accountId, 'lastPingTime' => ['$lt' => TimeUtil::msTime(time() - PendingClient::PING_THRESHOLD)]]); foreach ($pendingClients as $pendingClient) { $pendingClient->delete(); } // Clean offline helpdesk $cache = Yii::$app->cache; $onlineHelpDesks = $cache->get(HelpDesk::CACHE_PREFIX_HELPDESK_PING . $accountId); if (!empty($onlineHelpDesks)) { foreach ($onlineHelpDesks as $deskId => $lastPingTime) { if ($lastPingTime < TimeUtil::msTime(time() - HelpDesk::PING_THRESHOLD)) { HelpDesk::leave($deskId, $accountId, ['type' => 'droping']); } } } } } }
public function getCreatorDetail() { if (!empty($this->origin) && $this->origin !== IssueUser::HELPDESK) { return $this->hasOne(IssueUser::className(), ['_id' => 'creator']); } return $this->hasOne(HelpDesk::className(), ['_id' => 'creator']); }
/** * Set helpdesk notification type */ public function actionSetNotificationType() { $helpdesks = HelpDesk::findAll([]); if (!empty($helpdesks)) { foreach ($helpdesks as $helpdesk) { if (!isset($helpdesk->notificationType)) { $helpdesk->notificationType = HelpDesk::NOTIFICATION_TYPE_DESKTOP_MARK; if ($helpdesk->save()) { echo "Success to set notification type to helpdesk " . $helpdesk->_id . "\n"; } else { echo "Fail to set notification type to helpdesk " . $helpdesk->_id . " successfully" . "\n"; } } } } }
/** * This method is used to push message. * If mobile help-desk client is on backend, push message. */ public function pushMessage($deskId, $eventType, $extra, $message = null) { $cache = \Yii::$app->cache; $deskStrId = $deskId . ''; //Update the helpdesk unread message count $count = $cache->get(ConversationController::UNREAD_COUNT_PREFIX . $deskStrId); empty($count) && ($count = 0); $desk = HelpDesk::findByPk($deskId); if (empty($desk->deviceToken) || empty($desk->environment)) { LogUtil::info(['push' => 'missing device token', 'desk' => $deskId, 'eventType' => $eventType, 'extra' => $extra]); } else { if ($eventType == ConversationController::EVENT_CHAT_MESSAGE && $this->isPushMessage($deskId, $desk->accountId)) { $cache->set(ConversationController::UNREAD_COUNT_PREFIX . $deskStrId, ++$count); } else { //no code here, push state like 'sessionConnected', no need to add $count } $extra['type'] = $eventType; $target = [$desk->environment => [$desk->deviceToken]]; \Yii::$app->tuisongbao->pushMessage($target, $count, $extra, $message); } }
public function actionRemoveTag() { $result = ['status' => 'ok']; $params = $this->getParams(); $accountId = $this->getAccountId(); if (empty($params['tagName'])) { throw new BadRequestHttpException("Tag name can not be empty"); } if (empty($params['helpdeskIds'])) { throw new BadRequestHttpException("Helpdesk id can not be empty"); } $tagName = $params['tagName']; $helpdeskIds = $params['helpdeskIds']; if (!empty($helpdeskIds)) { $helpdeskMongoIds = []; foreach ($helpdeskIds as $helpdeskId) { $helpdeskMongoIds[] = new \MongoId($helpdeskId); } HelpDesk::removeTag($tagName, $helpdeskMongoIds); } return $result; }
public function getUser() { $userId = $this->getUserId(); return HelpDesk::findOne(['_id' => $userId]); }
/** * Used for tuisongbao web hook, realtime engine will call the service when user subscribe or unsubscribe a channel * @return string json status */ public function actionUserState() { $body = $this->getParams(); $headers = Yii::$app->request->getHeaders(); $signature = hash_hmac('sha256', json_encode($body), TUISONGBAO_SECRET); LogUtil::info(['body' => $body, 'signature' => $signature], 'webhook'); // Check whether it is called by the tuisongbao web hook if ($signature === $headers['X-Engine-Signature']) { //TODO: First event may not be the correct one $time = $body['timestamp']; $event = $body['events'][0]; $userId = $event['userId']; $eventName = $event['name']; //Tip: Only handle the user_removed event on global channel here if (strpos($event['channel'], ChatConversation::CHANNEL_GLOBAL) !== false && ChatConversation::MONGOID_LENGTH === strlen($userId)) { $helpdesk = HelpDesk::findByPk(new \MongoId($userId)); if (!empty($helpdesk)) { if ($eventName == ChatConversation::EVENT_USER_ADDED || $eventName == ChatConversation::EVENT_USER_REMOVED) { $updateTimePrefix = 'wm-update-time'; $cache = Yii::$app->cache; $lastTime = $cache->get($updateTimePrefix . $userId); // failed events will be send again, but timestamp will not change // if a event is failed and other event has success, skip that failed event // Event_1. user_add failed; Event_2, user_remove successed; Event_1. user_add successed // skip Event_1. user_add successed if (empty($lastTime) || $lastTime < $time) { $cache->set($updateTimePrefix . $userId, $time); HelpDesk::cacheOnlineDesks($helpdesk->_id . '', $helpdesk->accountId . '', $eventName); } } } } } }
/** * Reset password */ public function actionResetPassword() { $code = $this->getParams('code'); $newPassword = $this->getParams('password'); $result = Validation::validateCode($code); if ($result == Validation::LINK_INVALID) { throw new BadRequestHttpException(Yii::t('common', 'link_invalid')); } else { if ($result == Validation::LINK_EXPIRED) { throw new BadRequestHttpException(Yii::t('common', 'link_expired')); } } $userId = $result; $user = HelpDesk::findByPk($userId); if (empty($user)) { throw new BadRequestHttpException(Yii::t('commmon', 'incorrect_userid')); } // update the user password $user->password = HelpDesk::encryptPassword($newPassword, $user->salt); if (!$user->save()) { throw new ServerErrorHttpException("Save user failed!"); } return ['status' => 'ok']; }
/** * Get the latest documents of the pending clients. * The clients should ping the server in the threshold or sourced from WeChat. * @param Integer $count the count of the elements to be dequeued * @return array the client. */ public static function deQueue($deskId, $count = 1) { $helpdesk = HelpDesk::findOne($deskId); $condition = ['$or' => [['lastPingTime' => ['$gt' => TimeUtil::msTime(time() - self::PING_THRESHOLD)]], ['source' => ['$in' => [ChatConversation::TYPE_WECHAT, ChatConversation::TYPE_WEIBO, ChatConversation::TYPE_ALIPAY]]]], 'accountId' => $helpdesk->accountId]; if (!empty($helpdesk->tags)) { $condition['tags'] = ['$in' => $helpdesk->tags]; $pendingClients = self::find()->where($condition)->orderBy(['requestTime' => SORT_ASC])->limit($count)->all(); if (count($pendingClients) < $count) { $condition['tags'] = ['$nin' => $helpdesk->tags]; $excludeTagClients = self::find()->where($condition)->orderBy(['requestTime' => SORT_ASC])->limit($count - count($pendingClients))->all(); $pendingClients = array_merge_recursive($pendingClients, $excludeTagClients); } } else { $pendingClients = self::find()->where($condition)->orderBy(['requestTime' => SORT_ASC])->limit($count)->all(); } $clients = []; foreach ($pendingClients as $pendingClient) { $client = ['nick' => $pendingClient->nick, 'avatar' => $pendingClient->avatar, 'openId' => $pendingClient->openId, 'source' => $pendingClient->source, 'sourceChannel' => $pendingClient->sourceChannel]; if ($pendingClient->accountInfo) { $client['accountInfo'] = $pendingClient->accountInfo; } $clients[] = $client; $pendingClient->delete(); } return $clients; }
public function getAssigneeDetail() { return $this->hasOne(HelpDesk::className(), ['_id' => 'assignee']); }
public static function saveRecord($desk, $client, $status, $accountId) { $chatConversation = new ChatConversation(); $channelName = self::getChannelName($desk->_id, $client['openId']); $chatConversation->conversation = $channelName; $chatConversation->status = $status; $chatConversation->desk = ['id' => $desk->_id, 'badge' => $desk->badge, 'email' => $desk->email, 'avatar' => $desk->avatar, 'name' => $desk->name]; $chatConversation->client = $client; $chatConversation->accountId = $accountId; $chatConversation->lastChatTime = TimeUtil::msTime(); if (!$chatConversation->save()) { LogUtil::error(['message' => 'save chat conversation failed', 'error' => $chatConversation->errors], 'helpdesk'); throw new ServerErrorHttpException('save chatConversation failed'); } else { HelpDesk::incClientCount($desk->_id); } return $chatConversation; }
public function perform() { $args = $this->args; $message = $args['message']; $type = $message['msgType']; $content = $message['content']; $WEConnectAccountInfo = $args['account']; $WEConnectUserInfo = $args['user']; $accountId = new \MongoId($args['accountId']); $accountInfoType = null; $source = null; switch ($WEConnectAccountInfo['channel']) { case 'WEIBO': $source = ChatConversation::TYPE_WEIBO; $accountInfoType = 'WEIBO'; break; case 'ALIPAY': $source = ChatConversation::TYPE_ALIPAY; $accountInfoType = 'ALIPAY'; break; case 'WEIXIN': $source = ChatConversation::TYPE_WECHAT; $accountInfoType = $WEConnectAccountInfo['accountType']; break; default: throw new BadRequestHttpException("Unsupported channel type"); break; } $client = ['nick' => $WEConnectUserInfo['nickname'], 'avatar' => $WEConnectUserInfo['headerImgUrl'], 'openId' => $WEConnectUserInfo['id'], 'originId' => $WEConnectUserInfo['originId'], 'source' => $source, 'sourceChannel' => $WEConnectUserInfo['accountId'], 'accountId' => $accountId, 'accountInfo' => ['type' => $accountInfoType, 'name' => $WEConnectAccountInfo['name']]]; ResqueUtil::log(['message' => 'get message from wechat', 'WEConnectAccountInfo' => $WEConnectAccountInfo, 'WEConnectUserInfo' => $WEConnectUserInfo, 'accountId' => $accountId]); if (empty($type) || empty($content)) { ResqueUtil::log(['message' => 'missing required fields', 'senario' => 'accepting messages from WeConnect', 'WEConnectAccountInfo' => $WEConnectAccountInfo, 'WEConnectUserInfo' => $WEConnectUserInfo]); HelpDesk::sendSystemReplyByType($client, $accountId, HelpDeskSetting::REPLY_ERROR); return; } try { if ('EVENT' === $type) { switch ($content) { case 'CONNECT': $isInWorkingHour = HelpDeskSetting::isInWorkingHours($accountId); if ($isInWorkingHour) { if (empty($WEConnectAccountInfo)) { ResqueUtil::log(['message' => 'Account parameters missing', 'senario' => 'Wechat message, event connect']); HelpDesk::sendSystemReplyByType($client, $accountId, HelpDeskSetting::REPLY_ERROR); return; } //check if the wechat enduser has connected to a helpdesk $conversation = ChatConversation::findOpenByClientId($WEConnectUserInfo['id'], $accountId); if (!empty($conversation)) { ResqueUtil::log(['message' => 'Have connect to helpdesk already', 'senario' => 'Wechat message, event connect', 'user' => $WEConnectUserInfo, 'accountId' => $accountId]); HelpDesk::sendSystemReplyByType($client, $accountId, HelpDeskSetting::REPLY_CUSTOM, ChatConversation::NO_DUPLICATE_CLIENT); return; } return HelpDesk::connect($client, $accountId); } else { //send the disconnect event to WeConnect Yii::$app->weConnect->sendCustomerServiceMessage($WEConnectUserInfo['id'], $WEConnectUserInfo['accountId'], ['msgType' => ChatConversation::WECHAT_MESSAGE_TYPE_EVENT, 'content' => 'DISCONNECT']); HelpDesk::sendSystemReplyByType($client, $accountId, HelpDeskSetting::REPLY_NONWORKING); } break; case 'DISCONNECT': //get the conversationId $conversation = ChatConversation::findOpenByClientId($WEConnectUserInfo['id'], $accountId, ['type' => 'left']); if (empty($conversation)) { return ['status' => 'ok']; } //disconnect return HelpDesk::disconnect($conversation->_id, ['type' => 'brake']); break; default: throw new BadRequestHttpException("Unsupported event content type"); break; } } else { //get the conversation information $conversation = ChatConversation::findOpenByClientId($WEConnectUserInfo['id'], $accountId); if (empty($conversation)) { $pendingClient = PendingClient::findOne(['openId' => $WEConnectUserInfo['id'], 'accountId' => $accountId]); if (!empty($pendingClient)) { HelpDesk::sendSystemReplyByType($client, $accountId, HelpDeskSetting::REPLY_WAITTING); return ['status' => 'ok']; } else { return HelpDesk::connect($client, $accountId); } } $desk = $conversation->desk; $desk['id'] = (string) $desk['id']; $client = $conversation->client; $sentTime = TimeUtil::msTime(); //trigger send message event $name = ChatConversation::EVENT_CHAT_MESSAGE; $data = ['conversationId' => (string) $conversation->_id, 'desk' => $desk, 'client' => $client, 'chatMessage' => ['content' => ['msgType' => $type, 'body' => $content], 'sentTime' => $sentTime], 'isReply' => false]; //add chatMessage record $chatMessage = ChatMessage::saveRecord(['msgType' => $type, 'body' => $content], $sentTime, false, $conversation->_id, $accountId); //helpDesk web client message $channels = [ChatConversation::getChannelName($desk['id'], $client['openId'])]; $data['messageId'] = (string) $chatMessage->_id; Yii::$app->tuisongbao->triggerEvent($name, $data, $channels); //helpDesk mobile client message $pushExtra = ['messageId' => (string) $chatMessage->_id, 'openId' => $client['openId'], 'conversationId' => (string) $conversation->_id, 'sentTime' => $chatMessage->sentTime]; $pushMessage = empty($client['nick']) ? $content : $client['nick'] . ':' . $content; ChatConversation::pushMessage($desk['id'], ChatConversation::EVENT_CHAT_MESSAGE, $pushExtra, $pushMessage); return ['status' => 'ok']; } } catch (Exception $e) { LogUtil::error(['message' => $e->getMessage()], 'helpdesk'); return HelpDesk::sendSystemReplyByType($client, $account, HelpDeskSetting::REPLY_ERROR); } }
public function actionCheckAuth() { $secret = TUISONGBAO_SECRET; $socketId = $this->getParams('socketId'); $channelName = $this->getParams('channelName'); $authData = $this->getParams('authData'); //helpdesk is saved as authData $parts = explode(':', $authData); //If it is the helpdesk $clientId = $parts[1]; $userData = ['userId' => $clientId, 'userInfo' => []]; if ('h' === $parts[0]) { $client = HelpDesk::findByPk(new \MongoId($clientId)); $userData['userInfo'] = ['badge' => $client->badge, 'email' => $client->email]; } $userDataJsonStr = json_encode($userData); $strToSign = $socketId . ':' . $channelName . ':' . $userDataJsonStr; LogUtil::info(['strToSign' => $strToSign, 'secret' => $secret], 'signature'); $signature = hash_hmac('sha256', $strToSign, $secret); LogUtil::info(['signature' => $signature, 'channelData' => $userDataJsonStr], 'signature'); $result = ['signature' => $signature, 'channelData' => $userDataJsonStr]; header("Content-Type:application/json"); echo json_encode($result); }
/** * Activate a new user * * <b>Request Type</b>: POST<br/><br/> * <b>Request Endpoint</b>:http://{server-domain}/site/update-info<br/><br/> * <b>Content-type</b>: application/json<br/><br/> * <b>Summary</b>: This api is used for a user to activate account * <br/><br/> * * <b>Request Params</b>:<br/> * name: string, the user name, required<br/> * password: string, the user password, required<br/> * id: string, the user id, required<br/> * avatar: string, the user avatar, required<br/> * code: string, the user validation code, required<br/> * <br/><br/> * * <b>Response Params:</b><br/> * ack: integer, mark the create result, 0 means create successfully, 1 means create fail<br/> * data: array, json array to describe user id<br/> * <br/><br/> * * <b>Request Example:</b><br/> * <pre> * { * "name" : "sarazhang", * "password" : "45345345gdfgdf", * "id" : "643hfjht567", * "avatar" : "http://www.baidu.com/1.jpg", * "code" : "543gfdg45745sd", * * } * </pre> * <br/><br/> * * <b>Response Example</b>:<br/> * <pre> * { * 'ack' : 1, * 'data': {"id": "5345gdfg45745"} * } * </pre> */ public function actionUpdateInfo() { $data = $this->getParams(); if (empty($data['password']) || empty($data['name']) || empty($data['id']) || $data['password'] === md5('')) { throw new BadRequestHttpException(Yii::t('common', 'parameters_missing')); } $code = empty($data['code']) ? '' : $data['code']; $type = empty($data['type']) ? '' : $data['type']; $result = Validation::validateCode($code, false); if ($result == Validation::LINK_INVALID) { throw new GoneHttpException(Yii::t('common', 'link_invalid')); } else { if ($result == Validation::LINK_EXPIRED) { throw new GoneHttpException(Yii::t('common', 'link_invalid')); } } $salt = StringUtil::rndString(6); $password = User::encryptPassword($data['password'], $salt); $name = $data['name']; $avatar = $data['avatar']; $id = $data['id']; if (!empty($type) && $type == self::ACCOUNT_INVITATION) { $user = User::findOne(['_id' => $id]); $accountId = $user->accountId; if (empty(User::getByName($accountId, $name))) { $user->isActivated = User::ACTIVATED; $user->salt = $salt; $user->language = Yii::$app->language; $user->password = $password; $user->name = $name; $user->avatar = $avatar; $flag = $user->save(); } else { throw new InvalidParameterException(['name' => Yii::t('common', 'name_exist')]); } } else { if (!empty($type) && $type == self::HELPDESK_INVITATION) { $helpDesk = HelpDesk::findOne(['_id' => $id]); $accountId = $helpDesk->accountId; if (empty(HelpDesk::getByName($accountId, $name))) { $helpDesk->isActivated = User::ACTIVATED; $helpDesk->language = Yii::$app->language; $helpDesk->salt = $salt; $helpDesk->password = $password; $helpDesk->name = $name; $helpDesk->avatar = $avatar; $flag = $helpDesk->save(); } else { throw new InvalidParameterException(['name' => Yii::t('common', 'name_exist')]); } } } if ($flag) { Validation::deleteAll(['code' => $code]); return ['id' => $id, 'type' => $type]; } throw new ServerErrorHttpException('activate fail'); }
/** * HelpDesk offline * @param string $deskId * @param MongoId $accountId * @param Object $extra * @return array */ public static function leave($deskId, $accountId, $extra) { $cache = Yii::$app->cache; //remove it from conversation caches $conversations = $cache->get('conversations' . $accountId); unset($conversations[$deskId]); $cache->set('conversations' . $accountId, $conversations); //remove it from online caches self::cacheOnlineDesks($deskId, (string) $accountId, ChatConversation::EVENT_USER_REMOVED); //remove the helpdesk ping time $onlineHelpDesks = $cache->get(HelpDesk::CACHE_PREFIX_HELPDESK_PING . $accountId); unset($onlineHelpDesks[$deskId]); $cache->set(HelpDesk::CACHE_PREFIX_HELPDESK_PING . $accountId, $onlineHelpDesks); //get the conversation records in db according to the deskId $conversationInstances = ChatConversation::findOpenByDeskId(new \MongoId($deskId), $accountId); foreach ($conversationInstances as $conversationInstance) { $clientTemp = $conversationInstance->client; $desk = $conversationInstance->desk; //trigger deskLeft event $data = ['conversationId' => (string) $conversationInstance['_id'], 'desk' => $desk, 'client' => $clientTemp, 'extra' => $extra, 'sentTime' => TimeUtil::msTime()]; $channels = [ChatConversation::getChannelName($deskId, $clientTemp['openId'])]; Yii::$app->tuisongbao->triggerEvent(ChatConversation::EVENT_DESK_LEFT, $data, $channels); //set the status of the chatConversation to "closed" ChatConversation::closeById($conversationInstance['_id']); HelpDesk::sendSystemReplyByType($clientTemp, $accountId, isset($extra['type']) ? $extra['type'] : HelpDeskSetting::REPLY_CLOSE); } //flush the client count in db helpDesk::flushClientCount(new \MongoId($deskId)); LogUtil::info(['event' => 'deskLeft', 'deskId' => $deskId], 'helpdesk'); return ['status' => 'ok']; }