name = '禅道'; if($_SESSION['clientLang'] == 'zh-tw') $this->name = '禪道'; } } /** * Init bot, load model. * * @access public * @return void */ public function init() { $this->lang = $this->im->lang->im->bot->zentaoBot; foreach(array('view', 'start', 'close', 'finish') as $command) { $this->commands[] = array('command' => $command, 'alias' => $this->lang->commands->$command->alias, 'description' => $this->lang->commands->$command->description, 'internal' => true); } /* Backup user model of im module, load user module and restore. Use `$this->userModel` as user module from now on. */ $imUser = $this->im->user; $this->userModel = $this->im->loadModel('user'); $this->im->user = $imUser; $this->im->loadModel('task'); $this->im->app->loadClass('pager', $static = true); $this->users = array_filter($this->userModel->getPairs('noclosed|noletter')); $this->taskStatusList = array_filter($this->im->lang->task->statusList); $this->help = $this->lang->help; $this->inited = true; } /** * Close command. * * @param array $args bot command arguments * @access public * @return object|string */ public function close($args = array()) { $args = $this->removeEqualSign($args); foreach($args as $key => $value) { if(in_array(strtolower($value), $this->lang->condKeywords['task'])) unset($args[$key]); } $task = $this->closeTask($args); return $task; } /** * Finish command. * * @param array $args bot command arguments * @access public * @return object|string */ public function finish($args = array()) { $originArgs = $args; $args = array_filter(explode('=', implode('=', $args))); foreach($args as $key => $value) { if(in_array(strtolower($value), $this->lang->condKeywords['task'])) unset($args[$key]); } return $this->finishTask($args, $originArgs); } /** * View command. * * @param array $args bot command arguments * @param int $userID * @param object $user * @access public * @return object|string */ public function view($args = array(), $userID = 0, $user = null) { $pager = pager::init(0, 10, 1); $pageSearchRegex = $this->lang->pageSearchRegex; $pageIDKeywords = $this->lang->condKeywords['pageID']; $args = array_filter($args, function($arg) use ($pager, $pageSearchRegex, $pageIDKeywords) { $matches = array(); preg_match($pageSearchRegex, $arg, $matches); if(count($matches) > 2) { if(in_array(strtolower($matches[1]), $pageIDKeywords)) { $pager->pageID = $matches[2]; } else { $pager->recPerPage = $matches[2]; } return false; } return true; }); $originArgs = $args; $args = $this->removeEqualSign($args); foreach($args as $key => $value) { if(in_array(strtolower($value), $this->lang->condKeywords['task'])) { unset($args[$key]); $tasks = $this->viewTask($args, $user, $pager); return $this->renderTask($tasks, $originArgs, $pager); } } } /** * Parse bot command arguments. * * @param array $args bot command arguments * @param array $keys * @access public * @return object */ public function parseArguments($args = array(), $keys = array()) { $assignedToList = ''; $priList = ''; $statusList = ''; $idList = ''; $taskName = ''; $comment = ''; while(!empty($args)) { $arg = array_shift($args); if(isset($keys['pri'])) { if(in_array(strtolower($arg), $this->lang->condKeywords['pri'])) { $pris = array_shift($args); $priList = $this->buildCondParamByModifier($pris, $priList, 'p'); continue; } if(preg_match('/p[1-4]/i', $arg) == 1) { $priList = $this->buildCondParamByModifier($arg, $priList, 'p'); continue; } } if(isset($keys['id'])) { if(in_array(strtolower($arg), $this->lang->condKeywords['id'])) { $ids = array_shift($args); $idList .= $this->buildCondParamByModifier($ids, $idList, '#'); continue; } if(preg_match('/#\d+/', $arg) == 1) { $idList .= $this->buildCondParamByModifier($arg, $idList, '#'); continue; } } if(isset($keys['status'])) { if(in_array(strtolower($arg), $this->lang->condKeywords['status'])) { $statuses = array_shift($args); $statusList = $this->buildCondParam($statuses, $statusList, $this->taskStatusList); continue; } if($this->checkCondType($arg, $this->taskStatusList)) { $statusList = $this->buildCondParam($arg, $statusList, $this->taskStatusList); continue; } } if(isset($keys['assignTo'])) { if(in_array(strtolower($arg), $this->lang->condKeywords['assignTo'])) { $usernames = array_shift($args); $assignedToList = $this->buildCondParam($usernames, $assignedToList, $this->users); continue; } if($this->checkCondType($arg, $this->users)) { $assignedToList = $this->buildCondParam($arg, $assignedToList, $this->users); continue; } } if(isset($keys['comment'])) { if(in_array(strtolower($arg), $this->lang->condKeywords['comment'])) { $comment = array_shift($args); } else { $comment = $arg; } continue; } if(isset($keys['taskName'])) { if(in_array(strtolower($arg), $this->lang->condKeywords['taskName'])) { $taskName = array_shift($args); } else { $taskName = $arg; } } } $conds = new stdClass(); if(isset($keys['pri'])) $conds->priList = trim($priList, ','); if(isset($keys['assignTo'])) $conds->assignedToList = trim($assignedToList, ','); if(isset($keys['status'])) $conds->statusList = trim($statusList, ','); if(isset($keys['id'])) $conds->idList = trim($idList, ','); if(isset($keys['taskName'])) $conds->taskName = $taskName; if(isset($keys['comment'])) $conds->comment = $comment; return $conds; } /** * Check condition type. * * @param string $arg * @param array $list * @access public * @return bool */ public function checkCondType($arg, $list) { $args = explode(',', $arg); $array_inter = array_intersect($args, $list); if(!empty($array_inter)) return true; $list = array_flip($list); $array_inter = array_intersect($args, $list); if(!empty($array_inter)) return true; return false; } /** * Build condition param. * * @param string $arg bot command arguments * @param string $condParam * @param array $condArr * @access public * @return string */ public function buildCondParam($arg, $condParam, $condArr) { $arr = explode(',', $arg); foreach($arr as $cond) { if(array_key_exists($cond, $condArr)) { $condParam .= ',' . $cond; } else { $key = array_search($cond, $condArr); if($key !== false) $condParam .= ',' . $key; } } return $condParam; } /** * Remove equal sign from bot command arguments. * * @param array $args bot command arguments * @access public * @return array */ public function removeEqualSign($args) { return array_filter(explode('=', implode('=', $args))); } /** * Build condition param by modifier. * * @param array $args bot command arguments * @param string $condParam * @param string $modifier * @access public * @return string */ public function buildCondParamByModifier($arg, $condParam, $modifier) { $arg = strtolower($arg); $arg = str_replace($modifier, '', $arg); $condParam .= ',' . $arg; return $condParam; } /** * View task command. * * @param array $args bot command arguments * @param object $user * @param object $pager * @access public * @return array */ public function viewTask($args = array(), $user = null, $pager = null) { $keys = array(); foreach(array('pri', 'id', 'status', 'assignTo', 'taskName') as $key) $keys[$key] = true; if(empty($args)) { $args['assignTo'] = is_object($user) ? $user->account : false; $args['status'] = 'wait,doing,done,pause,cancel'; } $conds = $this->parseArguments($args, $keys); $conds->search = 1; $conds->order = 'status_asc'; $conds->limit = 10; if(is_object($pager)) { $conds->page = $pager->pageID; $conds->limit = $pager->recPerPage; } $result = $this->loadEntry('tasks', 'get', array(), (array)$conds); if(isset($result->tasks)) { $pager->setRecTotal($result->total); $pager->setRecPerPage($result->limit); $pager->setPageID($result->page); $pager->setPageTotal(); return $result->tasks; } return array(); } /** * Close task command. * * @param array $args bot command arguments * @access public * @return object|string */ public function closeTask($args) { $keys = array(); $keys['id'] = true; $keys['comment'] = true; $conds = $this->parseArguments($args, $keys); $tasks = array_filter(explode(',', $conds->idList)); $taskID = array_pop($tasks); $comment = $conds->comment; if(!is_numeric($taskID)) return $this->lang->errors->invalidCommand; $task = $this->loadEntry('task', 'get', array('taskID'=> $taskID)); if(empty($task)) return $this->lang->errors->emptyResult; if($task->status != 'done' && $task->status != 'cancel') return sprintf($this->lang->errors->invalidStatus, $this->taskStatusList[$task->status]); $task = $this->loadEntry('taskclose', 'post', array('taskID' => $taskID, 'comment' => $comment)); if(isset($task->result) && $task->result == 'fail') return $task->message; $link = str_replace('x.php', 'index.php', helper::createLink('task', 'view', "taskID=$taskID", 'html')); $messages = new stdClass(); $messages->type = 'url'; $messages->url = common::getSysURL() . $link; return array($this->lang->success, $messages); } /** * Finish task. * * @param array $args * @param array $originArgs * @access public * @return object|string */ private function finishTask($args, $originArgs) { $taskID = ''; $params = array(); while(!empty($args)) { $arg = array_shift($args); if(in_array(strtolower($arg), $this->lang->condKeywords['id'])) { $taskID = array_shift($args); $taskID = str_replace('#', '', $taskID); } elseif(preg_match('/#\d+/', $arg) == 1) { $taskID = str_replace('#', '', $arg); } else { parse_str(str_replace('amp;', '', end($originArgs)), $params); } } if(!is_numeric($taskID)) return $this->lang->errors->invalidCommand; $task = $this->loadEntry('task', 'get', array('taskID'=> $taskID)); if(!common::hasPriv('task', 'finish')) return $this->lang->errors->unauthorized; if(empty($task) || isset($task->error)) return $this->lang->errors->emptyResult; if($task->status != 'wait' && $task->status != 'doing') return sprintf($this->lang->errors->invalidStatus, $this->taskStatusList[$task->status]); $reply = new stdClass(); $reply->messages = array(); $reply->responses = array(); $realStarted = $this->formatDate($task, 'realStarted'); $finishedDate = $this->formatDate($task, 'finishedDate'); if(empty($params)) { $originCommand = rawurlencode($this->lang->finishCommand . ' ' . implode(' ', $originArgs)); $json = (object)array ( 'title' => "#{$taskID} {$task->name}", 'submitLabel' => $this->im->lang->task->finish, 'inputs' => array( (object)array ( 'name' => $this->im->lang->task->hasConsumed, 'type' => 'input', 'label' => $this->im->lang->task->hasConsumed, 'value' => $task->consumed, 'disabled' => true, 'addon' => $this->im->lang->task->hour, ), (object)array ( 'name' => $this->im->lang->task->currentConsumed, 'type' => 'input', 'label' => $this->im->lang->task->currentConsumed, 'value' => 0, 'addon' => $this->im->lang->task->hour, ), (object)array ( 'name' => $this->im->lang->task->realStarted, 'type' => 'datetime-local', 'label' => $this->im->lang->task->realStarted, 'value' => $realStarted, ), (object)array ( 'name' => $this->im->lang->task->finishedDate, 'type' => 'datetime-local', 'label' => $this->im->lang->task->finishedDate, 'value' => $finishedDate, ), ), ); $json = rawurlencode(json_encode($json)); $reply->messages[] = $this->lang->finish->tip; $reply->messages[] = "[{$this->lang->finish->tipLinkTitle}](xxc://openFormAndSendToServerBySendbox/$json/$originCommand)"; } else { $messageID = isset($params['messageId']) ? $params['messageId'] : ''; unset($params['messageId']); $currentConsumed = 0; foreach($params as $key => $value) { if(in_array(strtolower($key), $this->lang->condKeywords['realStarted'])) $realStarted = $value; if(in_array(strtolower($key), $this->lang->condKeywords['finishedDate'])) $finishedDate = $value; if(in_array(strtolower($key), $this->lang->condKeywords['currentConsumed'])) $currentConsumed = $value; }; if(empty($realStarted) || empty($finishedDate)) return ''; $params = array ( 'currentConsumed' => $currentConsumed, 'realStarted' => $realStarted, 'finishedDate' => $finishedDate, ); $task = $this->loadEntry('taskfinish', 'post', array_merge(array('taskID' => $taskID, 'assignedTo' => '', 'comment' => ''), $params)); if(isset($task->error)) { foreach($task->error as $error) $reply->messages[] = is_array($error) ? implode(',', $error): $error; return $reply; } $finishedDate = $this->formatDate($task, 'finishedDate', '', 'Y-m-d H:i:s'); $reply->messages[] = sprintf($this->lang->finish->done, $taskID, $finishedDate, $task->consumed); $reply->messages[] = $this->renderTaskTable(array($task)); $reply->responses[] = $this->disableMarkdownLinkInMessage($messageID); if(!empty($task->fromBug)) { $reply->messages[] = sprintf($this->lang->finish->bugTip, $taskID); $link = commonModel::getSysURL() . str_replace('x.php', 'index.php', helper::createLink('bug', 'view', "bugID={$task->fromBug}", 'html')); $href = urlencode($link); $reply->messages[] = "[{$this->lang->finish->bugTipLinkTitle}](xxc:openInApp/zentao-integrated/{$href})"; } } return $reply; } /** * Format date. * * @param object $task * @param string $field * @param string $default * @param string $format * @return string */ private function formatDate($task, $field, $default = '', $format = 'Y-m-d\TH:i') { try { $datetime = new DateTime(empty($task->$field) ? 'now' : $task->$field, new DateTimeZone('UTC')); $datetime->setTimezone(new DateTimeZone(date_default_timezone_get())); return $datetime->format($format); } catch(Exception $e) { return $default; } } /** * Render tasks as table html. * * @param array $tasks * @param array $originArgs * @param pager $pager * @return string|array */ public function renderTask($tasks, $originArgs, $pager) { $lang = $this->im->lang; $taskCount = $pager->recTotal ? $pager->recTotal : count($tasks); if($taskCount === 0) return $lang->task->noTask; if($taskCount == 1) { $task = current($tasks); $link = str_replace('x.php', 'index.php', helper::createLink('task', 'view', "taskID=$task->id", 'html')); $messages = new stdclass(); $messages->type = 'url'; $messages->url = common::getSysURL() . $link; return array(sprintf($this->lang->tasksFound, $taskCount), $messages); } $messages = array(); if($pager->pageID == 1) $messages[] = sprintf($this->lang->tasksFound, $taskCount); $taskTable = $this->renderTaskTable($tasks); $originCommand = $this->lang->viewCommand . ' ' . implode(' ', $originArgs); $paging = "$pager->pageID / $pager->pageTotal"; if($pager->pageID != 1) { $previousPageCommand = $originCommand . " {$this->lang->condKeywords['pageID'][1]}=" . ($pager->pageID - 1) . " {$this->lang->condKeywords['recPerPage'][1]}=" . $pager->recPerPage; $previousPageCommand = rawurlencode($previousPageCommand); $paging .= " [{$this->lang->prevPage}](xxc://sendContentToServerBySendbox/{$previousPageCommand})"; } if($pager->pageID != $pager->pageTotal) { $nextPageCommand = $originCommand . " {$this->lang->condKeywords['pageID'][1]}=" . ($pager->pageID + 1) . " {$this->lang->condKeywords['recPerPage'][1]}=" . $pager->recPerPage; $nextPageCommand = rawurlencode($nextPageCommand); $paging .= " [{$this->lang->nextPage}](xxc://sendContentToServerBySendbox/{$nextPageCommand})"; } $paging = "
$paging
"; $messages[] = $taskTable . $paging; return $messages; } /** * Render tasks as markdown table. * * @param array $tasks * @access public * @return void */ public function renderTaskTable($tasks) { $lang = $this->im->lang; $headMap = array('id'=>'ID', 'pri' => $lang->task->pri, 'name' => $lang->task->name, 'assignedTo' => $lang->task->assignedTo, 'status' => $lang->task->status, 'estimate' => $lang->task->estimateAB, 'consumed' => $lang->task->consumedAB, 'left' => $lang->task->leftAB, 'actions' => $lang->actions); $thead = ''; foreach($headMap as $value) $thead .= "{$value}"; $thead = "{$thead}"; $tbody = ''; foreach($tasks as $task) { $tr = ''; $link = str_replace('x.php', 'index.php', helper::createLink('task', 'view', "taskID=$task->id", 'html')); $href = urlencode(common::getSysURL() . $link); $isParent = $task->parent == -1; $isChild = $task->parent > 0; $isMulti = !empty($task->mode); foreach($headMap as $key => $value) { switch($key) { case 'name': $tr .= ""; if($isParent) $tr .= " {$lang->task->parentAB} "; if($isChild) $tr .= " {$lang->task->childrenAB} "; if($isMulti) $tr .= " " . zget($lang->task->modeList, $task->mode) . " "; $tr .= "{$task->$key}"; $tr .= ""; break; case 'status': $tr .= "{$this->taskStatusList[$task->$key]}"; break; case 'assignedTo': if($task->mode == 'multi') { $tr .= "{$lang->task->team}"; } else if($task->$key == 'closed') { $tr .= "Closed"; } else { $tr .= "" . (empty($task->assignedTo) ? $lang->task->noAssigned : (is_object($task->assignedTo) ? $task->assignedTo->realname : $task->assignedTo)) . ""; } break; case 'actions': $startUrl = 'xxc://sendContentToServerBySendbox/' . "{$this->lang->startCommand} #$task->id"; $finishUrl = 'xxc://sendContentToServerBySendbox/' . "{$this->lang->finishCommand} #$task->id"; $closeUrl = 'xxc://sendContentToServerBySendbox/' . "{$this->lang->closeCommand} #$task->id"; $canStart = in_array($task->status, array('wait', 'pause')); $canFinish = in_array($task->status, array('wait', 'pause', 'doing')); $canClose = in_array($task->status, array('done', 'cancel')); /* Disable all if task has children. */ if($isParent) { $canStart = false; $canFinish = false; $canClose = false; } $tr .= '
'; $tr .= $canStart ? "" : "
"; $tr .= $canFinish ? "" : "
"; $tr .= $canClose ? "" : "
"; $tr .= '
'; break; case 'estimate': case 'consumed': case 'left': $tr .= "{$task->$key}h"; break; default: $tr .= "{$task->$key}"; } } $tbody .= "{$tr}"; } return "{$thead}{$tbody}
"; } /** * Start task command. * * @param array $args bot command arguments * @param int $userID * @access public * @return object|string */ public function start($args = array(), $userID = 0) { $originArgs = $args; foreach($args as $key => $value) { if(in_array(strtolower($value), $this->lang->condKeywords['task'])) { unset($args[$key]); } } if(count($args) == 0) return $this->lang->errors->taskIDRequired; $taskID = 0; foreach($args as $key => $value) { if(strpos($value, '#') !== false) { $id = str_replace('#', '', $value); if($id && is_numeric($id)) { $taskID = $id; unset($args[$key]); } else { if($args[$key + 1] && is_numeric($args[$key + 1])) { $taskID = $args[$key + 1]; unset($args[$key]); unset($args[$key + 1]); } } } } if(!$taskID) return $this->lang->errors->taskIDRequired; $task = $this->loadEntry('task', 'get', array('taskID' => $taskID)); if(!$task) return $this->lang->errors->taskNotFound; if($task->status != 'wait') return sprintf($this->lang->errors->invalidStatus, $this->taskStatusList[$task->status]); $reply = new stdClass(); $reply->messages = array(); $reply->responses = array(); if(count($args) == 0) { $originCommand = rawurlencode($this->lang->startCommand . ' ' . implode(' ', $originArgs)); $realStarted = $this->formatDate($task, 'realStarted'); $json = (object)array ( 'title' => "#{$taskID} {$task->name}", 'submitLabel' => $this->im->lang->task->start, 'inputs' => array ( (object)array ( 'name' => $this->im->lang->task->realStarted, 'type' => 'datetime-local', 'label' => $this->im->lang->task->realStarted, 'value' => $realStarted, ), (object)array ( 'name' => $this->im->lang->task->consumed, 'type' => 'number', 'label' => $this->im->lang->task->consumed, 'value' => $task->consumed, 'addon' => $this->im->lang->task->hour, ), (object)array ( 'name' => $this->im->lang->task->left, 'type' => 'number', 'label' => $this->im->lang->task->left, 'value' => $task->left, 'addon' => $this->im->lang->task->hour, 'helpText' => $this->lang->start->finishWithZeroLeft, ) ), ); $json = rawurlencode(json_encode($json)); $reply->messages[] = sprintf($this->lang->start->tip, $taskID); $reply->messages[] = "[{$this->lang->start->tipLinkTitle}](xxc://openFormAndSendToServerBySendbox/$json/$originCommand)"; } else { $hasFormArgs = false; $left = $task->left; $consumed = $task->consumed; $realStarted = $task->realStarted; foreach($args as $arg) { if(strpos($arg, 'messageId')) { $hasFormArgs = true; $formArgs = explode('&', $arg); $replyArgs = array(); foreach($formArgs as $formArg) { $item = explode('=', $formArg); if(count($item) == 2) { $replyArgs[$item[0]] = $item[1]; } } foreach($replyArgs as $key => $value) { if(in_array(strtolower($key), $this->lang->condKeywords['consumed'])) $consumed = $value; if(in_array(strtolower($key), $this->lang->condKeywords['realStarted'])) $realStarted = $value; if(in_array(strtolower($key), $this->lang->condKeywords['left'])) $left = $value; } if($consumed == 0 && $left == 0) return $this->im->lang->task->noticeTaskStart; $task = $this->loadEntry('taskStart', 'post', array ( 'taskID' => $taskID, 'left' => $left, 'realStarted' => $realStarted, 'consumed' => $consumed, )); if(isset($task->result) and $task->result == 'fail') { $reply->messages[] = $task->message; } elseif(isset($task->error)) { foreach($task->error as $error) $reply->messages[] = is_array($error) ? implode(',', $error): $error; } else { $reply->messages[] = sprintf($this->lang->effortRecorded, $taskID); $reply->messages[] = $this->renderTaskTable(array($task)); $reply->responses[] = $this->disableMarkdownLinkInMessage($replyArgs['messageId']); } } } if(!$hasFormArgs) { while(!empty($args)) { $arg = array_shift($args); if(in_array(strtolower($arg), $this->lang->condKeywords['left'])) { $left = array_shift($args); } elseif(in_array(strtolower($arg), $this->lang->condKeywords['consumed'])) { $consumed = array_shift($args); } elseif(in_array(strtolower($arg), $this->lang->condKeywords['realStarted'])) { $realStarted = array_shift($args); } } $task = $this->loadEntry('taskStart', 'post', array ( 'taskID' => $taskID, 'left' => $left, 'realStarted' => $realStarted, 'consumed' => $consumed, )); if($task->result and $task->result == 'fail') { $reply->messages[] = $task->message; } elseif(isset($task->error)) { foreach($task->error as $error) $reply->messages[] = is_array($error) ? implode(',', $error): $error; } else { $reply->messages[] = sprintf($this->lang->effortRecorded, $taskID); $reply->messages[] = $this->renderTaskTable(array($task)); } } } return $reply; } /** * Remove a Markdown link in the message and generate a response. * * @param $messageID * @access public * @return object */ private function disableMarkdownLinkInMessage($messageID) { if(!$messageID) return null; $message = $this->im->message->getList('', array($messageID)); $message = current($message); if($message->user != 'xuanbot') return null; $message->content = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $message->content); $this->im->message->setMessage($message); $userID = array_filter(explode('&', $message->cgid), function($item) { return is_numeric($item); }); $response = new stdclass(); $response->result = 'success'; $response->method = 'messageUpdate'; $response->users = $userID; $response->data = array($message); return $response; } /** * Used to call zentao's API. * * @param string $entry * @param string $action * @param array $params * @param array $query * @param string $version * @access public * @return object */ public function loadEntry($entry, $action, $params = array(), $query = array(), $version = 'v1') { try { $this->im->loadModel('user'); $user = $this->userModel->getByID($this->im->app->input['userID'], 'id'); /* Authorize him and save to session. */ $user->rights = $this->userModel->authorize($user->account); $user->groups = $this->userModel->getGroups($user->account); $user->view = $this->userModel->grantUserView($user->account, $user->rights['acls'], $user->rights['projects']); $user->admin = strpos($this->im->app->company->admins, ",{$user->account},") !== false; global $app; $app->action = $action; $app->user = $user; include_once($this->im->app->appRoot . "framework/api/entry.class.php"); include_once($this->im->app->appRoot . "api/{$version}/entries/" . strtolower($entry) . ".php"); $entryName = $entry . 'Entry'; $entry = new $entryName(); if($action == 'post' || $action == 'put') { $entry->requestBody = (object)$params; } elseif($action == 'get' && !empty($query)) { $entry->setParam($query); } $content = call_user_func_array(array($entry, $action), $params); return json_decode($content); } catch(EndResponseException $endResponseException) { return $endResponseException->getContent(); } } }