vendor/botman/botman/src/BotMan.php line 430

Open in your IDE?
  1. <?php
  2. namespace BotMan\BotMan;
  3. use BotMan\BotMan\Commands\Command;
  4. use BotMan\BotMan\Commands\ConversationManager;
  5. use BotMan\BotMan\Drivers\DriverManager;
  6. use BotMan\BotMan\Exceptions\Base\BotManException;
  7. use BotMan\BotMan\Exceptions\Core\BadMethodCallException;
  8. use BotMan\BotMan\Exceptions\Core\UnexpectedValueException;
  9. use BotMan\BotMan\Handlers\ExceptionHandler;
  10. use BotMan\BotMan\Interfaces\CacheInterface;
  11. use BotMan\BotMan\Interfaces\DriverEventInterface;
  12. use BotMan\BotMan\Interfaces\DriverInterface;
  13. use BotMan\BotMan\Interfaces\ExceptionHandlerInterface;
  14. use BotMan\BotMan\Interfaces\Middleware\Heard;
  15. use BotMan\BotMan\Interfaces\StorageInterface;
  16. use BotMan\BotMan\Interfaces\UserInterface;
  17. use BotMan\BotMan\Messages\Attachments\Audio;
  18. use BotMan\BotMan\Messages\Attachments\Contact;
  19. use BotMan\BotMan\Messages\Attachments\File;
  20. use BotMan\BotMan\Messages\Attachments\Image;
  21. use BotMan\BotMan\Messages\Attachments\Location;
  22. use BotMan\BotMan\Messages\Attachments\Video;
  23. use BotMan\BotMan\Messages\Conversations\InlineConversation;
  24. use BotMan\BotMan\Messages\Incoming\Answer;
  25. use BotMan\BotMan\Messages\Incoming\IncomingMessage;
  26. use BotMan\BotMan\Messages\Matcher;
  27. use BotMan\BotMan\Messages\Outgoing\OutgoingMessage;
  28. use BotMan\BotMan\Messages\Outgoing\Question;
  29. use BotMan\BotMan\Middleware\MiddlewareManager;
  30. use BotMan\BotMan\Traits\HandlesConversations;
  31. use BotMan\BotMan\Traits\HandlesExceptions;
  32. use BotMan\BotMan\Traits\ProvidesStorage;
  33. use Closure;
  34. use Illuminate\Support\Collection;
  35. use Psr\Container\ContainerInterface;
  36. use Psr\Container\NotFoundExceptionInterface;
  37. use Symfony\Component\HttpFoundation\Response;
  38. /**
  39. * Class BotMan.
  40. */
  41. class BotMan
  42. {
  43. use ProvidesStorage,
  44. HandlesConversations,
  45. HandlesExceptions;
  46. /** @var \Illuminate\Support\Collection */
  47. protected $event;
  48. /** @var Command */
  49. protected $command;
  50. /** @var IncomingMessage */
  51. protected $message;
  52. /** @var OutgoingMessage|Question */
  53. protected $outgoingMessage;
  54. /** @var string */
  55. protected $driverName;
  56. /** @var array|null */
  57. protected $currentConversationData;
  58. /** @var ExceptionHandlerInterface */
  59. protected $exceptionHandler;
  60. /**
  61. * IncomingMessage service events.
  62. * @var array
  63. */
  64. protected $events = [];
  65. /**
  66. * The fallback message to use, if no match
  67. * could be heard.
  68. * @var callable|null
  69. */
  70. protected $fallbackMessage;
  71. /** @var array */
  72. protected $groupAttributes = [];
  73. /** @var array */
  74. protected $matches = [];
  75. /** @var DriverInterface */
  76. protected $driver;
  77. /** @var array */
  78. protected $config = [];
  79. /** @var MiddlewareManager */
  80. public $middleware;
  81. /** @var ConversationManager */
  82. protected $conversationManager;
  83. /** @var CacheInterface */
  84. private $cache;
  85. /** @var ContainerInterface */
  86. protected $container;
  87. /** @var StorageInterface */
  88. protected $storage;
  89. /** @var Matcher */
  90. protected $matcher;
  91. /** @var bool */
  92. protected $loadedConversation = false;
  93. /** @var bool */
  94. protected $firedDriverEvents = false;
  95. /** @var bool */
  96. protected $runsOnSocket = false;
  97. /**
  98. * BotMan constructor.
  99. * @param CacheInterface $cache
  100. * @param DriverInterface $driver
  101. * @param array $config
  102. * @param StorageInterface $storage
  103. */
  104. public function __construct(CacheInterface $cache, DriverInterface $driver, $config, StorageInterface $storage, ?Matcher $matcher = null)
  105. {
  106. $this->config = $config;
  107. $this->config['bot_id'] = $this->config['bot_id'] ?? '';
  108. $this->cache = $cache;
  109. $this->message = new IncomingMessage('', '', '', null, $this->config['bot_id']);
  110. $this->driver = $driver;
  111. $this->storage = $storage;
  112. $this->matcher = new Matcher();
  113. $this->middleware = new MiddlewareManager($this);
  114. $this->conversationManager = new ConversationManager($matcher);
  115. $this->exceptionHandler = new ExceptionHandler();
  116. }
  117. /**
  118. * Set a fallback message to use if no listener matches.
  119. *
  120. * @param callable $callback
  121. */
  122. public function fallback($callback)
  123. {
  124. $this->fallbackMessage = $callback;
  125. }
  126. /**
  127. * @param string $name The Driver name or class
  128. */
  129. public function loadDriver($name)
  130. {
  131. $this->driver = DriverManager::loadFromName($name, $this->config);
  132. }
  133. /**
  134. * @param DriverInterface $driver
  135. */
  136. public function setDriver(DriverInterface $driver)
  137. {
  138. $this->driver = $driver;
  139. }
  140. /**
  141. * @return DriverInterface
  142. */
  143. public function getDriver()
  144. {
  145. return $this->driver;
  146. }
  147. /**
  148. * @param ContainerInterface $container
  149. */
  150. public function setContainer(ContainerInterface $container)
  151. {
  152. $this->container = $container;
  153. }
  154. /**
  155. * Retrieve the chat message.
  156. *
  157. * @return array
  158. */
  159. public function getMessages()
  160. {
  161. return $this->getDriver()->getMessages();
  162. }
  163. /**
  164. * Retrieve the chat message that are sent from bots.
  165. *
  166. * @return array
  167. */
  168. public function getBotMessages()
  169. {
  170. return Collection::make($this->getDriver()->getMessages())->filter(function (IncomingMessage $message) {
  171. return $message->isFromBot();
  172. })->toArray();
  173. }
  174. /**
  175. * @return Answer
  176. */
  177. public function getConversationAnswer()
  178. {
  179. return $this->getDriver()->getConversationAnswer($this->message);
  180. }
  181. /**
  182. * @param bool $running
  183. * @return bool
  184. */
  185. public function runsOnSocket($running = null)
  186. {
  187. if (\is_bool($running)) {
  188. $this->runsOnSocket = $running;
  189. }
  190. return $this->runsOnSocket;
  191. }
  192. /**
  193. * @return UserInterface
  194. */
  195. public function getUser()
  196. {
  197. if ($user = $this->cache->get('user_' . $this->driver->getName() . '_' . $this->getMessage()->getSender())) {
  198. return $user;
  199. }
  200. $user = $this->getDriver()->getUser($this->getMessage());
  201. $this->cache->put(
  202. 'user_' . $this->driver->getName() . '_' . $user->getId(),
  203. $user,
  204. $this->config['user_cache_time'] ?? 30
  205. );
  206. return $user;
  207. }
  208. /**
  209. * Get the parameter names for the route.
  210. *
  211. * @param $value
  212. * @return array
  213. */
  214. protected function compileParameterNames($value)
  215. {
  216. preg_match_all(Matcher::PARAM_NAME_REGEX, $value, $matches);
  217. return array_map(function ($m) {
  218. return trim($m, '?');
  219. }, $matches[1]);
  220. }
  221. /**
  222. * @param array|string $pattern the pattern to listen for
  223. * @param Closure|string $callback the callback to execute. Either a closure or a Class@method notation
  224. * @param string $in the channel type to listen to (either direct message or public channel)
  225. * @return Command
  226. */
  227. public function hears($pattern, $callback, $in = null)
  228. {
  229. if (is_array($pattern)) {
  230. $pattern = '(?|' . implode('|', $pattern) . ')';
  231. }
  232. $command = new Command($pattern, $callback, $in);
  233. $command->applyGroupAttributes($this->groupAttributes);
  234. $this->conversationManager->listenTo($command);
  235. return $command;
  236. }
  237. /**
  238. * Listen for messaging service events.
  239. *
  240. * @param array|string $names
  241. * @param Closure|string $callback
  242. */
  243. public function on($names, $callback)
  244. {
  245. if (!is_array($names)) {
  246. $names = [$names];
  247. }
  248. $callable = $this->getCallable($callback);
  249. foreach ($names as $name) {
  250. $this->events[] = [
  251. 'name' => $name,
  252. 'callback' => $callable,
  253. ];
  254. }
  255. }
  256. /**
  257. * Listening for image files.
  258. *
  259. * @param $callback
  260. * @return Command
  261. */
  262. public function receivesImages($callback)
  263. {
  264. return $this->hears(Image::PATTERN, $callback);
  265. }
  266. /**
  267. * Listening for video files.
  268. *
  269. * @param $callback
  270. * @return Command
  271. */
  272. public function receivesVideos($callback)
  273. {
  274. return $this->hears(Video::PATTERN, $callback);
  275. }
  276. /**
  277. * Listening for audio files.
  278. *
  279. * @param $callback
  280. * @return Command
  281. */
  282. public function receivesAudio($callback)
  283. {
  284. return $this->hears(Audio::PATTERN, $callback);
  285. }
  286. /**
  287. * Listening for location attachment.
  288. *
  289. * @param $callback
  290. * @return Command
  291. */
  292. public function receivesLocation($callback)
  293. {
  294. return $this->hears(Location::PATTERN, $callback);
  295. }
  296. /**
  297. * Listening for contact attachment.
  298. *
  299. * @param $callback
  300. *
  301. * @return Command
  302. */
  303. public function receivesContact($callback)
  304. {
  305. return $this->hears(Contact::PATTERN, $callback);
  306. }
  307. /**
  308. * Listening for files attachment.
  309. *
  310. * @param $callback
  311. * @return Command
  312. */
  313. public function receivesFiles($callback)
  314. {
  315. return $this->hears(File::PATTERN, $callback);
  316. }
  317. /**
  318. * Create a command group with shared attributes.
  319. *
  320. * @param array $attributes
  321. * @param \Closure $callback
  322. */
  323. public function group(array $attributes, Closure $callback)
  324. {
  325. $previousGroupAttributes = $this->groupAttributes;
  326. $this->groupAttributes = array_merge_recursive($previousGroupAttributes, $attributes);
  327. \call_user_func($callback, $this);
  328. $this->groupAttributes = $previousGroupAttributes;
  329. }
  330. /**
  331. * Fire potential driver event callbacks.
  332. */
  333. protected function fireDriverEvents()
  334. {
  335. $driverEvent = $this->getDriver()->hasMatchingEvent();
  336. if ($driverEvent instanceof DriverEventInterface) {
  337. $this->firedDriverEvents = true;
  338. Collection::make($this->events)->filter(function ($event) use ($driverEvent) {
  339. return $driverEvent->getName() === $event['name'];
  340. })->each(function ($event) use ($driverEvent) {
  341. /**
  342. * Load the message, so driver events can reply.
  343. */
  344. $messages = $this->getDriver()->getMessages();
  345. if (isset($messages[0])) {
  346. $this->message = $messages[0];
  347. }
  348. \call_user_func_array($event['callback'], [$driverEvent->getPayload(), $this]);
  349. });
  350. }
  351. }
  352. /**
  353. * Try to match messages with the ones we should
  354. * listen to.
  355. */
  356. public function listen()
  357. {
  358. try {
  359. $isVerificationRequest = $this->verifyServices();
  360. if (!$isVerificationRequest) {
  361. $this->fireDriverEvents();
  362. if ($this->firedDriverEvents === false) {
  363. $this->loadActiveConversation();
  364. if ($this->loadedConversation === false) {
  365. $this->callMatchingMessages();
  366. }
  367. }
  368. /*
  369. * If the driver has a "messagesHandled" method, call it.
  370. * This method can be used to trigger driver methods
  371. * once the messages are handles.
  372. */
  373. if (method_exists($this->getDriver(), 'messagesHandled')) {
  374. $this->getDriver()->messagesHandled();
  375. }
  376. $this->firedDriverEvents = false;
  377. $this->message = new IncomingMessage('', '', '', null, $this->config['bot_id']);
  378. }
  379. } catch (\Throwable $e) {
  380. $this->exceptionHandler->handleException($e, $this);
  381. }
  382. }
  383. /**
  384. * Call matching message callbacks.
  385. */
  386. protected function callMatchingMessages()
  387. {
  388. $matchingMessages = $this->conversationManager->getMatchingMessages(
  389. $this->getMessages(),
  390. $this->middleware,
  391. $this->getConversationAnswer(),
  392. $this->getDriver()
  393. );
  394. foreach ($matchingMessages as $matchingMessage) {
  395. $this->command = $matchingMessage->getCommand();
  396. $callback = $this->command->getCallback();
  397. $callback = $this->getCallable($callback);
  398. // Set the message first, so it's available for middlewares
  399. $this->message = $matchingMessage->getMessage();
  400. $commandMiddleware = Collection::make($this->command->getMiddleware())->filter(function ($middleware) {
  401. return $middleware instanceof Heard;
  402. })->toArray();
  403. $this->message = $this->middleware->applyMiddleware(
  404. 'heard',
  405. $matchingMessage->getMessage(),
  406. $commandMiddleware
  407. );
  408. $parameterNames = $this->compileParameterNames($this->command->getPattern());
  409. $parameters = $matchingMessage->getMatches();
  410. if (\count($parameterNames) !== \count($parameters)) {
  411. $parameters = array_merge(
  412. //First, all named parameters (eg. function ($a, $b, $c))
  413. array_filter(
  414. $parameters,
  415. '\is_string',
  416. ARRAY_FILTER_USE_KEY
  417. ),
  418. //Then, all other unsorted parameters (regex non named results)
  419. array_filter(
  420. $parameters,
  421. '\is_integer',
  422. ARRAY_FILTER_USE_KEY
  423. )
  424. );
  425. }
  426. $this->matches = $parameters;
  427. array_unshift($parameters, $this);
  428. $parameters = $this->conversationManager->addDataParameters($this->message, $parameters);
  429. if (call_user_func_array($callback, array_values($parameters))) {
  430. return;
  431. }
  432. }
  433. if (empty($matchingMessages) && empty($this->getBotMessages()) && !\is_null($this->fallbackMessage)) {
  434. $this->callFallbackMessage();
  435. }
  436. }
  437. /**
  438. * Call the fallback method.
  439. */
  440. protected function callFallbackMessage()
  441. {
  442. $messages = $this->getMessages();
  443. if (!isset($messages[0])) {
  444. return;
  445. }
  446. $this->message = $messages[0];
  447. $this->fallbackMessage = $this->getCallable($this->fallbackMessage);
  448. \call_user_func($this->fallbackMessage, $this);
  449. }
  450. /**
  451. * Verify service webhook URLs.
  452. *
  453. * @return bool
  454. */
  455. protected function verifyServices()
  456. {
  457. return DriverManager::verifyServices($this->config);
  458. }
  459. /**
  460. * @param string|Question|OutgoingMessage $message
  461. * @param string|array $recipients
  462. * @param string|DriverInterface|null $driver
  463. * @param array $additionalParameters
  464. * @return Response
  465. * @throws BotManException
  466. */
  467. public function say($message, $recipients, $driver = null, $additionalParameters = [])
  468. {
  469. if ($driver === null && $this->driver === null) {
  470. throw new BotManException('The current driver can\'t be NULL');
  471. }
  472. $previousDriver = $this->driver;
  473. $previousMessage = $this->message;
  474. if ($driver instanceof DriverInterface) {
  475. $this->setDriver($driver);
  476. } elseif (\is_string($driver)) {
  477. $this->setDriver(DriverManager::loadFromName($driver, $this->config));
  478. }
  479. $recipients = \is_array($recipients) ? $recipients : [$recipients];
  480. foreach ($recipients as $recipient) {
  481. $this->message = new IncomingMessage('', $recipient, '', null, $this->config['bot_id'] ?? '');
  482. $response = $this->reply($message, $additionalParameters);
  483. }
  484. $this->message = $previousMessage;
  485. $this->driver = $previousDriver;
  486. return $response;
  487. }
  488. /**
  489. * @param string|Question $question
  490. * @param array|Closure $next
  491. * @param array $additionalParameters
  492. * @param null|string $recipient
  493. * @param null|string $driver
  494. * @return Response
  495. */
  496. public function ask($question, $next, $additionalParameters = [], $recipient = null, $driver = null)
  497. {
  498. if (!\is_null($recipient) && !\is_null($driver)) {
  499. if (\is_string($driver)) {
  500. $driver = DriverManager::loadFromName($driver, $this->config);
  501. }
  502. $this->message = new IncomingMessage('', $recipient, '', null, $this->config['bot_id']);
  503. $this->setDriver($driver);
  504. }
  505. $response = $this->reply($question, $additionalParameters);
  506. $this->storeConversation(new InlineConversation, $next, $question, $additionalParameters);
  507. return $response;
  508. }
  509. /**
  510. * @return $this
  511. */
  512. public function types()
  513. {
  514. $this->getDriver()->types($this->message);
  515. return $this;
  516. }
  517. /**
  518. * @param float $seconds Number of seconds to wait
  519. * @return $this
  520. */
  521. public function typesAndWaits(float $seconds)
  522. {
  523. $this->getDriver()->typesAndWaits($this->message, $seconds);
  524. return $this;
  525. }
  526. /**
  527. * Low-level method to perform driver specific API requests.
  528. *
  529. * @param string $endpoint
  530. * @param array $additionalParameters
  531. * @return $this
  532. * @throws BadMethodCallException
  533. */
  534. public function sendRequest($endpoint, $additionalParameters = [])
  535. {
  536. $driver = $this->getDriver();
  537. if (method_exists($driver, 'sendRequest')) {
  538. return $driver->sendRequest($endpoint, $additionalParameters, $this->message);
  539. }
  540. throw new BadMethodCallException('The driver ' . $this->getDriver()->getName() . ' does not support low level requests.');
  541. }
  542. /**
  543. * @param string|OutgoingMessage|Question $message
  544. * @param array $additionalParameters
  545. * @return mixed
  546. */
  547. public function reply($message, $additionalParameters = [])
  548. {
  549. $this->outgoingMessage = \is_string($message) ? OutgoingMessage::create($message) : $message;
  550. return $this->sendPayload($this->getDriver()->buildServicePayload(
  551. $this->outgoingMessage,
  552. $this->message,
  553. $additionalParameters
  554. ));
  555. }
  556. /**
  557. * @param $payload
  558. * @return mixed
  559. */
  560. public function sendPayload($payload)
  561. {
  562. return $this->middleware->applyMiddleware('sending', $payload, [], function ($payload) {
  563. $this->outgoingMessage = null;
  564. return $this->getDriver()->sendPayload($payload);
  565. });
  566. }
  567. /**
  568. * Return a random message.
  569. * @param array $messages
  570. * @return $this
  571. */
  572. public function randomReply(array $messages)
  573. {
  574. return $this->reply($messages[array_rand($messages)]);
  575. }
  576. /**
  577. * Make an action for an invokable controller.
  578. *
  579. * @param string $action
  580. * @return string
  581. * @throws UnexpectedValueException
  582. */
  583. protected function makeInvokableAction($action)
  584. {
  585. if (!method_exists($action, '__invoke')) {
  586. throw new UnexpectedValueException(sprintf(
  587. 'Invalid hears action: [%s]',
  588. $action
  589. ));
  590. }
  591. return $action . '@__invoke';
  592. }
  593. /**
  594. * @param mixed $callback
  595. * @return mixed
  596. * @throws UnexpectedValueException
  597. * @throws NotFoundExceptionInterface
  598. */
  599. protected function getCallable($callback)
  600. {
  601. if (is_callable($callback)) {
  602. return $callback;
  603. }
  604. if (strpos($callback, '@') === false) {
  605. $callback = $this->makeInvokableAction($callback);
  606. }
  607. [$class, $method] = explode('@', $callback);
  608. $command = $this->container ? $this->container->get($class) : new $class($this);
  609. return [$command, $method];
  610. }
  611. /**
  612. * @return array
  613. */
  614. public function getMatches()
  615. {
  616. return $this->matches;
  617. }
  618. /**
  619. * @return IncomingMessage
  620. */
  621. public function getMessage()
  622. {
  623. return $this->message;
  624. }
  625. /**
  626. * @return OutgoingMessage|Question
  627. */
  628. public function getOutgoingMessage()
  629. {
  630. return $this->outgoingMessage;
  631. }
  632. /**
  633. * @param string $name
  634. * @param array $arguments
  635. * @return mixed
  636. * @throws BadMethodCallException
  637. */
  638. public function __call($name, $arguments)
  639. {
  640. if (method_exists($this->getDriver(), $name)) {
  641. // Add the current message to the passed arguments
  642. $arguments[] = $this->getMessage();
  643. $arguments[] = $this;
  644. return \call_user_func_array([$this->getDriver(), $name], array_values($arguments));
  645. }
  646. throw new BadMethodCallException('Method [' . $name . '] does not exist.');
  647. }
  648. /**
  649. * Load driver on wakeup.
  650. */
  651. public function __wakeup()
  652. {
  653. $this->driver = DriverManager::loadFromName($this->driverName, $this->config);
  654. }
  655. /**
  656. * @return array
  657. */
  658. public function __sleep()
  659. {
  660. $this->driverName = $this->driver->getName();
  661. return [
  662. 'event',
  663. 'exceptionHandler',
  664. 'driverName',
  665. 'storage',
  666. 'message',
  667. 'cache',
  668. 'matches',
  669. 'matcher',
  670. 'config',
  671. 'middleware',
  672. ];
  673. }
  674. }