├── .gitignore ├── doc └── magebot_configuration.png ├── registration.php ├── src ├── etc │ ├── module.xml │ ├── frontend │ │ └── routes.xml │ ├── di.xml │ ├── acl.xml │ ├── config.xml │ └── adminhtml │ │ └── system.xml ├── StateMachine │ ├── Actions.php │ ├── Triggers.php │ ├── States.php │ ├── State.php │ ├── Transition.php │ ├── Transitions.php │ ├── Serialization │ │ ├── ActionFactory.php │ │ ├── TriggerFactory.php │ │ ├── FakeActionFactory.php │ │ ├── FakeTriggerFactory.php │ │ ├── NewActionFactory.php │ │ ├── NewTriggerFactory.php │ │ ├── LazyLoadingAction.php │ │ ├── LazyLoadingTrigger.php │ │ ├── SerializableAction.php │ │ └── SerializableTrigger.php │ ├── StateMachine.php │ ├── UnstartedState.php │ ├── Action.php │ ├── FixedTrigger.php │ ├── Trigger.php │ ├── InitialTransition.php │ ├── StateSet.php │ ├── FakeTrigger.php │ ├── CallbackAction.php │ ├── TransitionList.php │ ├── FakeAction.php │ ├── ConversationContext.php │ ├── ActionList.php │ ├── TriggerList.php │ ├── ConversationTransition.php │ ├── ConversationState.php │ └── Conversation.php ├── Botman │ ├── BotmanConfig.php │ ├── BotmanAction.php │ ├── ConversationDefinitions.php │ ├── ConversationDefinition.php │ ├── ConversationDefinitionList.php │ ├── BotmanActionFactory.php │ ├── MessageAction.php │ ├── AnswerTrigger.php │ ├── BotmanConversationContext.php │ ├── CacheBridge.php │ ├── BotmanConversation.php │ ├── FakeDriver.php │ ├── ProxyDriver.php │ ├── BotmanWebDriver.php │ └── QuestionAction.php ├── Controller │ └── Index │ │ └── Index.php ├── MageBot.php ├── Conversations │ └── CustomerGroupConversation.php └── Helper │ └── Config.php ├── tests ├── MageBotTest.php ├── unit │ ├── StateMachine │ │ ├── CallbackActionTest.php │ │ ├── ActionListTest.php │ │ ├── TransitionListTest.php │ │ ├── StateSetTest.php │ │ ├── LazyLoadingActionTest.php │ │ ├── LazyLoadingTriggerTest.php │ │ ├── SerializableTriggerTest.php │ │ ├── SerializableActionTest.php │ │ ├── ConversationTransitionTest.php │ │ ├── ConversationStateTest.php │ │ └── ConversationTest.php │ └── Botman │ │ ├── MessageActionTest.php │ │ ├── BotmanConversationContextTest.php │ │ ├── AnswerTriggerTest.php │ │ ├── ConversationDefinitionListTest.php │ │ ├── QuestionActionTest.php │ │ └── BotmanConversationTest.php └── integration │ └── Botman │ └── CacheTest.php ├── phpunit.xml.dist ├── README.md └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ._.DS_Store 3 | /composer.lock 4 | /vendor 5 | -------------------------------------------------------------------------------- /doc/magebot_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magento-hackathon/magebot/HEAD/doc/magebot_configuration.png -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/StateMachine/Actions.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(MageBot::class, $mageBot); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 5 | 6 | tests/unit 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/StateMachine/State.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Botman/BotmanAction.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/StateMachine/Serialization/ActionFactory.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(\stdClass::class) 13 | ->setMethods(['__invoke']) 14 | ->getMock(); 15 | $callbackMock->expects(static::once())->method('__invoke'); 16 | $action = new CallbackAction($callbackMock); 17 | $action->execute($this->createMock(ConversationContext::class)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/StateMachine/UnstartedState.php: -------------------------------------------------------------------------------- 1 | Configuration->Services->MageBot"** 13 | 3. Define your Bot conversation (see [Define your Bot conversation](Define your Bot conversation)) 14 | 15 | ![Magebot Configuration](./doc/magebot_configuration.png "Magebot Configuration") 16 | 17 | ## Define your Bot conversation 18 | //ToDo 19 | -------------------------------------------------------------------------------- /src/etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/StateMachine/Action.php: -------------------------------------------------------------------------------- 1 | matches = $matches; 18 | } 19 | 20 | public function activated(ConversationContext $context) : bool 21 | { 22 | return $this->matches; 23 | } 24 | 25 | public function type() : string 26 | { 27 | return __CLASS__; 28 | } 29 | 30 | public function parameters() : array 31 | { 32 | return [self::PARAM_MATCHES => $this->matches]; 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /src/StateMachine/Trigger.php: -------------------------------------------------------------------------------- 1 | initialState = $initialState; 17 | } 18 | 19 | public function name() : string 20 | { 21 | return ''; 22 | } 23 | 24 | public function target() : State 25 | { 26 | return $this->initialState; 27 | } 28 | 29 | public function triggeredAt(State $currentState, ConversationContext $context) : bool 30 | { 31 | return $currentState instanceof UnstartedState; 32 | } 33 | 34 | public function toArray() : array 35 | { 36 | return []; 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/StateMachine/StateSet.php: -------------------------------------------------------------------------------- 1 | getArrayCopy(), true); 25 | } 26 | 27 | public function with(State $state) : States 28 | { 29 | return new static($state, ...$this->getArrayCopy()); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/ActionListTest.php: -------------------------------------------------------------------------------- 1 | actionExpectedToBeCalled(), $this->actionExpectedToBeCalled()); 13 | $actionList->executeAll($this->createMock(ConversationContext::class)); 14 | } 15 | 16 | /** 17 | * @return CallbackAction 18 | */ 19 | private function actionExpectedToBeCalled():CallbackAction 20 | { 21 | $callbackMock = $this->getMockBuilder(\stdClass::class) 22 | ->setMethods(['__invoke']) 23 | ->getMock(); 24 | $callbackMock->expects(static::once())->method('__invoke'); 25 | return new CallbackAction($callbackMock); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/StateMachine/FakeTrigger.php: -------------------------------------------------------------------------------- 1 | parameters = $parameters; 19 | } 20 | 21 | public function type() : string 22 | { 23 | return __CLASS__; 24 | } 25 | 26 | public function parameters() : array 27 | { 28 | return $this->parameters; 29 | } 30 | 31 | public function activated(ConversationContext $context) : bool 32 | { 33 | return !empty($this->parameters['activated']); 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /src/StateMachine/CallbackAction.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 18 | } 19 | 20 | public function type() : string 21 | { 22 | return __CLASS__; 23 | } 24 | 25 | public function parameters() : array 26 | { 27 | throw new \RuntimeException("CallbackAction type is not serializable"); 28 | } 29 | 30 | 31 | public function execute(ConversationContext $context) 32 | { 33 | ($this->callback)(); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/Botman/ConversationDefinitionList.php: -------------------------------------------------------------------------------- 1 | hears($conversationDefinition->patternToStart(), function(BotMan $bot) use ($conversationDefinition) { 24 | $bot->startConversation(new BotmanConversation($bot, $conversationDefinition->create())); 25 | }); 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/StateMachine/TransitionList.php: -------------------------------------------------------------------------------- 1 | triggeredAt($currentState, $context)) { 22 | 23 | return $transition->target(); 24 | } 25 | } 26 | 27 | return $currentState; 28 | } 29 | 30 | public function prepend(Transition $transition) : Transitions 31 | { 32 | return new static($transition, ...$this->getArrayCopy()); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/Botman/BotmanActionFactory.php: -------------------------------------------------------------------------------- 1 | botman = $botman; 23 | } 24 | 25 | public function create(string $type, array $parameters) : Action 26 | { 27 | if (is_a($type, BotmanAction::class, true)) { 28 | return new $type($this->botman, ...array_values($parameters)); 29 | } 30 | return new $type(...array_values($parameters)); 31 | } 32 | } -------------------------------------------------------------------------------- /src/StateMachine/FakeAction.php: -------------------------------------------------------------------------------- 1 | parameters = $parameters; 20 | } 21 | 22 | public function type() : string 23 | { 24 | return __CLASS__; 25 | } 26 | 27 | public function parameters() : array 28 | { 29 | return $this->parameters; 30 | } 31 | 32 | public function execute(ConversationContext $context) 33 | { 34 | ++$this->timesExecuted; 35 | } 36 | 37 | /** 38 | * @return int 39 | */ 40 | public function timesExecuted(): int 41 | { 42 | return $this->timesExecuted; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /tests/unit/Botman/MessageActionTest.php: -------------------------------------------------------------------------------- 1 | createMock(BotMan::class), 'Hello, world!'); 15 | static::assertEquals(MessageAction::class, $action->type()); 16 | static::assertEquals(['message' => 'Hello, world!'], $action->parameters()); 17 | } 18 | public function testSendMessage() 19 | { 20 | $botMock = $this->createMock(BotMan::class); 21 | $botMock->expects(static::once()) 22 | ->method('reply') 23 | ->with('Hello, world!'); 24 | $action = new MessageAction($botMock, 'Hello, world!'); 25 | $action->execute($this->createMock(ConversationContext::class)); 26 | } 27 | } -------------------------------------------------------------------------------- /src/StateMachine/ConversationContext.php: -------------------------------------------------------------------------------- 1 | mageBot = $mageBot; 18 | } 19 | 20 | 21 | public function execute() 22 | { 23 | $this->mageBot->start(); 24 | return $this->emptyResult(); 25 | } 26 | 27 | /** 28 | * @return \Magento\Framework\Controller\Result\Raw 29 | */ 30 | private function emptyResult() 31 | { 32 | /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ 33 | $resultRaw = $this->resultFactory->create(ResultFactory::TYPE_RAW); 34 | return $resultRaw; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/TransitionListTest.php: -------------------------------------------------------------------------------- 1 | prepend($transitionBtoA)) 21 | ); 22 | static::assertEquals( 23 | [$transitionAtoB], 24 | $transitions->getArrayCopy() 25 | ); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Botman/MessageAction.php: -------------------------------------------------------------------------------- 1 | botman = $botman; 25 | $this->message = $message; 26 | } 27 | 28 | public function type() : string 29 | { 30 | return __CLASS__; 31 | } 32 | 33 | public function parameters() : array 34 | { 35 | return [ 36 | self::PARAM_MESSAGE => $this->message, 37 | ]; 38 | } 39 | 40 | public function execute(ConversationContext $context) 41 | { 42 | $this->botman->reply($this->message); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /tests/unit/Botman/BotmanConversationContextTest.php: -------------------------------------------------------------------------------- 1 | setPersistentVar('Persist me', 'I am a variable!'); 14 | $serializedContext = serialize($context); 15 | /** @var BotmanConversationContext $unserializedContext */ 16 | $unserializedContext = unserialize($serializedContext, ['allowed_classes' => [BotmanConversationContext::class]]); 17 | static::assertTrue($unserializedContext->hasPersistentVar('Persist me')); 18 | static::assertEquals('I am a variable!', $unserializedContext->getPersistentVar('Persist me')); 19 | } 20 | 21 | public function testUnsetVariables() 22 | { 23 | $context = new BotmanConversationContext(); 24 | static::expectException(\RuntimeException::class); 25 | $context->getPersistentVar('I do not exist'); 26 | } 27 | } -------------------------------------------------------------------------------- /tests/unit/Botman/AnswerTriggerTest.php: -------------------------------------------------------------------------------- 1 | type()); 16 | static::assertEquals(['value' => 'foo'], $trigger->parameters()); 17 | } 18 | 19 | public function testActivatedByBotmanMessage() 20 | { 21 | $context = new BotmanConversationContext(); 22 | $context->setAnswer(Answer::create('foo')); 23 | $trigger = new AnswerTrigger('foo'); 24 | static::assertTrue($trigger->activated($context)); 25 | } 26 | 27 | public function testNotActivatedByNotMatchingBotmanMessage() 28 | { 29 | $context = new BotmanConversationContext(); 30 | $context->setAnswer(Answer::create('fooman')); 31 | $trigger = new AnswerTrigger('foo'); 32 | static::assertFalse($trigger->activated($context)); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Botman/AnswerTrigger.php: -------------------------------------------------------------------------------- 1 | value = $value; 24 | } 25 | 26 | public function activated(ConversationContext $context) : bool 27 | { 28 | //TODO find out if and in which cases Answer::getValue() should be used instead 29 | return $context instanceof BotmanConversationContext && $context->getAnswer()->__toString() === $this->value; 30 | } 31 | 32 | public function type() : string 33 | { 34 | return __CLASS__; 35 | } 36 | 37 | public function parameters() : array 38 | { 39 | return [ 40 | self::PARAM_VALUE => $this->value 41 | ]; 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magento-hackathon/magebot", 3 | "description": "A Magento 2 integration of the botman chatbot framework", 4 | "type": "magento2-module", 5 | "require": { 6 | "magento/framework": "^100.1", 7 | "mpociot/botman": "^1.4" 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "^6.0" 11 | }, 12 | "autoload": { 13 | "files": [ 14 | "registration.php" 15 | ], 16 | "psr-4": { 17 | "FireGento\\MageBot\\": "src" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "FireGento\\MageBot\\": "tests" 23 | } 24 | }, 25 | "repositories": [ 26 | { 27 | "type": "composer", 28 | "url": "https://repo.magento.com/" 29 | } 30 | ], 31 | "license": "OSL 3.0", 32 | "authors": [ 33 | { 34 | "name": "Fabian Schmengler", 35 | "email": "fs@integer-net.de" 36 | }, 37 | { 38 | "name": "Harald Deiser", 39 | "email": "h.deiser@techdivision.com" 40 | } 41 | ], 42 | "minimum-stability": "stable" 43 | } 44 | -------------------------------------------------------------------------------- /src/StateMachine/ActionList.php: -------------------------------------------------------------------------------- 1 | execute($context); 24 | } 25 | } 26 | 27 | public function serialize() 28 | { 29 | $this->makeActionsSerializable(); 30 | return parent::serialize(); 31 | } 32 | 33 | public function jsonSerialize() 34 | { 35 | $this->makeActionsSerializable(); 36 | return $this->getArrayCopy(); 37 | } 38 | 39 | private function makeActionsSerializable() 40 | { 41 | foreach ($this as $key => $action) { 42 | $this[$key] = new SerializableAction($action); 43 | } 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/StateSetTest.php: -------------------------------------------------------------------------------- 1 | getArrayCopy(), 18 | '', 0.0, 10, true 19 | ); 20 | } 21 | public function testStateSetCanBeExtended() 22 | { 23 | $stateA = ConversationState::createWithoutActions('A'); 24 | $stateB = ConversationState::createWithoutActions('B'); 25 | $stateSet = new StateSet($stateA); 26 | static::assertEquals( 27 | [$stateA, $stateB], 28 | \iterator_to_array($stateSet->with($stateB)), 29 | '', 0.0, 10, true 30 | ); 31 | static::assertEquals( 32 | [$stateA], 33 | $stateSet->getArrayCopy() 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/StateMachine/TriggerList.php: -------------------------------------------------------------------------------- 1 | activated($context)) { 24 | return true; 25 | } 26 | } 27 | return false; 28 | } 29 | 30 | public function serialize() 31 | { 32 | $this->makeTriggersSerializable(); 33 | return parent::serialize(); 34 | } 35 | 36 | public function jsonSerialize() 37 | { 38 | $this->makeTriggersSerializable(); 39 | return $this->getArrayCopy(); 40 | } 41 | 42 | private function makeTriggersSerializable() 43 | { 44 | foreach ($this as $key => $trigger) { 45 | $this[$key] = new SerializableTrigger($trigger); 46 | } 47 | } 48 | 49 | 50 | } -------------------------------------------------------------------------------- /src/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/unit/Botman/ConversationDefinitionListTest.php: -------------------------------------------------------------------------------- 1 | createMock(BotMan::class); 14 | $botman->expects(static::exactly(2)) 15 | ->method('hears') 16 | ->withConsecutive( 17 | ['pattern1', static::isInstanceOf(\Closure::class)], 18 | ['pattern2', static::isInstanceOf(\Closure::class)] 19 | )->willReturnCallback( 20 | function($pattern, $callback) use ($botman) { 21 | $callback($botman); 22 | } 23 | ); 24 | $botman->expects(static::exactly(2)) 25 | ->method('startConversation') 26 | ->with(static::isInstanceOf(BotmanConversation::class)); 27 | $conversationDefinitionList = new ConversationDefinitionList( 28 | $this->conversationDefinitionStub('pattern1'), $this->conversationDefinitionStub('pattern2') 29 | ); 30 | $conversationDefinitionList->register($botman); 31 | } 32 | 33 | private function conversationDefinitionStub(string $pattern1) : ConversationDefinition 34 | { 35 | $stub = $this->createMock(ConversationDefinition::class); 36 | $stub->method('patternToStart')->willReturn($pattern1); 37 | return $stub; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Botman/BotmanConversationContext.php: -------------------------------------------------------------------------------- 1 | answer; 22 | } 23 | public function setAnswer(Answer $answer) 24 | { 25 | $this->answer = $answer; 26 | } 27 | 28 | public function setPersistentVar(string $key, $value) 29 | { 30 | $this->persistentVars[$key] = $value; 31 | } 32 | 33 | public function hasPersistentVar(string $key) : bool 34 | { 35 | return array_key_exists($key, $this->persistentVars); 36 | } 37 | 38 | public function getPersistentVar(string $key) 39 | { 40 | if (! $this->hasPersistentVar($key)) { 41 | throw new \RuntimeException("Persistent variable {$key} is not set."); 42 | } 43 | return $this->persistentVars[$key]; 44 | } 45 | 46 | public function serialize() 47 | { 48 | return json_encode($this->persistentVars); 49 | } 50 | 51 | public function unserialize($serialized) 52 | { 53 | $this->persistentVars = json_decode($serialized, true); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/StateMachine/Serialization/LazyLoadingAction.php: -------------------------------------------------------------------------------- 1 | actionFactory = $actionFactory; 34 | $this->type = $type; 35 | $this->parameters = $parameters; 36 | } 37 | 38 | public function type() : string 39 | { 40 | return $this->type; 41 | } 42 | 43 | public function parameters() : array 44 | { 45 | return $this->parameters; 46 | } 47 | 48 | public function execute(ConversationContext $context) 49 | { 50 | $this->loadedAction()->execute($context); 51 | } 52 | 53 | /** 54 | * @return Action 55 | */ 56 | private function loadedAction() : Action 57 | { 58 | if ($this->action === null) { 59 | $this->action = $this->actionFactory->create($this->type, $this->parameters); 60 | } 61 | return $this->action; 62 | } 63 | 64 | 65 | } -------------------------------------------------------------------------------- /src/StateMachine/Serialization/LazyLoadingTrigger.php: -------------------------------------------------------------------------------- 1 | triggerFactory = $triggerFactory; 31 | $this->type = $type; 32 | $this->parameters = $parameters; 33 | } 34 | 35 | public function activated(ConversationContext $context) : bool 36 | { 37 | return $this->loadedTrigger()->activated($context); 38 | } 39 | 40 | public function type() : string 41 | { 42 | return $this->type; 43 | } 44 | 45 | public function parameters() : array 46 | { 47 | return $this->parameters; 48 | } 49 | 50 | /** 51 | * @return Trigger 52 | */ 53 | private function loadedTrigger():Trigger 54 | { 55 | if ($this->trigger === null) { 56 | $this->trigger = $this->triggerFactory->create($this->type, $this->parameters); 57 | } 58 | return $this->trigger; 59 | } 60 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/LazyLoadingActionTest.php: -------------------------------------------------------------------------------- 1 | actionFactoryMock = $this->getMockBuilder(ActionFactory::class) 20 | ->getMockForAbstractClass(); 21 | } 22 | 23 | public function testLazyLoad() 24 | { 25 | $parameters = ['foo' => 'bar']; 26 | 27 | $lazyLoadingAction = new LazyLoadingAction($this->actionFactoryMock, FakeAction::class, $parameters); 28 | 29 | $loadedAction = new FakeAction($parameters); 30 | $this->actionFactoryMock->expects(static::once()) 31 | ->method('create') 32 | ->with(FakeAction::class, $parameters) 33 | ->willReturn($loadedAction); 34 | 35 | static::assertEquals(FakeAction::class, $lazyLoadingAction->type()); 36 | static::assertEquals($parameters, $lazyLoadingAction->parameters()); 37 | $lazyLoadingAction->execute($this->createMock(ConversationContext::class)); 38 | $lazyLoadingAction->execute($this->createMock(ConversationContext::class)); 39 | 40 | static::assertEquals(2, $loadedAction->timesExecuted()); 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /tests/integration/Botman/CacheTest.php: -------------------------------------------------------------------------------- 1 | objectManager = ObjectManager::getInstance(); 18 | $this->cache = $this->objectManager->get(CacheInterface::class); 19 | } 20 | public function testInstantiation() 21 | { 22 | $this->assertInstanceOf(CacheBridge::class, $this->cache); 23 | } 24 | /** 25 | * @dataProvider dataSaveCache 26 | */ 27 | public function testSaveCache($key, $value) 28 | { 29 | $this->cache->put($key, $value, 1); 30 | $this->assertEquals($value, $this->cache->get($key)); 31 | } 32 | public static function dataSaveCache() 33 | { 34 | return [ 35 | 'string' => ['foo', 'bar'], 36 | 'array' => ['foo', ['bar' => 'bar']], 37 | 'array_with_object' => [ 38 | 'foo', 39 | [ 40 | 'conversation' => new ConversationStub, 41 | ] 42 | ], 43 | ]; 44 | } 45 | } 46 | 47 | /** 48 | * Anonymous classes cannot be serialized by design, so we need a real stub 49 | */ 50 | class ConversationStub extends Conversation 51 | { 52 | public function run() 53 | { 54 | } 55 | } -------------------------------------------------------------------------------- /src/Botman/CacheBridge.php: -------------------------------------------------------------------------------- 1 | magentoCache = $magentoCache; 23 | } 24 | 25 | public function has($key) 26 | { 27 | return $this->magentoCache->load($key) !== false; 28 | } 29 | 30 | public function get($key, $default = null) 31 | { 32 | if (! $this->has($key)) { 33 | return $default; 34 | } 35 | return $this->deserializeValue($this->magentoCache->load($key)); 36 | } 37 | 38 | public function pull($key, $default = null) 39 | { 40 | $value = $this->get($key, $default); 41 | $this->magentoCache->remove($key); 42 | return $value; 43 | } 44 | 45 | public function put($key, $value, $minutes) 46 | { 47 | $this->magentoCache->save($this->serializeValue($value), $key, [self::CACHE_TAG_BOTMAN], $minutes * 60); 48 | } 49 | 50 | private function serializeValue($value) : string 51 | { 52 | return \serialize($value); 53 | } 54 | 55 | private function deserializeValue(string $value) 56 | { 57 | return \unserialize($value, ['allowed_classes' => true]); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/Botman/BotmanConversation.php: -------------------------------------------------------------------------------- 1 | bot = $bot; 19 | $this->stateMachine = $stateMachine; 20 | } 21 | 22 | public function run() 23 | { 24 | $this->continue(BotMan\Answer::create()); 25 | } 26 | 27 | /** 28 | * continue() is used as central "next" callback for Botman. The first parameter is always an Answer object, 29 | * the last one is the conversation itself (not needed here) 30 | * 31 | * Additional parameters are added for conditional callbacks with pattern matching, but we do not use these, 32 | * everything is routed through continue() and handled by the state machine. 33 | * 34 | * @see \Mpociot\BotMan\BotMan::loadActiveConversation() 35 | * 36 | * @param BotMan\Answer $answer 37 | */ 38 | public function continue(BotMan\Answer $answer) 39 | { 40 | /** @var BotmanConversationContext $context */ 41 | $context = $this->stateMachine->context(); 42 | $context->setAnswer($answer); 43 | $this->stateMachine = $this->stateMachine->continue(); 44 | //in PHP 7.1: Closure::fromCallable() 45 | $next = function(BotMan\Answer $answer) { 46 | $this->continue($answer); 47 | }; 48 | $this->bot->storeConversation($this, $next); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/LazyLoadingTriggerTest.php: -------------------------------------------------------------------------------- 1 | triggerFactoryMock = $this->getMockBuilder(TriggerFactory::class) 20 | ->getMockForAbstractClass(); 21 | } 22 | 23 | public function testLazyLoad() 24 | { 25 | $type = 'MockTrigger'; 26 | $parameters = ['foo' => 'bar']; 27 | 28 | $lazyLoadingTrigger = new LazyLoadingTrigger($this->triggerFactoryMock, $type, $parameters); 29 | 30 | $loadedTrigger = $this->getMockBuilder(Trigger::class) 31 | ->getMock(); 32 | $loadedTrigger->method('type')->willReturn($type); 33 | $loadedTrigger->method('parameters')->willReturn($parameters); 34 | $loadedTrigger->expects(static::exactly(2)) 35 | ->method('activated') 36 | ->willReturn(false); 37 | $this->triggerFactoryMock->expects(static::once()) 38 | ->method('create') 39 | ->with($type, $parameters) 40 | ->willReturn($loadedTrigger); 41 | 42 | static::assertEquals($type, $lazyLoadingTrigger->type()); 43 | static::assertEquals($parameters, $lazyLoadingTrigger->parameters()); 44 | static::assertFalse($lazyLoadingTrigger->activated($this->createMock(ConversationContext::class))); 45 | static::assertFalse($lazyLoadingTrigger->activated($this->createMock(ConversationContext::class))); 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /src/StateMachine/ConversationTransition.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | $this->source = $source; 29 | $this->target = $target; 30 | $this->triggers = $triggers; 31 | } 32 | public function name() : string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function target() : State 38 | { 39 | return $this->target; 40 | } 41 | 42 | public function triggeredAt(State $currentState, ConversationContext $context) : bool 43 | { 44 | return $currentState == $this->source && $this->triggers->anyActivated($context); 45 | } 46 | 47 | /** 48 | * Array representation that can be used to store the transition definition in a database. Source and target states 49 | * still must be mapped to an ID by the repository 50 | * 51 | * @return array 52 | */ 53 | public function toArray() : array 54 | { 55 | return [ 56 | 'name' => $this->name, 57 | 'source' => $this->source, 58 | 'target' => $this->target, 59 | 'triggers' => json_encode($this->triggers) 60 | ]; 61 | } 62 | } -------------------------------------------------------------------------------- /src/StateMachine/ConversationState.php: -------------------------------------------------------------------------------- 1 | name = $name; 34 | $this->entryActions = $entryActions; 35 | $this->exitActions = $exitActions; 36 | } 37 | public function name() : string 38 | { 39 | return $this->name; 40 | } 41 | 42 | public function entryActions() : Actions 43 | { 44 | return $this->entryActions; 45 | } 46 | 47 | public function exitActions() : Actions 48 | { 49 | return $this->exitActions; 50 | } 51 | 52 | /** 53 | * Array representation that can be used to store the state definition in a database. 54 | * 55 | * @return string[] 56 | */ 57 | public function toArray() : array 58 | { 59 | return [ 60 | 'name' => $this->name, 61 | 'entry_actions' => json_encode($this->entryActions()), 62 | 'exit_actions' => json_encode($this->exitActions()), 63 | ]; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Botman/FakeDriver.php: -------------------------------------------------------------------------------- 1 | matchesRequest; 27 | } 28 | 29 | public function getMessages() 30 | { 31 | return $this->messages; 32 | } 33 | 34 | public function isBot() 35 | { 36 | return $this->isBot; 37 | } 38 | 39 | public function isConfigured() 40 | { 41 | return $this->isConfigured; 42 | } 43 | 44 | public function getUser(Message $matchingMessage) 45 | { 46 | return new User($matchingMessage->getUser()); 47 | } 48 | 49 | public function getConversationAnswer(Message $message) 50 | { 51 | return Answer::create($message->getMessage())->setMessage($message); 52 | } 53 | 54 | public function reply($message, $matchingMessage, $additionalParameters = []) 55 | { 56 | $this->botMessages[] = $message; 57 | return $this; 58 | } 59 | 60 | public function getName() 61 | { 62 | return 'Fake Driver'; 63 | } 64 | 65 | public function types(Message $matchingMessage) 66 | { 67 | $this->botIsTyping = true; 68 | } 69 | 70 | /** 71 | * @return bool 72 | */ 73 | public function isBotTyping() 74 | { 75 | return $this->botIsTyping; 76 | } 77 | 78 | /** 79 | * @return string[]|Question[] 80 | */ 81 | public function getBotMessages() 82 | { 83 | return $this->botMessages; 84 | } 85 | 86 | public function resetBotMessages() 87 | { 88 | $this->botMessages = []; 89 | } 90 | } -------------------------------------------------------------------------------- /src/MageBot.php: -------------------------------------------------------------------------------- 1 | config = $config; 40 | $this->cache = $cache; 41 | $this->customerGroupConversation = $customerGroupConversation; 42 | } 43 | 44 | public function start() 45 | { 46 | DriverManager::loadDriver(BotmanWebDriver::class); 47 | 48 | /** @var BotMan $botman */ 49 | $botman = BotManFactory::create($this->config->toArray(), $this->cache); 50 | $this->config->configureBotMan($botman); 51 | 52 | // this is how it should work: 53 | // $conversations = new ConversationDefinitionList(...$this->conversationRepository->findAllActive()); 54 | // $conversations->register($botman); 55 | // --- 56 | 57 | $botman->hears('hi|hello|hallo', function (BotMan $bot) { 58 | $bot->reply('Hi! We sell really great stuff. But first, I\'ll ask you a few questions to understand your need.'); 59 | $bot->startConversation($this->customerGroupConversation); 60 | }); 61 | 62 | $botman->listen(); 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /src/Botman/ProxyDriver.php: -------------------------------------------------------------------------------- 1 | matchesRequest(); 39 | } 40 | 41 | public function getMessages() 42 | { 43 | return self::instance()->getMessages(); 44 | } 45 | 46 | public function isBot() 47 | { 48 | return self::instance()->isBot(); 49 | } 50 | 51 | public function isConfigured() 52 | { 53 | return self::instance()->isConfigured(); 54 | } 55 | 56 | public function getUser(Message $matchingMessage) 57 | { 58 | return self::instance()->getUser($matchingMessage); 59 | } 60 | 61 | public function getConversationAnswer(Message $message) 62 | { 63 | return self::instance()->getConversationAnswer($message); 64 | } 65 | 66 | public function reply($message, $matchingMessage, $additionalParameters = []) 67 | { 68 | return self::instance()->reply($message, $matchingMessage, $additionalParameters); 69 | } 70 | 71 | public function getName() 72 | { 73 | return self::instance()->getName(); 74 | } 75 | 76 | public function types(Message $matchingMessage) 77 | { 78 | return self::instance()->types($matchingMessage); 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/Botman/BotmanWebDriver.php: -------------------------------------------------------------------------------- 1 | request = $request; 31 | } 32 | 33 | public function matchesRequest() 34 | { 35 | return $this->request->get('test'); 36 | } 37 | 38 | public function getMessages() 39 | { 40 | return [ 41 | new Message($this->request->get('msg'), 'dummy user', 'dummy channel') 42 | ]; 43 | } 44 | 45 | public function isBot() 46 | { 47 | return false; 48 | } 49 | 50 | public function isConfigured() 51 | { 52 | return true; 53 | } 54 | 55 | public function getUser(Message $matchingMessage) 56 | { 57 | return new User(); 58 | } 59 | 60 | public function getConversationAnswer(Message $message) 61 | { 62 | return Answer::create($message->getMessage())->setMessage($message); 63 | } 64 | 65 | public function reply($message, $matchingMessage, $additionalParameters = []) 66 | { 67 | $this->replies[] = [ 68 | 'message' => $message, 69 | 'parameters' => $additionalParameters, 70 | ]; 71 | } 72 | 73 | public function getName() 74 | { 75 | return self::DRIVER_NAME; 76 | } 77 | 78 | public function __destruct() 79 | { 80 | // cannot use DI with botman drivers or access botman driver from third party code, so just echo at the end of the request: 81 | if (!empty($this->replies)) { 82 | header('Content-type: application/json; charset=utf-8', true); 83 | echo json_encode($this->replies); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/SerializableTriggerTest.php: -------------------------------------------------------------------------------- 1 | triggerFactoryMock = $this->getMockBuilder(TriggerFactory::class) 21 | ->getMockForAbstractClass(); 22 | SerializableTrigger::setTriggerFactory($this->triggerFactoryMock); 23 | } 24 | protected function tearDown() 25 | { 26 | SerializableTrigger::resetTriggerFactory(); 27 | } 28 | 29 | public function testRecreateSerializedTrigger() 30 | { 31 | $parameters = ['foo' => 'bar', 'activated' => false]; 32 | $trigger = new FakeTrigger($parameters); 33 | $this->triggerFactoryMock->expects(static::once()) 34 | ->method('create') 35 | ->with(FakeTrigger::class, $parameters) 36 | ->willReturn($trigger); 37 | 38 | $serializedTrigger = serialize(new SerializableTrigger($trigger)); 39 | /** @var SerializableTrigger $unserializedTrigger */ 40 | $unserializedTrigger = unserialize($serializedTrigger); 41 | 42 | static::assertEquals($parameters, $unserializedTrigger->parameters()); 43 | static::assertFalse($unserializedTrigger->activated($this->createMock(ConversationContext::class))); 44 | } 45 | 46 | public function testPreventMultipleDecoration() 47 | { 48 | $parameters = ['boo' => 'far', 'activated' => true]; 49 | $trigger = new FakeTrigger($parameters); 50 | $this->triggerFactoryMock->expects(static::once()) 51 | ->method('create') 52 | ->with(FakeTrigger::class, $parameters) 53 | ->willReturn($trigger); 54 | 55 | $serialized = serialize(new SerializableTrigger(new SerializableTrigger($trigger))); 56 | /** @var SerializableTrigger $unserializedTrigger */ 57 | $unserializedTrigger = unserialize($serialized); 58 | static::assertEquals($parameters, $unserializedTrigger->parameters()); 59 | static::assertTrue($unserializedTrigger->activated($this->createMock(ConversationContext::class))); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/StateMachine/Serialization/SerializableAction.php: -------------------------------------------------------------------------------- 1 | action = $action; 48 | } 49 | 50 | public function jsonSerialize() 51 | { 52 | return [ 53 | 'type' => $this->action->type(), 54 | 'parameters' => $this->action->parameters() 55 | ]; 56 | } 57 | 58 | public function serialize() : string 59 | { 60 | return json_encode( 61 | array_values($this->jsonSerialize()) 62 | ); 63 | } 64 | 65 | public function unserialize($serialized) 66 | { 67 | list($type, $params) = json_decode($serialized, true); 68 | $this->action = static::$actionFactory->create($type, $params); 69 | } 70 | 71 | public function type() : string 72 | { 73 | return get_class($this->action); 74 | } 75 | 76 | public function parameters() : array 77 | { 78 | return $this->action->parameters(); 79 | } 80 | 81 | public function execute(ConversationContext $context) 82 | { 83 | $this->action->execute($context); 84 | } 85 | 86 | } 87 | SerializableAction::resetActionFactory(); -------------------------------------------------------------------------------- /src/StateMachine/Serialization/SerializableTrigger.php: -------------------------------------------------------------------------------- 1 | trigger = $trigger; 48 | } 49 | 50 | public function jsonSerialize():array 51 | { 52 | return [ 53 | 'type' => $this->trigger->type(), 54 | 'parameters' => $this->trigger->parameters() 55 | ]; 56 | } 57 | 58 | public function serialize() : string 59 | { 60 | return json_encode( 61 | array_values($this->jsonSerialize()) 62 | ); 63 | } 64 | 65 | public function unserialize($serialized) 66 | { 67 | list($type, $params) = json_decode($serialized, true); 68 | $this->trigger = static::$triggerFactory->create($type, $params); 69 | } 70 | 71 | public function type() : string 72 | { 73 | return get_class($this->trigger); 74 | } 75 | 76 | public function parameters() : array 77 | { 78 | return $this->trigger->parameters(); 79 | } 80 | 81 | public function activated(ConversationContext $context) : bool 82 | { 83 | return $this->trigger->activated($context); 84 | } 85 | 86 | } 87 | SerializableTrigger::resetTriggerFactory(); -------------------------------------------------------------------------------- /src/StateMachine/Conversation.php: -------------------------------------------------------------------------------- 1 | contains($currentState)) { 20 | throw new \DomainException('Current state must be element of known states'); 21 | } 22 | $this->context = $context; 23 | $this->states = $states; 24 | $this->currentState = $currentState; 25 | $this->transitions = $transitions; 26 | } 27 | 28 | public static function createUnstarted(States $states, Transitions $transitions, State $initialState, ConversationContext $context) : Conversation 29 | { 30 | if (!$states->contains($initialState)) { 31 | throw new \DomainException('Initial state must be element of known states'); 32 | } 33 | $unstarted = new UnstartedState(); 34 | $initialTransition = new InitialTransition($initialState); 35 | return static::create($states->with($unstarted), $transitions->prepend($initialTransition), $unstarted, $context); 36 | } 37 | 38 | public static function create(States $states, Transitions $transitions, State $currentState, ConversationContext $context) : Conversation 39 | { 40 | return new static($states, $transitions, $currentState, $context); 41 | } 42 | 43 | public function context() : ConversationContext 44 | { 45 | return $this->context; 46 | } 47 | 48 | public function state() : State 49 | { 50 | return $this->currentState; 51 | } 52 | 53 | //TODO consider removing the states reference, it should only be needed in ConversationDefinition if at all 54 | public function states() : States 55 | { 56 | return $this->states; 57 | } 58 | 59 | public function continue() : StateMachine 60 | { 61 | $nextState = $this->transitions->nextState($this->currentState, $this->context); 62 | if ($nextState !== $this->currentState) { 63 | $this->currentState->exitActions()->executeAll($this->context); 64 | $nextState->entryActions()->executeAll($this->context); 65 | return static::create($this->states, $this->transitions, $nextState, $this->context); 66 | } 67 | return $this; 68 | } 69 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/SerializableActionTest.php: -------------------------------------------------------------------------------- 1 | actionFactoryMock = $this->getMockBuilder(ActionFactory::class) 21 | ->getMockForAbstractClass(); 22 | SerializableAction::setActionFactory($this->actionFactoryMock); 23 | } 24 | protected function tearDown() 25 | { 26 | SerializableAction::resetActionFactory(); 27 | } 28 | 29 | public function testRecreateSerializedAction() 30 | { 31 | $parameters = ['foo' => 'bar']; 32 | $action = new FakeAction($parameters); 33 | $this->actionFactoryMock->expects(static::once()) 34 | ->method('create') 35 | ->with(FakeAction::class, $parameters) 36 | ->willReturn($action); 37 | 38 | $serializedAction = serialize(new SerializableAction($action)); 39 | /** @var SerializableAction $unserializedAction */ 40 | $unserializedAction = unserialize($serializedAction); 41 | 42 | static::assertEquals($parameters, $unserializedAction->parameters()); 43 | static::assertEquals(0, $action->timesExecuted()); 44 | $unserializedAction->execute($this->createMock(ConversationContext::class)); 45 | static::assertEquals(1, $action->timesExecuted()); 46 | } 47 | 48 | public function testPreventMultipleDecoration() 49 | { 50 | $parameters = ['boo' => 'far']; 51 | $action = new FakeAction($parameters); 52 | $this->actionFactoryMock->expects(static::once()) 53 | ->method('create') 54 | ->with(FakeAction::class, $parameters) 55 | ->willReturn($action); 56 | 57 | $serialized = serialize(new SerializableAction(new SerializableAction($action))); 58 | /** @var SerializableAction $deserialized */ 59 | $deserialized = unserialize($serialized); 60 | static::assertEquals($parameters, $deserialized->parameters()); 61 | 62 | static::assertEquals($parameters, $deserialized->parameters()); 63 | static::assertEquals(0, $action->timesExecuted()); 64 | $deserialized->execute($this->createMock(ConversationContext::class)); 65 | static::assertEquals(1, $action->timesExecuted()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Botman/QuestionAction.php: -------------------------------------------------------------------------------- 1 | botman = $botman; 43 | $this->message = $message; 44 | $this->answers = $answers; 45 | } 46 | 47 | private static function validateAnswer($answer) 48 | { 49 | if (! is_array($answer)) { 50 | throw new \DomainException('Answer definition must be array'); 51 | } 52 | if (! array_key_exists(self::ANSWER_PARAM_TEXT, $answer)) { 53 | throw new \DomainException('Answer definition must contain "text"'); 54 | } 55 | if (! array_key_exists(self::ANSWER_PARAM_VALUE, $answer)) { 56 | throw new \DomainException('Answer definition must contain "value"'); 57 | } 58 | } 59 | 60 | public function type() : string 61 | { 62 | return __CLASS__; 63 | } 64 | 65 | public function parameters() : array 66 | { 67 | return [ 68 | self::PARAM_MESSAGE => $this->message, 69 | self::PARAM_ANSWERS => $this->answers, 70 | ]; 71 | } 72 | 73 | public function execute(ConversationContext $context) 74 | { 75 | $this->botman->reply( 76 | Question::create($this->message)->addButtons( 77 | array_map( 78 | function(array $answer) : Button { 79 | return Button::create($answer[QuestionAction::ANSWER_PARAM_TEXT]) 80 | ->value($answer[QuestionAction::ANSWER_PARAM_VALUE]) 81 | ->image($answer[QuestionAction::ANSWER_PARAM_IMAGE_URL] ?? ''); 82 | }, 83 | $this->answers 84 | ) 85 | ) 86 | ); 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /tests/unit/Botman/QuestionActionTest.php: -------------------------------------------------------------------------------- 1 | 'yes', 'value' => 1], 19 | ['text' => 'no', 'value' => 0], 20 | ]; 21 | $action = new QuestionAction( 22 | $this->createMock(BotMan::class), 23 | $message, $answers 24 | ); 25 | static::assertEquals(QuestionAction::class, $action->type()); 26 | static::assertEquals( 27 | [ 28 | 'message' => $message, 29 | 'answers' => $answers, 30 | ], 31 | $action->parameters() 32 | ); 33 | } 34 | 35 | /** 36 | * @dataProvider dataInvalidDefinitions 37 | */ 38 | public function testInvalidDefinition(array $answers) 39 | { 40 | $this->expectException(\DomainException::class); 41 | new QuestionAction( 42 | $this->createMock(BotMan::class), 43 | 'what?', 44 | $answers 45 | ); 46 | } 47 | 48 | public static function dataInvalidDefinitions() 49 | { 50 | return [ 51 | 'answer is not array' => [ 52 | ['answer!'] 53 | ], 54 | 'answer does not contain text' => [ 55 | [['value' => 'only-value']] 56 | ], 57 | 'answer does not contain value' => [ 58 | [['text' => 'only text']] 59 | ], 60 | ]; 61 | } 62 | 63 | public function testSendMessage() 64 | { 65 | $message = 'Top or flop?'; 66 | $answers = [ 67 | ['text' => 'top', 'value' => 1, 'image_url' => 'hop.png'], 68 | ['text' => 'flop', 'value' => -1, 'image_url' => 'flop.gif'], 69 | ['text' => 'meh', 'value' => 0] 70 | ]; 71 | 72 | $botMock = $this->createMock(BotMan::class); 73 | $botMock->expects(static::once()) 74 | ->method('reply') 75 | ->willReturnCallback(function(Question $question) use ($message, $answers) { 76 | static::assertEquals($message, $question->getText()); 77 | $buttons = $question->getButtons(); 78 | static::assertCount(\count($answers), $buttons); 79 | reset($buttons); 80 | foreach ($answers as $expectedAnswer) { 81 | static::assertArraySubset($expectedAnswer, current($buttons)); 82 | next($buttons); 83 | } 84 | }); 85 | 86 | $action = new QuestionAction( 87 | $botMock, 88 | $message, $answers 89 | ); 90 | $action->execute($this->createMock(ConversationContext::class)); 91 | } 92 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/ConversationTransitionTest.php: -------------------------------------------------------------------------------- 1 | notSerializableTrigger(false), 32 | $this->notSerializableTrigger(true) 33 | ) 34 | ); 35 | $serializedTransition = serialize($transition); 36 | /** @var ConversationTransition $unserializedTransition */ 37 | $unserializedTransition = unserialize($serializedTransition); 38 | 39 | static::assertTrue($unserializedTransition->triggeredAt($stateFrom, $this->createMock(ConversationContext::class))); 40 | } 41 | 42 | public function testToArray() 43 | { 44 | $stateFrom = ConversationState::createWithoutActions('denmark'); 45 | $stateTo = ConversationState::createWithoutActions('germany'); 46 | $transition = ConversationTransition::create( 47 | 'shopping tour', 48 | $stateFrom, 49 | $stateTo, 50 | new TriggerList( 51 | new FakeTrigger(['alcohol' => 0]) 52 | ) 53 | ); 54 | $transitionAsArray = $transition->toArray(); 55 | static::assertEquals( 56 | ['name', 'source', 'target', 'triggers'], 57 | array_keys($transitionAsArray) 58 | ); 59 | static::assertEquals('shopping tour', $transitionAsArray['name']); 60 | static::assertSame($stateFrom, $transitionAsArray['source']); 61 | static::assertSame($stateTo, $transitionAsArray['target']); 62 | static::assertJsonStringEqualsJsonString( 63 | json_encode([ 64 | ['type' => FakeTrigger::class, 'parameters' => ['alcohol' => 0]] 65 | ]), 66 | $transitionAsArray['triggers'] 67 | ); 68 | } 69 | 70 | private function notSerializableTrigger($activated) : Trigger 71 | { 72 | $trigger = new FakeTrigger(['activated' => $activated]); 73 | $trigger->evilProperty = new class 74 | { 75 | // in case that serialization of anonymous classes is allowed in future PHP versions: 76 | public function __sleep() 77 | { 78 | throw new \Exception('I should not be serialized'); 79 | } 80 | }; 81 | return $trigger; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/unit/StateMachine/ConversationStateTest.php: -------------------------------------------------------------------------------- 1 | name()); 26 | static::assertEquals(new ActionList, $state->entryActions()); 27 | static::assertEquals(new ActionList, $state->exitActions()); 28 | } 29 | public function testCreateWithActions() 30 | { 31 | $entryActions = $this->dummyActionList(); 32 | $exitActions = $this->dummyActionList(); 33 | $state = ConversationState::createWithActions('both', $entryActions, $exitActions); 34 | static::assertEquals('both', $state->name()); 35 | static::assertSame($entryActions, $state->entryActions()); 36 | static::assertSame($exitActions, $state->exitActions()); 37 | } 38 | public function testCreateWithEntryActions() 39 | { 40 | $entryActions = $this->dummyActionList(); 41 | $state = ConversationState::createWithEntryActions('entry', $entryActions); 42 | static::assertEquals('entry', $state->name()); 43 | static::assertSame($entryActions, $state->entryActions()); 44 | static::assertEquals(new ActionList, $state->exitActions()); 45 | } 46 | public function testCreateWithExitActions() 47 | { 48 | $exitActions = $this->dummyActionList(); 49 | $state = ConversationState::createWithExitActions('exit', $exitActions); 50 | static::assertEquals('exit', $state->name()); 51 | static::assertEquals(new ActionList, $state->entryActions()); 52 | static::assertSame($exitActions, $state->exitActions()); 53 | } 54 | 55 | public function testCanBeSerializedWithActions() 56 | { 57 | $state = ConversationState::createWithEntryActions( 58 | 'state-1', 59 | new ActionList( 60 | $this->notSerializableAction(), 61 | $this->notSerializableAction() 62 | ) 63 | ); 64 | $serializedState = serialize($state); 65 | /** @var ConversationState $unserializedState */ 66 | $unserializedState = unserialize($serializedState); 67 | static::assertCount(2, $unserializedState->entryActions(), 'Unserialized state should still have two entry actions'); 68 | $unserializedState->entryActions()->executeAll($this->createMock(ConversationContext::class)); 69 | } 70 | 71 | public function testToArray() 72 | { 73 | $state = ConversationState::createWithActions( 74 | 'state name', 75 | new ActionList( 76 | new FakeAction(['bot' => 'bender']) 77 | ), 78 | new ActionList( 79 | new FakeAction(['bot' => 'marvin', 'mood' => 'depressed']) 80 | ) 81 | ); 82 | $stateAsArray = $state->toArray(); 83 | static::assertEquals( 84 | ['name', 'entry_actions', 'exit_actions'], 85 | array_keys($stateAsArray) 86 | ); 87 | static::assertEquals('state name', $stateAsArray['name']); 88 | static::assertJsonStringEqualsJsonString( 89 | \json_encode([ 90 | ['type' => FakeAction::class, 'parameters' => ['bot' => 'bender']], 91 | ]), $stateAsArray['entry_actions'] 92 | ); 93 | static::assertJsonStringEqualsJsonString( 94 | \json_encode([ 95 | ['type' => FakeAction::class, 'parameters' => ['bot' => 'marvin', 'mood' => 'depressed']], 96 | ]), $stateAsArray['exit_actions'] 97 | ); 98 | } 99 | 100 | /** 101 | * @return ActionList 102 | */ 103 | private function dummyActionList() : ActionList 104 | { 105 | static $counter = 0; 106 | ++$counter; 107 | return new ActionList( 108 | new CallbackAction( 109 | function () use ($counter) { 110 | return $counter; 111 | } 112 | ) 113 | ); 114 | } 115 | 116 | /** 117 | * @return Action 118 | */ 119 | private function notSerializableAction() : Action 120 | { 121 | $action = new FakeAction([]); 122 | $action->evilProperty = new class 123 | { 124 | // in case that serialization of anonymous classes is allowed in future PHP versions: 125 | public function __sleep() 126 | { 127 | throw new \Exception('I should not be serialized'); 128 | } 129 | }; 130 | return $action; 131 | } 132 | } -------------------------------------------------------------------------------- /src/etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | service 7 | FireGento_MageBot::config_magebot 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Magento\Config\Model\Config\Backend\Encrypted 18 | 19 | 20 | 21 | Magento\Config\Model\Config\Backend\Encrypted 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Magento\Config\Model\Config\Backend\Encrypted 35 | 36 | 37 | 38 | 39 | 40 | 41 | Magento\Config\Model\Config\Backend\Encrypted 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Magento\Config\Model\Config\Backend\Encrypted 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Magento\Config\Model\Config\Backend\Encrypted 63 | 64 | 65 | 66 | Magento\Config\Model\Config\Backend\Encrypted 67 | 68 | 69 | 70 | Magento\Config\Model\Config\Backend\Encrypted 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Magento\Config\Model\Config\Backend\Encrypted 81 | 82 | 83 | 84 |
85 |
86 |
-------------------------------------------------------------------------------- /tests/unit/Botman/BotmanConversationTest.php: -------------------------------------------------------------------------------- 1 | fakeDriver = new FakeDriver(); 54 | ProxyDriver::setInstance($this->fakeDriver); 55 | $this->botman = BotManFactory::create([]); 56 | $conversationDefinitions = new ConversationDefinitionList( 57 | $this->mittagessenConversationDefinition($this->botman) 58 | ); 59 | $conversationDefinitions->register($this->botman); 60 | 61 | SerializableAction::setActionFactory(new BotmanActionFactory($this->botman)); 62 | SerializableTrigger::setTriggerFactory(new NewTriggerFactory); 63 | } 64 | 65 | protected function tearDown() 66 | { 67 | ProxyDriver::setInstance(new NullDriver(new Request, [], new Curl)); 68 | SerializableAction::resetActionFactory(); 69 | SerializableTrigger::resetTriggerFactory(); 70 | } 71 | 72 | public function testConversationWithQuestionAndAnswer() 73 | { 74 | $this->username = 'user123'; 75 | $this->channel = '#mittagessen'; 76 | 77 | $this->userWrites('Mittagessen'); 78 | $this->botman->listen(); 79 | $this->assertBotReplies( 80 | [ 81 | 'Mittagessenplan', 82 | Question::create('Ihr Kind isst')->addButtons( 83 | [ 84 | Button::create('Fisch')->value('fisch'), 85 | Button::create('Fleisch')->value('fleisch') 86 | ] 87 | ) 88 | ] 89 | ); 90 | $this->userWrites('fisch'); 91 | $this->assertBotReplies(['Ihr Kind isst kein Fleisch']); 92 | } 93 | 94 | private function mittagessenConversationDefinition(BotMan $botman) : ConversationDefinition 95 | { 96 | $initialState = ConversationState::createWithEntryActions( 97 | 'state-fisch', new ActionList( 98 | new MessageAction($botman, 'Mittagessenplan'), 99 | new QuestionAction($botman, 'Ihr Kind isst', [['text' => 'Fisch', 'value' => 'fisch'], ['text' => 'Fleisch', 'value' => 'fleisch']]) 100 | ) 101 | ); 102 | $fischState = ConversationState::createWithEntryActions( 103 | 'state-fisch', 104 | new ActionList(new MessageAction($botman, 'Ihr Kind isst kein Fleisch')) 105 | ); 106 | $fleischState = ConversationState::createWithEntryActions( 107 | 'state-fleisch', 108 | new ActionList(new MessageAction($botman, 'Ihr Kind isst kein Fisch')) 109 | ); 110 | $conversation = Conversation::createUnstarted( 111 | new StateSet($initialState, $fischState, $fleischState), 112 | new TransitionList( 113 | ConversationTransition::createAnonymous( 114 | $initialState, $fischState, new TriggerList( 115 | new AnswerTrigger('fisch') 116 | ) 117 | ), 118 | ConversationTransition::createAnonymous( 119 | $initialState, $fleischState, new TriggerList( 120 | new AnswerTrigger('fleisch') 121 | ) 122 | ) 123 | ), 124 | $initialState, new BotmanConversationContext() 125 | ); 126 | return new class($conversation) implements ConversationDefinition { 127 | /** @var Conversation */ 128 | private $conversation; 129 | 130 | public function __construct(Conversation $conversation) 131 | { 132 | $this->conversation = $conversation; 133 | } 134 | 135 | public function patternToStart() : string 136 | { 137 | return 'Mittagessen'; 138 | } 139 | 140 | public function create() : Conversation 141 | { 142 | return $this->conversation; 143 | } 144 | }; 145 | } 146 | 147 | private function userWrites(string $text) 148 | { 149 | $this->fakeDriver->resetBotMessages(); 150 | $this->fakeDriver->messages = [new Message($text, $this->username, $this->channel)]; 151 | $this->botman->loadActiveConversation(); 152 | } 153 | 154 | private function assertBotReplies($replies) 155 | { 156 | static::assertEquals($replies, $this->fakeDriver->getBotMessages()); 157 | } 158 | } -------------------------------------------------------------------------------- /src/Conversations/CustomerGroupConversation.php: -------------------------------------------------------------------------------- 1 | productRepository = $productRepository; 34 | $this->searchCriteriaBuilder = $searchCriteriaBuilder; 35 | $this->imageBuilder = $imageBuilder; 36 | } 37 | 38 | public function askForMainGroup() 39 | { 40 | $botResponse = ButtonTemplate::create('What is your goal?') 41 | ->addButton(ElementButton::create('Loose Weight')->type('postback')->payload('weight')) 42 | ->addButton(ElementButton::create('Eat Healthy')->type('postback')->payload('eat')) 43 | ->addButton(ElementButton::create('Become Fit')->type('postback')->payload('fit')); 44 | 45 | $this->ask($botResponse, function (Answer $answer) { 46 | 47 | $this->group = $answer->getText(); 48 | 49 | if($answer->getText() === "weight") { 50 | $this->askForWeight(); 51 | } else if ($answer->getText() === "eat") { 52 | // Cookbooks 53 | 54 | $this->say('Ok, members who want to keep their weight and eat healthy like our cookbooks:'); 55 | 56 | $products = $this->productRepository->getList($this->searchCriteriaBuilder->create())->getItems(); 57 | $this->returnProducts($products); 58 | 59 | $this->askFinalQuestion(); 60 | } else { 61 | // Fitness Products 62 | 63 | $this->say('Ok, sounds great. Take a look at these fitness products:'); 64 | 65 | $products = $this->productRepository->getList($this->searchCriteriaBuilder->create())->getItems(); 66 | $this->returnProducts($products); 67 | 68 | $this->askFinalQuestion(); 69 | } 70 | }); 71 | } 72 | 73 | private function askForWeight() 74 | { 75 | $question = Question::create('Do you want to lose more than 10kg?') 76 | ->fallback('Unable to ask question') 77 | ->callbackId('ask_approval') 78 | ->addButtons([ 79 | Button::create('Yes')->value('yes'), 80 | Button::create('No')->value('no') 81 | ]); 82 | 83 | $this->ask($question, function (Answer $answer) { 84 | 85 | if($answer->getValue() === "yes") { 86 | 87 | // ask for member status 88 | 89 | $this->askForMemberStatus(); 90 | 91 | } else { 92 | 93 | $this->say('Excellent, take a look at these products:'); 94 | 95 | //$products = $this->productRepository->getList($this->searchCriteriaBuilder->create())->getItems(); 96 | //$this->returnProducts($products); 97 | 98 | //$this->askFinalQuestion(); 99 | } 100 | 101 | }); 102 | } 103 | 104 | public function run() 105 | { 106 | $this->askForMainGroup(); 107 | } 108 | 109 | private function askForMemberStatus() 110 | { 111 | $this->say('Loosing much Weight works best in a group.'); 112 | 113 | $question = Question::create('Are you currently a Weight Watchers member?') 114 | ->fallback('Unable to ask question') 115 | ->callbackId('ask_approval') 116 | ->addButtons([ 117 | Button::create('Yes, I am')->value('yes'), 118 | Button::create('Not yet')->value('no') 119 | ]); 120 | 121 | $this->ask($question, function (Answer $answer) { 122 | 123 | if($answer->getValue() === "yes") { 124 | 125 | $this->say('Nice, you can try our cookbooks for healthier eating to speed up your progress or take alook at our fitness stuff'); 126 | 127 | $products = $this->productRepository->getList($this->searchCriteriaBuilder->create())->getItems(); 128 | $this->returnProducts($products); 129 | 130 | $this->askFinalQuestion(); 131 | 132 | } else { 133 | 134 | $this->say('We like to support you with our best program:'); 135 | 136 | $this->say(GenericTemplate::create() 137 | ->addElement( 138 | Element::create('Feel Good') 139 | ->subtitle('Das Weight Watchers Programm') 140 | ->image('https://www.weightwatchers.com/de/sites/de/files/styles/wwvs_bts_image_thumbnail_large/public/welcher-sporttyp-bist-du_thumbnail.png?itok=oUIUqiuY') 141 | ->addButton(ElementButton::create('Visit') 142 | ->url('https://www.weightwatchers.com/de/programm') 143 | )) 144 | ); 145 | 146 | $this->askFinalQuestion(); 147 | 148 | } 149 | 150 | }); 151 | } 152 | 153 | private function askFinalQuestion() 154 | { 155 | $question = Question::create('Anything else I can help you with?') 156 | ->fallback('Unable to ask question') 157 | ->callbackId('ask_approval') 158 | ->addButtons([ 159 | Button::create('Yes')->value('yes'), 160 | Button::create('No, thanks')->value('no') 161 | ]); 162 | 163 | $this->ask($question, function (Answer $answer) { 164 | 165 | if($answer->getValue() === "yes") { 166 | $this->askForMainGroup(); 167 | } else { 168 | $this->say('Ok, goodbye!'); 169 | } 170 | }); 171 | } 172 | 173 | private function returnProducts($products) 174 | { 175 | $list = GenericTemplate::create(); 176 | 177 | /** @var Product $product */ 178 | foreach ($products as $product) { 179 | $list->addElement( 180 | Element::create($product->getName()) 181 | ->subtitle(strip_tags(html_entity_decode($product->getShortDescription()))) 182 | ->image($this->imageBuilder->setProduct($product) 183 | ->setImageId('product_page_image_large') 184 | ->create()->getImageUrl()) 185 | ->addButton(ElementButton::create('Visit') 186 | ->url($product->getProductUrl()) 187 | )); 188 | } 189 | 190 | $this->say($list); 191 | } 192 | } -------------------------------------------------------------------------------- /tests/unit/StateMachine/ConversationTest.php: -------------------------------------------------------------------------------- 1 | context = $this->createMock(ConversationContext::class); 15 | } 16 | 17 | public function testConversationCanBeCreatedWithState() 18 | { 19 | $initialState = ConversationState::createWithoutActions('initial'); 20 | $states = new StateSet($initialState); 21 | $conversation = Conversation::create($states, new TransitionList(), $initialState, $this->context); 22 | static::assertEquals($initialState, $conversation->state()); 23 | } 24 | public function testConversationCanReturnStateList() 25 | { 26 | $initialState = ConversationState::createWithoutActions('initial'); 27 | $states = new StateSet($initialState, ConversationState::createWithoutActions('state-1'), ConversationState::createWithoutActions('state-2')); 28 | $conversation = Conversation::create($states, new TransitionList(), $initialState, $this->context); 29 | static::assertEquals($states, $conversation->states()); 30 | } 31 | public function testConversationCannotBeCreatedWithUnknownState() 32 | { 33 | $initialState = ConversationState::createWithoutActions('initial'); 34 | $states = new StateSet(ConversationState::createWithoutActions('only-something-else')); 35 | 36 | $this->expectException(\DomainException::class); 37 | Conversation::create($states, new TransitionList(), $initialState, $this->context); 38 | } 39 | 40 | public function testConversationCanBeCreatedInUnstartedState() 41 | { 42 | $initialState = ConversationState::createWithEntryActions('initial', new ActionList($this->actionExpectedNotToBeCalled())); 43 | $states = new StateSet($initialState); 44 | $conversation = Conversation::createUnstarted($states, new TransitionList(), $initialState, $this->context); 45 | static::assertInstanceOf(UnstartedState::class, $conversation->state()); 46 | } 47 | 48 | public function testUnstartedConversationCanBeStarted() 49 | { 50 | $initialState = ConversationState::createWithEntryActions('initial', new ActionList($this->actionExpectedToBeCalled())); 51 | $states = new StateSet($initialState); 52 | $conversation = Conversation::createUnstarted($states, new TransitionList(), $initialState, $this->context); 53 | static::assertSame($initialState, $conversation->continue()->state()); 54 | } 55 | 56 | public function testUnstartedConversationCannotBeCreatedWithUnknownInitialState() 57 | { 58 | $initialState = ConversationState::createWithEntryActions('initial', new ActionList($this->actionExpectedNotToBeCalled())); 59 | $states = new StateSet(ConversationState::createWithoutActions('only-something-else')); 60 | 61 | $this->expectException(\DomainException::class); 62 | Conversation::createUnstarted($states, new TransitionList(), $initialState, $this->context); 63 | } 64 | 65 | public function testTransitionBasedOnTriggers() 66 | { 67 | $initialState = ConversationState::createWithoutActions('initial'); 68 | $alternateState = ConversationState::createWithoutActions('final'); 69 | $unreachableState = ConversationState::createWithoutActions('unreachable'); 70 | /** @var Conversation $conversation */ 71 | $conversation = Conversation::create( 72 | new StateSet($initialState, $alternateState), 73 | new TransitionList( 74 | ConversationTransition::createAnonymous($initialState, $unreachableState, new TriggerList(new FixedTrigger(false))), 75 | ConversationTransition::createAnonymous($initialState, $alternateState, new TriggerList(new FixedTrigger(true))), 76 | ConversationTransition::createAnonymous($alternateState, $initialState, new TriggerList(new FixedTrigger(true))) 77 | ), 78 | $initialState, 79 | $this->context 80 | ); 81 | static::assertEquals($initialState, $conversation->state()); 82 | $conversation = $conversation->continue(); 83 | static::assertEquals($alternateState, $conversation->state()); 84 | $conversation = $conversation->continue(); 85 | static::assertEquals($initialState, $conversation->state()); 86 | } 87 | public function testNewObjectIfStateChanged() 88 | { 89 | $initialState = ConversationState::createWithoutActions('initial'); 90 | $nextState = ConversationState::createWithoutActions('final'); 91 | $conversation = Conversation::create( 92 | new StateSet($initialState, $nextState), 93 | new TransitionList( 94 | ConversationTransition::createAnonymous($initialState, $nextState, new TriggerList(new FixedTrigger(true))) 95 | ), 96 | $initialState, 97 | $this->context 98 | ); 99 | static::assertNotSame($conversation, $conversation->continue(), 'continue() should return new object if state changed'); 100 | static::assertEquals($initialState, $conversation->state(), 'Conversation should be immutable'); 101 | } 102 | public function testNoNewObjectIfStateUnchanged() 103 | { 104 | $initialState = ConversationState::createWithoutActions('initial'); 105 | $conversation = Conversation::create( 106 | new StateSet($initialState), 107 | new TransitionList( 108 | ), 109 | $initialState, 110 | $this->context 111 | ); 112 | static::assertSame($conversation, $conversation->continue(), 'continue() should return new object if state changed'); 113 | } 114 | public function testEntryAndExitActions() 115 | { 116 | $initialState = ConversationState::createWithExitActions('initial', new ActionList($this->actionExpectedToBeCalled())); 117 | $finalState = ConversationState::createWithEntryActions('final', new ActionList($this->actionExpectedToBeCalled())); 118 | $conversation = Conversation::create( 119 | new StateSet($initialState, $finalState), 120 | new TransitionList( 121 | ConversationTransition::createAnonymous($initialState, $finalState, new TriggerList(new FixedTrigger(true))) 122 | ), 123 | $initialState, 124 | $this->context 125 | ); 126 | $conversation->continue()->continue(); 127 | } 128 | 129 | /** 130 | * @return CallbackAction 131 | */ 132 | private function actionExpectedToBeCalled() : CallbackAction 133 | { 134 | $callbackMock = $this->getMockBuilder(\stdClass::class) 135 | ->setMethods(['__invoke']) 136 | ->getMock(); 137 | $callbackMock->expects(static::once())->method('__invoke'); 138 | return new CallbackAction($callbackMock); 139 | } 140 | 141 | /** 142 | * @return CallbackAction 143 | */ 144 | private function actionExpectedNotToBeCalled() : CallbackAction 145 | { 146 | $callbackMock = $this->getMockBuilder(\stdClass::class) 147 | ->setMethods(['__invoke']) 148 | ->getMock(); 149 | $callbackMock->expects(static::never())->method('__invoke'); 150 | return new CallbackAction($callbackMock); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Helper/Config.php: -------------------------------------------------------------------------------- 1 | scopeConfig = $scopeConfig; 76 | $this->logger = $logger; 77 | } 78 | 79 | /** 80 | * Returns the configured value for the config value magebot/hipchat/urls 81 | * 82 | * @return array 83 | */ 84 | public function getMageBotHipChatUrls() 85 | { 86 | $config = $this->scopeConfig->getValue(Config::CONFIG_PATH_HIPCHAT_URLS, ScopeInterface::SCOPE_STORE); 87 | return explode(",", $config); 88 | } 89 | 90 | /** 91 | * Returns the configured value for the config value magebot/nexmo/key 92 | * 93 | * @return string 94 | */ 95 | public function getMageBotNexmoKey() 96 | { 97 | $this->logger->alert(__METHOD__); 98 | $this->logger->alert($this->scopeConfig->getValue(Config::CONFIG_PATH_NEXMO_KEY, ScopeInterface::SCOPE_STORE)); 99 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_NEXMO_KEY, ScopeInterface::SCOPE_STORE); 100 | } 101 | 102 | /** 103 | * Returns the configured value for the config value magebot/nexmo/secret 104 | * 105 | * @return string 106 | */ 107 | public function getMageBotNexmoSecret() 108 | { 109 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_NEXMO_SECRET, ScopeInterface::SCOPE_STORE); 110 | } 111 | 112 | /** 113 | * Returns the configured value for the config value magebot/nexmo/secret 114 | * 115 | * @return string | null 116 | */ 117 | public function getMageBotMicrosoftBotHandle() 118 | { 119 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_MICROSOFT_BOT_HANDLE, ScopeInterface::SCOPE_STORE); 120 | } 121 | 122 | /** 123 | * Returns the configured value for the config value magebot/microsoft/app_id 124 | * 125 | * @return string | null 126 | */ 127 | public function getMageBotMicrosoftAppId() 128 | { 129 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_MICROSOFT_ADD_ID, ScopeInterface::SCOPE_STORE); 130 | } 131 | 132 | /** 133 | * Returns the configured value for the config value magebot/microsoft/app_key 134 | * 135 | * @return string | null 136 | */ 137 | public function getMageBotMicrosoftAppKey() 138 | { 139 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_MICROSOFT_ADD_KEY, ScopeInterface::SCOPE_STORE); 140 | } 141 | 142 | /** 143 | * Returns the configured value for the config value magebot/slack_real_time_api/token 144 | * 145 | * @return string | null 146 | */ 147 | public function getMageBotSlackToken() 148 | { 149 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_SLACK_REAL_TIME_API_TOKEN, ScopeInterface::SCOPE_STORE); 150 | } 151 | 152 | /** 153 | * Returns the configured value for the config value magebot/slack_real_time_api/token 154 | * 155 | * @return string | null 156 | */ 157 | public function getMageBotTelegramToken() 158 | { 159 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_TELEGRAM_API_TOKEN, ScopeInterface::SCOPE_STORE); 160 | } 161 | 162 | /** 163 | * Returns the configured value for the config value magebot/facebook/app_secret 164 | * 165 | * @return string | null 166 | */ 167 | public function getMageBotFacebookToken() 168 | { 169 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_FACEBOOK_TOKEN, ScopeInterface::SCOPE_STORE); 170 | } 171 | 172 | /** 173 | * Returns the configured value for the config value magebot/facebook/app_secret 174 | * 175 | * @return string | null 176 | */ 177 | public function getMageBotFacebookAppSecret() 178 | { 179 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_FACEBOOK_APP_SECRET, ScopeInterface::SCOPE_STORE); 180 | } 181 | 182 | /** 183 | * Returns the configured value for the config value magebot/facebook/webhook_token 184 | * 185 | * @return string | null 186 | */ 187 | private function getMageBotFacebookWebhookToken() 188 | { 189 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_FACEBOOK_WEBHOOK_TOKEN, ScopeInterface::SCOPE_STORE); 190 | } 191 | 192 | /** 193 | * Returns the configured value for the config value magebot/facebook/app_secret 194 | * 195 | * @return string | null 196 | */ 197 | public function getMageBotWechatAppId() 198 | { 199 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_WECHAT_APP_ID, ScopeInterface::SCOPE_STORE); 200 | } 201 | 202 | /** 203 | * Returns the configured value for the config value magebot/facebook/app_secret 204 | * 205 | * @return string | null 206 | */ 207 | public function getMageBotWechatAppKey() 208 | { 209 | return $this->scopeConfig->getValue(Config::CONFIG_PATH_WECHAT_APP_KEY, ScopeInterface::SCOPE_STORE); 210 | } 211 | 212 | /** 213 | * @return array 214 | */ 215 | public function toArray() : array 216 | { 217 | return $config = [ 218 | 'hipchat_urls' => $this->getMageBotHipChatUrls(), 219 | 'nexmo_key' => $this->getMageBotNexmoKey(), 220 | 'nexmo_secret' => $this->getMageBotNexmoSecret(), 221 | 'microsoft_bot_handle' => $this->getMageBotMicrosoftBotHandle(), 222 | 'microsoft_app_id' => $this->getMageBotMicrosoftAppId(), 223 | 'microsoft_app_key' => $this->getMageBotMicrosoftAppKey(), 224 | 'slack_token' => $this->getMageBotSlackToken(), 225 | 'telegram_token' => $this->getMageBotTelegramToken(), 226 | 'facebook_token' => $this->getMageBotFacebookToken(), 227 | 'facebook_app_secret' => $this->getMageBotFacebookAppSecret(), 228 | 'wechat_app_id' => $this->getMageBotWechatAppId(), 229 | 'wechat_app_key' => $this->getMageBotWechatAppKey() 230 | ]; 231 | } 232 | 233 | /** 234 | * @param BotMan $botman 235 | */ 236 | public function configureBotMan(BotMan $botman) 237 | { 238 | if($this->getMageBotFacebookWebhookToken()) { 239 | $botman->verifyServices($this->getMageBotFacebookWebhookToken()); 240 | } 241 | } 242 | } 243 | --------------------------------------------------------------------------------