bot.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. <?php
  2. class imBot extends model
  3. {
  4. /**
  5. * Bot code list.
  6. *
  7. * @var array
  8. * @access public
  9. */
  10. public $bots = array();
  11. /**
  12. * Bot instance map.
  13. *
  14. * @var object
  15. * @access public
  16. */
  17. public $botInstances;
  18. /**
  19. * Inited bot list.
  20. *
  21. * @var array
  22. * @access public
  23. */
  24. public $initedBots = array();
  25. /**
  26. * Load and create bot instances.
  27. *
  28. * @param string $appName
  29. * @param object $im imModel instance
  30. * @access public
  31. * @return void
  32. */
  33. public function __construct($appName, $im)
  34. {
  35. parent::__construct($appName);
  36. /* Save imModel for further uses. */
  37. $this->im = $im;
  38. /* Load bot classes and init. */
  39. $modulePaths = $im->app->getModuleExtPath('im', 'bot');
  40. $modulePath = array_key_exists('xuan', $modulePaths) ? $modulePaths['xuan'] : current($modulePaths);
  41. helper::import($modulePath . 'xuanbot.php');
  42. $botFiles = helper::ls($modulePath, '*.bot.php');
  43. foreach($botFiles as $botFile) helper::import($botFile);
  44. $botClasses = array_filter(get_declared_classes(), function($className)
  45. {
  46. return is_subclass_of($className, xuanBot::className);
  47. });
  48. $this->botInstances = new stdclass();
  49. foreach($botClasses as $botClass)
  50. {
  51. $vars = get_class_vars($botClass);
  52. if(empty($vars['code']) || !preg_match('/^[a-zA-Z]\\w*$/', $vars['code'])) continue;
  53. $code = $vars['code'];
  54. $this->bots[] = $code;
  55. /* Init bots with im module. */
  56. $bot = new $botClass();
  57. $this->botInstances->$code = $bot;
  58. $this->botInstances->$code->im = $im;
  59. }
  60. }
  61. /**
  62. * Init a bot.
  63. *
  64. * @param string $botCode
  65. * @access public
  66. * @return bool
  67. */
  68. public function initBot($botCode)
  69. {
  70. if(!in_array($botCode, $this->bots)) return false;
  71. if(in_array($botCode, $this->initedBots)) return true;
  72. $this->botInstances->$botCode->init();
  73. $this->initedBots[] = $botCode;
  74. return true;
  75. }
  76. /**
  77. * Process message with bots.
  78. *
  79. * @param object $message
  80. * @param int $userID
  81. * @access public
  82. * @return array messages from bots
  83. */
  84. public function processMessage($message, $userID = 0)
  85. {
  86. $replies = array();
  87. /* Convert characters from "zen-kaku"(full-width) to "han-kaku"(half-width). */
  88. $message->content = mb_convert_kana($message->content, "rnas", 'UTF-8');
  89. /* Check if message is command. */
  90. $isCommand = $message->type == 'botcommand' || ($message->type == 'normal' && in_array($message->contentType, array('text', 'plain')) && strpos($message->content, '/') === 0);
  91. if($isCommand)
  92. {
  93. /* Call bot commands. */
  94. $commandLine = ltrim($message->content, '/');
  95. $commandLine = explode(' ', $commandLine);
  96. $command = array_shift($commandLine);
  97. $args = $commandLine;
  98. $result = $this->executeCommand($command, $args, $message, $userID);
  99. if(isset($result->executedBot))
  100. {
  101. $executedBot = $result->executedBot;
  102. $replies[] = $result->result;
  103. }
  104. else
  105. {
  106. $replies[] = $result;
  107. }
  108. }
  109. else
  110. {
  111. /* Call onMessage on bots. */
  112. foreach($this->bots as $bot)
  113. {
  114. if(!method_exists($this->botInstances->$bot, 'onMessage')) continue;
  115. $this->initBot($bot);
  116. $replies[] = $this->botInstances->$bot->onMessage($message->content, $userID);
  117. }
  118. }
  119. $messages = array();
  120. $responses = array();
  121. $replies = array_filter($replies, function($reply)
  122. {
  123. return !is_null($reply) && $reply !== false;
  124. });
  125. foreach($replies as $reply)
  126. {
  127. /* Use reply as is if is properly set. E.g., (object)array('messages' => array(), 'responses' => array()) */
  128. if(is_object($reply) && isset($reply->messages) && isset($reply->responses))
  129. {
  130. $messages = array_merge($messages, $reply->messages);
  131. $responses = array_merge($responses, $reply->responses);
  132. continue;
  133. }
  134. /* Non-array / non-object values are treated as message contents. */
  135. if(!is_array($reply))
  136. {
  137. $messages[] = $reply;
  138. continue;
  139. }
  140. /* Flatten and extract array of message contents or responses. */
  141. if(is_array($reply) && array_keys($reply) === range(0, count($reply) - 1))
  142. {
  143. foreach($reply as $item)
  144. {
  145. if(is_array($item) || (is_object($item) && isset($item->method) && isset($item->result)))
  146. {
  147. $responses[] = $item;
  148. }
  149. else
  150. {
  151. $messages[] = $item;
  152. }
  153. }
  154. continue;
  155. }
  156. /* Otherwise it's a response. */
  157. $responses[] = $reply;
  158. }
  159. $sender = new stdclass();
  160. $sender->id = 0;
  161. $sender->displayName = isset($executedBot->code) && $executedBot->code != 'default' ? $this->lang->im->bot->commonName . '@' . $executedBot->name : $this->lang->im->bot->commonName;
  162. $sender->avatar = commonModel::getSysURL() . $this->config->webRoot . 'data/image/xuanbot.png';
  163. $messageData = new stdclass();
  164. $messageData->sender = $sender;
  165. $messages = array_map(function($reply) use ($message, $messageData)
  166. {
  167. $botUserID = str_replace("$message->user&", '', $message->cgid);
  168. $replyMessage = new stdclass();
  169. $replyMessage->gid = imModel::createGID();
  170. $replyMessage->cgid = $message->cgid;
  171. $replyMessage->user = $botUserID;
  172. $replyMessage->content = is_object($reply) ? json_encode($reply) : $reply;
  173. $replyMessage->type = 'normal';
  174. $replyMessage->contentType = is_object($reply) ? 'object' : 'text';
  175. $replyMessage->data = json_encode($messageData);
  176. return $replyMessage;
  177. }, $messages);
  178. return (object)array('messages' => $messages, 'responses' => $responses);
  179. }
  180. /**
  181. * Execute command with bot.
  182. *
  183. * @param string $command
  184. * @param array $args
  185. * @param object $message
  186. * @param int $userID
  187. * @access public
  188. * @return object
  189. */
  190. public function executeCommand($command, $args, $message, $userID)
  191. {
  192. $user = $this->im->userGetByID($userID);
  193. $currentBot = $this->getUserCurrentBot($userID);
  194. /* Handle built-in command. */
  195. if(empty($args))
  196. {
  197. foreach($this->lang->im->bot->help->commands as $helpCommand)
  198. {
  199. if($helpCommand == mb_strtolower($command) || mb_strpos(mb_strtolower($command), "{$helpCommand}@") !== false)
  200. {
  201. if($currentBot != 'default' && !strpos($command, '@')) $command .= '@' . $currentBot;
  202. if($currentBot == 'default') return $this->genHelpContentReply();
  203. return $this->helpCommand($command, $user);
  204. }
  205. }
  206. if(in_array(mb_strtolower($command), $this->lang->im->bot->show->commands)) return $this->showCommand($command, $user);
  207. $isSwitchBot = false;
  208. $botCode = 'default';
  209. if(in_array(mb_strtolower($command), $this->lang->im->bot->exit->commands))
  210. {
  211. $isSwitchBot = true;
  212. }
  213. else
  214. {
  215. foreach($this->bots as $bot)
  216. {
  217. $botClass = $this->botInstances->$bot;
  218. if($botClass->code == $command || $botClass->name == $command || (isset($botClass->alias) && in_array($command, $botClass->alias)))
  219. {
  220. $isSwitchBot = true;
  221. $botCode = $botClass->code;
  222. break;
  223. }
  224. }
  225. }
  226. if($isSwitchBot)
  227. {
  228. $reply = new stdClass();
  229. $reply->messages = array();
  230. $reply->responses = array();
  231. $this->setUserCurrentBot($botCode, $userID);
  232. if($botCode == 'default' && $currentBot != 'default')
  233. {
  234. $reply->messages[] = sprintf($this->lang->im->bot->exit->tips, $this->botInstances->$currentBot->name);
  235. return $reply;
  236. }
  237. $reply->messages[] = sprintf($this->lang->im->bot->switch->tips, $this->botInstances->$botCode->name);
  238. if($botCode == 'default' && $currentBot == 'default') return $reply;
  239. $help = $this->helpCommand($botCode, $user);
  240. if(isset($help->result)) $reply->messages[] = $help->result;
  241. return $reply;
  242. }
  243. }
  244. $botsToCall = $this->getListByCommand($command, $currentBot != 'default' ? $currentBot : '');
  245. if(empty($botsToCall)) return $this->lang->im->bot->error->noSuchCommand;
  246. $bot = current($botsToCall);
  247. $this->initBot($bot->code);
  248. /* Check permission. */
  249. $commandToCheck = array();
  250. foreach($this->botInstances->{$bot->bot}->commands as $cmd)
  251. {
  252. if(is_array($cmd) && $bot->command === $cmd['command']) $commandToCheck = $cmd;
  253. if(is_string($cmd) && $bot->command === $cmd) $commandToCheck = $cmd;
  254. }
  255. if(empty($commandToCheck)) return $this->lang->im->bot->error->noSuchCommand;
  256. $executedBot = (object)array('name' => $bot->name, 'code' => $bot->code);
  257. if(isset($commandToCheck['adminOnly']) && $commandToCheck['adminOnly'] && $user->admin == 'no') return $this->lang->im->bot->error->unauthorized;
  258. if(isset($commandToCheck['validator']))
  259. {
  260. $validationResult = $this->botInstances->{$bot->bot}->{$commandToCheck['validator']}($args);
  261. if(!$validationResult) return (object)array('executedBot' => $executedBot, 'result' => $this->lang->im->bot->error->badArguments);
  262. }
  263. if(!method_exists($this->botInstances->{$bot->bot}, $bot->command)) return $this->lang->im->bot->error->noSuchCommand;
  264. return (object)array('executedBot' => $executedBot, 'result' => $this->botInstances->{$bot->bot}->{$bot->command}($args, $userID, $user));
  265. }
  266. /**
  267. * Get the configured bot for the user.
  268. *
  269. * @param int $userID
  270. * @access public
  271. * @return string
  272. */
  273. public function getUserCurrentBot($userID)
  274. {
  275. $user = $this->im->userGetByID($userID);
  276. $botCode = $this->dao->select('value')->from(TABLE_CONFIG)
  277. ->where('owner')->eq($user->account)
  278. ->andWhere('module')->eq('xuanxuan')
  279. ->andWhere('section')->eq('bot')
  280. ->andWhere('`key`')->eq('context')
  281. ->fetch('value');
  282. if(empty($botCode))
  283. {
  284. $data = new stdclass();
  285. $data->owner = $user->account;
  286. $data->module = 'xuanxuan';
  287. $data->section = 'bot';
  288. $data->key = 'context';
  289. $data->value = 'default';
  290. $this->dao->insert(TABLE_CONFIG)->data($data)->exec();
  291. return 'default';
  292. }
  293. return $botCode;
  294. }
  295. /**
  296. * Set the configured bot for the user.
  297. * @param $botCode
  298. * @param $userID
  299. * @return void
  300. */
  301. public function setUserCurrentBot($botCode, $userID)
  302. {
  303. $user = $this->im->userGetByID($userID);
  304. $this->dao->update(TABLE_CONFIG)->set('value')->eq($botCode)->where('owner')->eq($user->account)->andWhere('module')->eq('xuanxuan')->andWhere('section')->eq('bot')->andWhere('`key`')->eq('context')->exec();
  305. }
  306. /**
  307. * Handle show command.
  308. *
  309. * @param string $command
  310. * @param object $user
  311. * @return object
  312. */
  313. public function showCommand($command, $user)
  314. {
  315. $bots = array_values($this->bots);
  316. $result = array();
  317. $reply = new stdClass();
  318. $reply->messages = array();
  319. $reply->responses = array();
  320. foreach($bots as $bot) $result[] = array($this->botInstances->$bot->name, sprintf($this->lang->im->bot->help->sendCommandLink, $bot, $bot));
  321. $reply->messages[] = xuanBot::printMarkdownTable(array($this->lang->im->bot->name, $this->lang->im->bot->code), $result);
  322. return $reply;
  323. }
  324. /**
  325. * Get all command with bot.
  326. *
  327. * @param string $command
  328. * @param object $user
  329. * @access public
  330. * @return string|object
  331. */
  332. public function helpCommand($command, $user)
  333. {
  334. $executedBot = new stdClass();
  335. $helpContent = array();
  336. $botCodeOffset = -1;
  337. if(isset($this->botInstances->{$command}))
  338. {
  339. $bot = $command;
  340. $this->initBot($bot);
  341. $executedBot = $this->getExecutedBot($bot);
  342. if(!empty($this->botInstances->$bot->help)) return (object)array('executedBot' => $executedBot, 'result' => $this->botInstances->$bot->getHelp());
  343. }
  344. if(!isset($this->botInstances->{$command})) $botCodeOffset = mb_strpos($command, "@");
  345. if($botCodeOffset === false) $bots = $this->bots;
  346. if($botCodeOffset !== false)
  347. {
  348. $bot = mb_substr($command, $botCodeOffset + 1);
  349. if(!isset($this->botInstances->$bot)) return $this->lang->im->bot->error->noSuchBot;
  350. $bots = array($bot);
  351. $executedBot = $this->getExecutedBot($bot);
  352. }
  353. foreach($bots as $bot)
  354. {
  355. if(!isset($this->botInstances->$bot) || !isset($this->botInstances->$bot->commands)) continue;
  356. $this->initBot($bot);
  357. if(!empty($this->botInstances->$bot->help) && count($bots) == 1)
  358. {
  359. return (object)array('executedBot' => $executedBot, 'result' => $this->botInstances->$bot->getHelp());
  360. }
  361. foreach($this->botInstances->$bot->commands as $botCommand)
  362. {
  363. if(is_array($botCommand) && !empty($botCommand['adminOnly']) && $user->admin == 'no') continue;
  364. $botCode = $this->botInstances->$bot->code;
  365. $command = is_array($botCommand) ? $botCommand['command'] : $botCommand;
  366. $description = is_array($botCommand) && isset($botCommand['description']) ? $botCommand['description'] : '';
  367. $isClickToSend = is_array($botCommand) && (!isset($botCommand['argsRequired']) || !$botCommand['argsRequired']);
  368. $commandLink = sprintf($this->lang->im->bot->help->{$isClickToSend ? 'sendBotCommandLink' : 'botCommandLink'}, $command, $command, $botCode);
  369. $helpContent[] = array($commandLink, $description, $botCode);
  370. }
  371. }
  372. $result = xuanBot::printMarkdownTable($this->lang->im->bot->help->header, $helpContent);
  373. return (object)array('executedBot' => $executedBot, 'result' => $result);
  374. }
  375. /**
  376. * Get Executed Bot.
  377. *
  378. * @param string $bot
  379. * @access public
  380. * @return object
  381. */
  382. public function getExecutedBot($bot)
  383. {
  384. if(!isset($this->botInstances->$bot)) return new stdClass();
  385. return (object)array('name' => $this->botInstances->$bot->name, 'code' => $this->botInstances->$bot->code, 'avatar' => $this->botInstances->$bot->avatar);
  386. }
  387. /**
  388. * Get bot list by command, returns list of bot with such command.
  389. *
  390. * @param string $command
  391. * @access public
  392. * @return array
  393. */
  394. public function getListByCommand($command, $bot = '')
  395. {
  396. /* Get command from specified bot. */
  397. if(strpos($command, '@') !== false)
  398. {
  399. list($command, $bot) = explode('@', $command);
  400. if(!isset($this->botInstances->$bot) || !isset($this->botInstances->$bot->commands)) return array();
  401. return $this->getListByCommand($command, $bot);
  402. }
  403. $currentBot = $bot;
  404. $command = mb_strtolower($command); // Ignore case of command.
  405. /* Get command bot pairs. */
  406. $bots = array();
  407. $botsToCheck = empty($bot) ? $this->bots : array($bot);
  408. foreach($botsToCheck as $bot)
  409. {
  410. if(!isset($this->botInstances->$bot) || !isset($this->botInstances->$bot->commands)) continue;
  411. $this->initBot($bot); // init bot on demand.
  412. /* Check command array of bot. */
  413. foreach($this->botInstances->$bot->commands as $botCommand)
  414. {
  415. $isThisCommand = false;
  416. /* Command is array, check for properties. */
  417. if(is_array($botCommand))
  418. {
  419. /* Check if command equal (ignore case). */
  420. if(!empty($botCommand['command']) && mb_strtolower($botCommand['command']) == $command)
  421. {
  422. $isThisCommand = true;
  423. }
  424. elseif(!empty($botCommand['alias'])) // Check for alias (ignore case).
  425. {
  426. foreach($botCommand['alias'] as $alias)
  427. {
  428. if(mb_strtolower($alias) == $command)
  429. {
  430. $isThisCommand = true;
  431. break;
  432. }
  433. }
  434. }
  435. /* Check if command is internal (can only be called within the bot app). */
  436. if($isThisCommand && (!isset($botCommand['internal']) || !$botCommand['internal'] || $bot == $currentBot))
  437. {
  438. $bots[] = (object)array('bot' => $bot, 'command' => $botCommand['command'], 'name' => $this->botInstances->$bot->name, 'code' => $this->botInstances->$bot->code, 'avatar' => $this->botInstances->$bot->avatar);
  439. break;
  440. }
  441. }
  442. elseif(mb_strtolower($botCommand) == $command) // Command is string, check if equal (ignore case).
  443. {
  444. $bots[] = (object)array('bot' => $bot, 'command' => $command, 'name' => $this->botInstances->$bot->name, 'code' => $this->botInstances->$bot->code, 'avatar' => $this->botInstances->$bot->avatar);
  445. break;
  446. }
  447. }
  448. }
  449. return $bots;
  450. }
  451. /**
  452. * Gen help content reply.
  453. *
  454. * @return object
  455. */
  456. public function genHelpContentReply()
  457. {
  458. $reply = new stdClass();
  459. $reply->messages = array();
  460. $reply->responses = array();
  461. $reply->messages[] = $this->lang->im->bot->help->welcome;
  462. $reply->messages[] = $this->lang->im->bot->help->global . $this->lang->im->bot->help->system;
  463. return $reply;
  464. }
  465. /**
  466. * Create default bot sender.
  467. *
  468. * @param string $extraName
  469. * @access public
  470. * @return object
  471. */
  472. public function createDefaultBotSender($extraName = '')
  473. {
  474. $sender = new stdclass();
  475. $sender->id = 0;
  476. $sender->displayName = $this->lang->im->bot->commonName;
  477. $sender->avatar = commonModel::getSysURL() . $this->config->webRoot . 'data/image/xuanbot.png';
  478. if(!empty($extraName)) $sender->displayName .= '@' . $extraName;
  479. return $sender;
  480. }
  481. }