├── .php_cs ├── .scrutinizer.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── resources └── stubs │ ├── Activator.stub │ ├── Intent.stub │ └── Interaction.stub ├── src ├── Channels │ ├── Channel.php │ ├── ChannelManager.php │ ├── Chat.php │ ├── Driver.php │ ├── Exceptions │ │ ├── ChannelNotFound.php │ │ ├── DriverException.php │ │ ├── DriverNotFound.php │ │ ├── InvalidArgument.php │ │ ├── InvalidConfiguration.php │ │ └── InvalidRequest.php │ └── User.php ├── Contracts │ ├── Channels │ │ ├── Driver.php │ │ ├── Manager.php │ │ └── WebhookVerification.php │ ├── Conversation │ │ ├── Activator.php │ │ ├── Conversable.php │ │ └── Manager.php │ ├── Event.php │ └── Template.php ├── Conversation │ ├── Activators │ │ ├── Attachment.php │ │ ├── Contains.php │ │ ├── Exact.php │ │ ├── In.php │ │ ├── Payload.php │ │ └── Regex.php │ ├── Concerns │ │ ├── Authorization.php │ │ ├── InteractsWithContext.php │ │ └── SendsMessages.php │ ├── Context.php │ ├── ConversationManager.php │ ├── FallbackIntent.php │ ├── Intent.php │ └── Interaction.php ├── Drivers │ ├── PendingReply.php │ └── TemplateCompiler.php ├── Events │ ├── Event.php │ ├── MessageReceived.php │ └── Unknown.php ├── Foundation │ ├── API.php │ ├── Commands │ │ ├── SendAttachment.php │ │ └── SendMessage.php │ ├── Composer.php │ ├── Controller.php │ ├── Kernel.php │ ├── Listeners │ │ └── HandleConversation.php │ ├── Middleware │ │ └── InitializeKernel.php │ ├── Providers │ │ ├── ChannelServiceProvider.php │ │ ├── ConversationServiceProvider.php │ │ ├── EventServiceProvider.php │ │ ├── FoundationServiceProvider.php │ │ └── RouteServiceProvider.php │ └── ServiceProvider.php ├── Framework │ ├── Application.php │ ├── Console │ │ ├── Application.php │ │ └── Kernel.php │ ├── Exceptions │ │ └── Handler.php │ └── Http │ │ └── Kernel.php ├── Templates │ ├── Attachment.php │ ├── Keyboard.php │ ├── Keyboard │ │ ├── Button.php │ │ ├── PayloadButton.php │ │ ├── ReplyButton.php │ │ └── UrlButton.php │ └── Location.php ├── Toolbelt │ ├── InstallDriverCommand.php │ ├── ListChannelsCommand.php │ ├── ListDriversCommand.php │ ├── ListIntentsCommand.php │ ├── MakeActivatorCommand.php │ ├── MakeIntentCommand.php │ ├── MakeInteractionCommand.php │ └── ToolbeltServiceProvider.php └── helpers.php └── tests ├── Mocks ├── FakeChannel.php ├── FakeDriver.php ├── FakeIntent.php ├── FakeIntentWithClosureActivator.php └── FakeInteraction.php ├── TestCase.php └── Unit ├── Channels ├── ChannelManagerTest.php ├── ChatTest.php ├── DriverTest.php └── UserTest.php ├── Conversation ├── Activators │ ├── AttachmentTest.php │ ├── ContainsTest.php │ ├── ExactTest.php │ ├── InArrayTest.php │ ├── PayloadTest.php │ └── RegexTest.php ├── Concerns │ ├── AuthorizationTest.php │ ├── InteractsWithContextTest.php │ └── SendsMessagesTest.php ├── ContextTest.php ├── ConversationManagerTest.php ├── FallbackIntentTest.php ├── IntentTest.php └── InteractionTest.php ├── Drivers └── TemplateCompilerTest.php ├── Foundation ├── Commands │ ├── SendAttachmentTest.php │ └── SendMessageTest.php └── KernelTest.php ├── HelpersTest.php └── Templates ├── AttachmentTest.php ├── Keyboard ├── PayloadButtonTest.php ├── ReplyButtonTest.php └── UrlButtonTest.php ├── KeyboardTest.php └── LocationTest.php /.php_cs: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->in(__DIR__.'/src') 6 | ->in(__DIR__.'/tests') 7 | ->name('*.php') 8 | ->ignoreDotFiles(true) 9 | ->ignoreVCS(true); 10 | 11 | return PhpCsFixer\Config::create() 12 | ->setRules([ 13 | '@PSR1' => false, 14 | '@PSR2' => true, 15 | 'strict_param' => true, 16 | 'declare_strict_types' => true, 17 | 'array_syntax' => ['syntax' => 'short'], 18 | 'binary_operator_spaces' => [ 19 | 'align_equals' => false, 20 | 'align_double_arrow' => null, 21 | ], 22 | 'blank_line_after_opening_tag' => true, 23 | 'blank_line_before_return' => true, 24 | 'cast_spaces' => true, 25 | 'concat_space' => ['spacing' => 'none'], 26 | 'declare_equal_normalize' => true, 27 | 'function_typehint_space' => true, 28 | 'general_phpdoc_annotation_remove' => ['access', 'package', 'subpackage'], 29 | 'hash_to_slash_comment' => true, 30 | 'heredoc_to_nowdoc' => true, 31 | 'include' => true, 32 | 'lowercase_cast' => true, 33 | 'method_separation' => true, 34 | 'native_function_casing' => true, 35 | 'no_alias_functions' => true, 36 | 'no_blank_lines_after_class_opening' => true, 37 | 'no_blank_lines_after_phpdoc' => true, 38 | 'no_empty_phpdoc' => true, 39 | 'no_empty_statement' => true, 40 | 'no_extra_consecutive_blank_lines' => ['throw', 'use', 'useTrait', 'extra'], 41 | 'no_leading_import_slash' => true, 42 | 'no_leading_namespace_whitespace' => true, 43 | 'no_mixed_echo_print' => ['use' => 'echo'], 44 | 'no_multiline_whitespace_around_double_arrow' => true, 45 | 'no_multiline_whitespace_before_semicolons' => true, 46 | 'no_short_bool_cast' => true, 47 | 'no_singleline_whitespace_before_semicolons' => true, 48 | 'no_spaces_around_offset' => ['inside'], 49 | 'no_trailing_comma_in_list_call' => true, 50 | 'no_trailing_comma_in_singleline_array' => true, 51 | 'no_unneeded_control_parentheses' => true, 52 | 'no_unreachable_default_argument_value' => true, 53 | 'no_unused_imports' => true, 54 | 'no_useless_return' => true, 55 | 'no_whitespace_before_comma_in_array' => true, 56 | 'no_whitespace_in_blank_line' => true, 57 | 'normalize_index_brace' => true, 58 | 'object_operator_without_whitespace' => true, 59 | 'ordered_imports' => ['sortAlgorithm' => 'length'], 60 | 'phpdoc_indent' => true, 61 | 'phpdoc_inline_tag' => true, 62 | 'phpdoc_no_alias_tag' => ['type' => 'var'], 63 | 'phpdoc_no_useless_inheritdoc' => true, 64 | 'phpdoc_scalar' => true, 65 | 'phpdoc_single_line_var_spacing' => true, 66 | 'phpdoc_summary' => true, 67 | 'phpdoc_to_comment' => true, 68 | 'phpdoc_trim' => true, 69 | 'phpdoc_types' => true, 70 | 'phpdoc_var_without_name' => true, 71 | 'psr4' => true, 72 | 'self_accessor' => false, 73 | 'short_scalar_cast' => true, 74 | 'single_blank_line_before_namespace' => true, 75 | 'single_quote' => true, 76 | 'space_after_semicolon' => true, 77 | 'standardize_not_equals' => true, 78 | 'ternary_operator_spaces' => true, 79 | 'trailing_comma_in_multiline_array' => true, 80 | 'trim_array_spaces' => true, 81 | 'unary_operator_spaces' => true, 82 | 'visibility_required' => ['method', 'property'], 83 | 'whitespace_after_comma_in_array' => true, 84 | ]) 85 | ->setFinder($finder) 86 | ->setUsingCache(false); 87 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | nodes: 3 | analysis: 4 | tests: 5 | override: 6 | - php-scrutinizer-run 7 | filter: 8 | excluded_paths: 9 | - tests/* 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at vladimir@fondbot.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vladimir Yuldashev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/fondbot/framework/v/stable?format=flat-square)](https://packagist.org/packages/fondbot/framework) 4 | [![Latest Unstable Version](https://poser.pugx.org/fondbot/framework/v/unstable?format=flat-square)](https://packagist.org/packages/fondbot/framework) 5 | [![Quality Score](https://img.shields.io/scrutinizer/g/fondbot/framework.svg?style=flat-square)](https://scrutinizer-ci.com/g/fondbot/framework) 6 | [![License](https://poser.pugx.org/fondbot/framework/license?format=flat-square)](https://packagist.org/packages/fondbot/framework) 7 | [![Slack Invite](https://img.shields.io/badge/slack-invite-red.svg?style=flat-square)](https://slack.fondbot.io/) 8 | 9 | > **Note:** This repository contains the core code of the FondBot framework. If you want to to build chatbot using FondBot framework, visit the main [FondBot repository](https://github.com/fondbot/fondbot). 10 | 11 | ## About FondBot 12 | FondBot is a framework for building chat bots. 13 | 14 | The main goal of this project is to provide elegant and flexible architecture to develop and maintain chatbot projects from small to the big ones. 15 | 16 | ## Installation And Usage 17 | 18 | You can find all installation instructions and other documentation at https://fondbot.io 19 | 20 | ## Security Vulnerabilities 21 | 22 | If you discover a security vulnerability within FondBot, please send an e-mail to Vladimir Yuldashev at vladimir@fondbot.io. All security vulnerabilities will be promptly addressed. 23 | 24 | ## Community 25 | 26 | If you have questions or suggestions you are welcome to our Slack channel: 27 | [https://slack.fondbot.io](https://slack.fondbot.io) 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fondbot/framework", 3 | "description": "FondBot framework.", 4 | "type": "library", 5 | "keywords": [ 6 | "fondbot", 7 | "bot", 8 | "bots", 9 | "chatbots", 10 | "telegram", 11 | "facebook messenger", 12 | "vk" 13 | ], 14 | "license": "MIT", 15 | "homepage": "https://fondbot.io", 16 | "authors": [ 17 | { 18 | "name": "Vladimir Yuldashev", 19 | "email": "vladimir@fondbot.io" 20 | } 21 | ], 22 | "require": { 23 | "php": "^7.1", 24 | "ext-json": "*", 25 | "guzzlehttp/guzzle": "^6.3", 26 | "laravel/framework": "5.6.*" 27 | }, 28 | "require-dev": { 29 | "fzaninotto/faker": "^1.7", 30 | "league/flysystem-memory": "^1.0", 31 | "mockery/mockery": "^1.1", 32 | "orchestra/testbench": "^3.6", 33 | "phpunit/phpunit": "^7.2" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "FondBot\\": "src/" 38 | }, 39 | "files": [ 40 | "src/helpers.php" 41 | ] 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "FondBot\\Tests\\": "tests/" 46 | } 47 | }, 48 | "scripts": { 49 | "test": "vendor/bin/phpunit" 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "3.0-dev" 54 | } 55 | }, 56 | "config": { 57 | "preferred-install": "dist", 58 | "sort-packages": true 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true 62 | } 63 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/stubs/Activator.stub: -------------------------------------------------------------------------------- 1 | name = $name; 16 | $this->driver = $driver; 17 | $this->secret = $secret; 18 | } 19 | 20 | public function getName(): string 21 | { 22 | return $this->name; 23 | } 24 | 25 | public function getDriver(): Driver 26 | { 27 | return $this->driver; 28 | } 29 | 30 | public function getSecret(): ?string 31 | { 32 | return $this->secret; 33 | } 34 | 35 | public function getWebhookUrl(): string 36 | { 37 | return route('fondbot.webhook', [$this->name, $this->secret]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Channels/ChannelManager.php: -------------------------------------------------------------------------------- 1 | channels = collect([]); 28 | } 29 | 30 | /** 31 | * Register channels. 32 | * 33 | * @param array $channels 34 | */ 35 | public function register(array $channels): void 36 | { 37 | $this->channels = collect($channels); 38 | } 39 | 40 | /** 41 | * Get all channels. 42 | * 43 | * @return Collection 44 | */ 45 | public function all(): Collection 46 | { 47 | return $this->channels; 48 | } 49 | 50 | /** 51 | * Get channels by driver. 52 | * 53 | * @param string $driver 54 | * 55 | * @return Collection 56 | */ 57 | public function getByDriver(string $driver): Collection 58 | { 59 | return $this->channels->filter(function (array $channel) use ($driver) { 60 | return $channel['driver'] === $driver; 61 | }); 62 | } 63 | 64 | /** 65 | * Create channel. 66 | * 67 | * @param string $name 68 | * 69 | * @return Channel 70 | * @throws ChannelNotFound 71 | */ 72 | public function create(string $name): Channel 73 | { 74 | if (!array_has($this->channels, $name)) { 75 | throw new ChannelNotFound('Channel `'.$name.'` not found.'); 76 | } 77 | 78 | $parameters = $this->channels[$name]; 79 | 80 | // Create driver and initialize it with channel parameters 81 | $driver = $this->createDriver($parameters['driver']); 82 | $driver->initialize(collect($parameters)->except('driver')); 83 | 84 | return new Channel($name, $driver, $parameters['webhook-secret'] ?? null); 85 | } 86 | 87 | /** 88 | * Get the default driver name. 89 | * 90 | * @return string 91 | */ 92 | public function getDefaultDriver(): ?string 93 | { 94 | return null; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Channels/Chat.php: -------------------------------------------------------------------------------- 1 | id = $id; 19 | $this->title = $title; 20 | $this->type = $type; 21 | } 22 | 23 | public function getId(): string 24 | { 25 | return $this->id; 26 | } 27 | 28 | public function getTitle(): ?string 29 | { 30 | return $this->title; 31 | } 32 | 33 | public function getType(): string 34 | { 35 | return $this->type; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Channels/Driver.php: -------------------------------------------------------------------------------- 1 | templateCompiler = $templateCompiler; 22 | } 23 | 24 | /** 25 | * Get driver short name. 26 | * 27 | * This name is used as an alias for configuration. 28 | * 29 | * @return string 30 | */ 31 | public function getShortName(): string 32 | { 33 | return class_basename($this); 34 | } 35 | 36 | /** 37 | * Initialize driver. 38 | * 39 | * @param Collection $parameters 40 | * 41 | * @return Driver|DriverContract|static 42 | */ 43 | public function initialize(Collection $parameters): DriverContract 44 | { 45 | $parameters->each(function ($value, $key) { 46 | $key = Str::camel($key); 47 | $this->$key = $value; 48 | }); 49 | 50 | $this->client = $this->createClient(); 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Get API client. 57 | * 58 | * @return mixed 59 | */ 60 | public function getClient() 61 | { 62 | return $this->client; 63 | } 64 | 65 | /** 66 | * Create HTTP response. 67 | * 68 | * @param Request $request 69 | * @param Event $event 70 | * 71 | * @return mixed 72 | */ 73 | public function createResponse(Request $request, Event $event) 74 | { 75 | return []; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Channels/Exceptions/ChannelNotFound.php: -------------------------------------------------------------------------------- 1 | $this->getMessage()]); 14 | } 15 | 16 | public function render() 17 | { 18 | return response(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Channels/Exceptions/DriverNotFound.php: -------------------------------------------------------------------------------- 1 | id = $id; 19 | $this->name = $name; 20 | $this->username = $username; 21 | $this->data = collect($data); 22 | } 23 | 24 | public function getId(): string 25 | { 26 | return $this->id; 27 | } 28 | 29 | public function getName(): ?string 30 | { 31 | return $this->name; 32 | } 33 | 34 | public function getUsername(): ?string 35 | { 36 | return $this->username; 37 | } 38 | 39 | /** 40 | * Additional user information. 41 | * 42 | * @return Collection 43 | */ 44 | public function getData(): Collection 45 | { 46 | return $this->data; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Contracts/Channels/Driver.php: -------------------------------------------------------------------------------- 1 | type = $type; 18 | } 19 | 20 | public static function make(string $type = null) 21 | { 22 | return new static($type); 23 | } 24 | 25 | public function file(): self 26 | { 27 | $this->type = Template::TYPE_FILE; 28 | 29 | return $this; 30 | } 31 | 32 | public function image(): self 33 | { 34 | $this->type = Template::TYPE_IMAGE; 35 | 36 | return $this; 37 | } 38 | 39 | public function audio(): self 40 | { 41 | $this->type = Template::TYPE_AUDIO; 42 | 43 | return $this; 44 | } 45 | 46 | public function video(): self 47 | { 48 | $this->type = Template::TYPE_VIDEO; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Result of matching activator. 55 | * 56 | * @param MessageReceived $message 57 | * 58 | * @return bool 59 | */ 60 | public function matches(MessageReceived $message): bool 61 | { 62 | if ($this->type === null) { 63 | return $message->getAttachment() !== null; 64 | } 65 | 66 | return hash_equals($message->getAttachment()->getType(), $this->type); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Conversation/Activators/Contains.php: -------------------------------------------------------------------------------- 1 | toArray(); 22 | } 23 | 24 | $this->needles = $needles; 25 | } 26 | 27 | /** 28 | * @param array|string $needles 29 | * 30 | * @return static 31 | */ 32 | public static function make($needles) 33 | { 34 | return new static($needles); 35 | } 36 | 37 | /** 38 | * Result of matching activator. 39 | * 40 | * @param MessageReceived $message 41 | * 42 | * @return bool 43 | */ 44 | public function matches(MessageReceived $message): bool 45 | { 46 | $text = $message->getText(); 47 | if ($text === null) { 48 | return false; 49 | } 50 | 51 | return str_contains($text, (array) $this->needles); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Conversation/Activators/Exact.php: -------------------------------------------------------------------------------- 1 | value = $value; 19 | } 20 | 21 | public static function make(string $value) 22 | { 23 | return new static($value); 24 | } 25 | 26 | public function caseSensitive(): self 27 | { 28 | $this->caseSensitive = true; 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * Result of matching activator. 35 | * 36 | * @param MessageReceived $message 37 | * 38 | * @return bool 39 | */ 40 | public function matches(MessageReceived $message): bool 41 | { 42 | $text = $message->getText(); 43 | 44 | if ($text === null) { 45 | return false; 46 | } 47 | 48 | if (!$this->caseSensitive) { 49 | $text = Str::lower($text); 50 | $this->value = Str::lower($this->value); 51 | } 52 | 53 | return hash_equals($this->value, $text); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Conversation/Activators/In.php: -------------------------------------------------------------------------------- 1 | toArray(); 24 | } 25 | 26 | $this->values = $values; 27 | } 28 | 29 | /** 30 | * @param array|Collection $values 31 | * 32 | * @return static 33 | */ 34 | public static function make($values) 35 | { 36 | return new static($values); 37 | } 38 | 39 | /** 40 | * Result of matching activator. 41 | * 42 | * @param MessageReceived $message 43 | * 44 | * @return bool 45 | */ 46 | public function matches(MessageReceived $message): bool 47 | { 48 | $haystack = $this->values; 49 | 50 | if ($haystack instanceof Collection) { 51 | $haystack = $haystack->toArray(); 52 | } 53 | 54 | return in_array($message->getText(), $haystack, false); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Conversation/Activators/Payload.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | public static function make(string $value) 20 | { 21 | return new static($value); 22 | } 23 | 24 | /** 25 | * Result of matching activator. 26 | * 27 | * @param MessageReceived $message 28 | * 29 | * @return bool 30 | */ 31 | public function matches(MessageReceived $message): bool 32 | { 33 | return $message->getData() ? hash_equals($this->value, $message->getData()) : false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Conversation/Activators/Regex.php: -------------------------------------------------------------------------------- 1 | toArray(); 20 | } 21 | 22 | $this->patterns = $patterns; 23 | } 24 | 25 | public static function make($patterns) 26 | { 27 | return new static($patterns); 28 | } 29 | 30 | /** 31 | * Result of matching activator. 32 | * 33 | * @param MessageReceived $message 34 | * 35 | * @return bool 36 | */ 37 | public function matches(MessageReceived $message): bool 38 | { 39 | return str_is($this->patterns, $message->getText()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Conversation/Concerns/Authorization.php: -------------------------------------------------------------------------------- 1 | authorize($message); 22 | } 23 | 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Conversation/Concerns/InteractsWithContext.php: -------------------------------------------------------------------------------- 1 | context()->getChannel(); 22 | } 23 | 24 | /** 25 | * Get chat. 26 | * 27 | * @return Chat 28 | */ 29 | protected function getChat(): Chat 30 | { 31 | return $this->context()->getChat(); 32 | } 33 | 34 | /** 35 | * Get user. 36 | * 37 | * @return User 38 | */ 39 | protected function getUser(): User 40 | { 41 | return $this->context()->getUser(); 42 | } 43 | 44 | /** 45 | * Get the whole context or a single value. 46 | * 47 | * @param string|null $key 48 | * @param mixed $default 49 | * 50 | * @return Context|mixed 51 | */ 52 | protected function context(string $key = null, $default = null) 53 | { 54 | return context($key, $default); 55 | } 56 | 57 | /** 58 | * Remember value in context. 59 | * 60 | * @param string $key 61 | * @param mixed $value 62 | */ 63 | protected function remember(string $key, $value): void 64 | { 65 | context()->setItem($key, $value); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Conversation/Concerns/SendsMessages.php: -------------------------------------------------------------------------------- 1 | getChannel(), 23 | context()->getChat(), 24 | context()->getUser() 25 | ))->text($text); 26 | } 27 | 28 | /** 29 | * Send attachment to user. 30 | * 31 | * @param Attachment $attachment 32 | * 33 | * @return PendingReply 34 | */ 35 | protected function sendAttachment(Attachment $attachment): PendingReply 36 | { 37 | return (new PendingReply( 38 | context()->getChannel(), 39 | context()->getChat(), 40 | context()->getUser() 41 | ))->attachment($attachment); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Conversation/Context.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 25 | $this->chat = $chat; 26 | $this->user = $user; 27 | $this->items = collect($items); 28 | } 29 | 30 | public function getChannel(): Channel 31 | { 32 | return $this->channel; 33 | } 34 | 35 | public function getChat(): Chat 36 | { 37 | return $this->chat; 38 | } 39 | 40 | public function getUser(): User 41 | { 42 | return $this->user; 43 | } 44 | 45 | public function getIntent(): ?Intent 46 | { 47 | return $this->intent; 48 | } 49 | 50 | public function setIntent(Intent $intent): Context 51 | { 52 | $this->intent = $intent; 53 | 54 | return $this; 55 | } 56 | 57 | public function getInteraction(): ?Interaction 58 | { 59 | return $this->interaction; 60 | } 61 | 62 | public function setInteraction(?Interaction $interaction): Context 63 | { 64 | $this->interaction = $interaction; 65 | 66 | return $this; 67 | } 68 | 69 | public function getItem(string $key, $default = null) 70 | { 71 | return $this->items->get($key, $default); 72 | } 73 | 74 | public function setItem(string $key, $value): Context 75 | { 76 | $this->items->put($key, $value); 77 | 78 | return $this; 79 | } 80 | 81 | public function incrementAttempts(): Context 82 | { 83 | $this->attempts++; 84 | 85 | return $this; 86 | } 87 | 88 | public function attempts(): int 89 | { 90 | return $this->attempts; 91 | } 92 | 93 | public function toArray(): array 94 | { 95 | return [ 96 | 'intent' => $this->intent ? get_class($this->intent) : null, 97 | 'interaction' => $this->interaction ? get_class($this->interaction) : null, 98 | 'items' => $this->items->toArray(), 99 | 'attempts' => $this->attempts, 100 | ]; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Conversation/ConversationManager.php: -------------------------------------------------------------------------------- 1 | application = $application; 33 | $this->cache = $cache; 34 | } 35 | 36 | /** {@inheritdoc} */ 37 | public function registerIntent(string $class): void 38 | { 39 | $this->intents[] = $class; 40 | } 41 | 42 | /** {@inheritdoc} */ 43 | public function registerFallbackIntent(string $class): void 44 | { 45 | $this->fallbackIntent = $class; 46 | } 47 | 48 | /** {@inheritdoc} */ 49 | public function getIntents(): array 50 | { 51 | return $this->intents; 52 | } 53 | 54 | /** {@inheritdoc} */ 55 | public function matchIntent(MessageReceived $messageReceived): ?Intent 56 | { 57 | foreach ($this->intents as $intent) { 58 | /** @var Intent $intent */ 59 | $intent = resolve($intent); 60 | 61 | foreach ($intent->activators() as $activator) { 62 | if (!$intent->passesAuthorization($messageReceived)) { 63 | continue; 64 | } 65 | 66 | if ($activator instanceof Closure && value($activator($messageReceived)) === true) { 67 | return $intent; 68 | } 69 | 70 | if ($activator instanceof Activator && $activator->matches($messageReceived)) { 71 | return $intent; 72 | } 73 | } 74 | } 75 | 76 | // Otherwise, return fallback intent 77 | return resolve($this->fallbackIntent); 78 | } 79 | 80 | /** {@inheritdoc} */ 81 | public function resolveContext(Channel $channel, Chat $chat, User $user): Context 82 | { 83 | $value = $this->cache->get($this->getCacheKeyForContext($channel, $chat, $user), [ 84 | 'chat' => $chat, 85 | 'user' => $user, 86 | 'intent' => null, 87 | 'interaction' => null, 88 | 'items' => [], 89 | ]); 90 | 91 | $context = new Context($channel, $chat, $user, $value['items'] ?? []); 92 | 93 | if (isset($value['intent'])) { 94 | $context->setIntent(resolve($value['intent'])); 95 | } 96 | 97 | if (isset($value['interaction'])) { 98 | $context->setInteraction(resolve($value['interaction'])); 99 | } 100 | 101 | // Bind resolved instance to the container 102 | $this->application->instance('fondbot.conversation.context', $context); 103 | 104 | return $context; 105 | } 106 | 107 | /** {@inheritdoc} */ 108 | public function saveContext(Context $context): void 109 | { 110 | $this->cache->forever( 111 | $this->getCacheKeyForContext($context->getChannel(), $context->getChat(), $context->getUser()), 112 | $context->toArray() 113 | ); 114 | } 115 | 116 | /** {@inheritdoc} */ 117 | public function flushContext(Context $context): void 118 | { 119 | $this->cache->forget( 120 | $this->getCacheKeyForContext($context->getChannel(), $context->getChat(), $context->getUser()) 121 | ); 122 | } 123 | 124 | /** {@inheritdoc} */ 125 | public function getContext(): ?Context 126 | { 127 | if (!$this->application->has('fondbot.conversation.context')) { 128 | return null; 129 | } 130 | 131 | return $this->application->get('fondbot.conversation.context'); 132 | } 133 | 134 | /** {@inheritdoc} */ 135 | public function setReceivedMessage(MessageReceived $messageReceived): void 136 | { 137 | $this->messageReceived = $messageReceived; 138 | } 139 | 140 | /** {@inheritdoc} */ 141 | public function markAsTransitioned(): void 142 | { 143 | $this->transitioned = true; 144 | } 145 | 146 | /** {@inheritdoc} */ 147 | public function transitioned(): bool 148 | { 149 | return $this->transitioned; 150 | } 151 | 152 | /** {@inheritdoc} */ 153 | public function converse(Conversable $conversable): void 154 | { 155 | context()->incrementAttempts(); 156 | 157 | if ($conversable instanceof Intent) { 158 | context()->setIntent($conversable)->setInteraction(null); 159 | } 160 | 161 | $conversable->handle($this->messageReceived); 162 | } 163 | 164 | /** {@inheritdoc} */ 165 | public function restartInteraction(Interaction $interaction): void 166 | { 167 | context()->setInteraction(null); 168 | 169 | $this->converse($interaction); 170 | 171 | $this->markAsTransitioned(); 172 | } 173 | 174 | public function __destruct() 175 | { 176 | $context = $this->getContext(); 177 | 178 | if ($context === null) { 179 | return; 180 | } 181 | 182 | // Close session if conversation has not been transitioned 183 | if (!$this->transitioned()) { 184 | $this->flushContext($context); 185 | } 186 | 187 | // Save context if exists 188 | if ($this->transitioned() && $context = context()) { 189 | $this->saveContext($context); 190 | } 191 | } 192 | 193 | private function getCacheKeyForContext(Channel $channel, Chat $chat, User $user): string 194 | { 195 | return implode('.', ['context', $channel->getName(), $chat->getId(), $user->getId()]); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Conversation/FallbackIntent.php: -------------------------------------------------------------------------------- 1 | random(); 29 | 30 | $this->reply($text); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Conversation/Intent.php: -------------------------------------------------------------------------------- 1 | run($message); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Conversation/Interaction.php: -------------------------------------------------------------------------------- 1 | converse(resolve(static::class)); 42 | $conversation->markAsTransitioned(); 43 | } 44 | 45 | /** 46 | * Restart current interaction. 47 | */ 48 | protected function restart(): void 49 | { 50 | /** @var Manager $conversation */ 51 | $conversation = resolve(Manager::class); 52 | $conversation->restartInteraction($this); 53 | } 54 | 55 | /** 56 | * Handle interaction. 57 | * 58 | * @param MessageReceived $message 59 | */ 60 | public function handle(MessageReceived $message): void 61 | { 62 | $context = context(); 63 | 64 | if ($context->getInteraction() instanceof $this) { 65 | $this->process($message); 66 | } else { 67 | $context->setInteraction($this); 68 | $this->run($message); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Drivers/PendingReply.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 28 | $this->chat = $chat; 29 | $this->user = $user; 30 | } 31 | 32 | /** 33 | * Set reply text. 34 | * 35 | * @param null|string $text 36 | * 37 | * @return static 38 | */ 39 | public function text(?string $text) 40 | { 41 | $this->text = $text; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Set template for reply. 48 | * 49 | * @param Template|null $template 50 | * 51 | * @return static 52 | */ 53 | public function template(?Template $template) 54 | { 55 | $this->template = $template; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Set attachment for send. 62 | * 63 | * @param Attachment $attachment 64 | * 65 | * @return static 66 | */ 67 | public function attachment(Attachment $attachment) 68 | { 69 | $this->attachment = $attachment; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Set the desired delay for the job. 76 | * 77 | * @param \DateTime|int|null $delay 78 | * 79 | * @return static 80 | */ 81 | public function delay($delay) 82 | { 83 | $this->delay = $delay; 84 | 85 | return $this; 86 | } 87 | 88 | public function __destruct() 89 | { 90 | if ($this->text) { 91 | SendMessage::dispatch( 92 | $this->channel, 93 | $this->chat, 94 | $this->user, 95 | $this->text, 96 | $this->template 97 | )->delay($this->delay); 98 | } 99 | 100 | if ($this->attachment) { 101 | SendAttachment::dispatch( 102 | $this->channel, 103 | $this->chat, 104 | $this->user, 105 | $this->attachment 106 | )->delay($this->delay); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Drivers/TemplateCompiler.php: -------------------------------------------------------------------------------- 1 | getName()); 32 | if (!method_exists($this, $method)) { 33 | return null; 34 | } 35 | 36 | return $this->$method($template); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Events/Event.php: -------------------------------------------------------------------------------- 1 | chat = $chat; 32 | $this->from = $from; 33 | $this->text = $text; 34 | $this->location = $location; 35 | $this->attachment = $attachment; 36 | $this->data = $data; 37 | $this->raw = $raw; 38 | } 39 | 40 | public function getChat(): Chat 41 | { 42 | return $this->chat; 43 | } 44 | 45 | public function getFrom(): User 46 | { 47 | return $this->from; 48 | } 49 | 50 | public function getText(): string 51 | { 52 | return $this->text; 53 | } 54 | 55 | public function getLocation(): Location 56 | { 57 | return $this->location; 58 | } 59 | 60 | public function getAttachment(): ?Attachment 61 | { 62 | return $this->attachment; 63 | } 64 | 65 | public function getData(): ?string 66 | { 67 | return $this->data; 68 | } 69 | 70 | public function getRaw() 71 | { 72 | return $this->raw; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Events/Unknown.php: -------------------------------------------------------------------------------- 1 | client = $client; 19 | } 20 | 21 | /** 22 | * Get all available drivers. 23 | * 24 | * @return Collection 25 | */ 26 | public function getDrivers(): Collection 27 | { 28 | $response = $this->client->get(self::URL.'/drivers', ['json' => ['version' => Kernel::VERSION]]); 29 | 30 | return collect(json_decode((string) $response->getBody(), true)); 31 | } 32 | 33 | /** 34 | * Find driver by name. 35 | * 36 | * @param string $name 37 | * 38 | * @return array|null 39 | */ 40 | public function findDriver(string $name): ?array 41 | { 42 | return $this->getDrivers()->first(function ($item) use ($name) { 43 | return $item['name'] === $name; 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Foundation/Commands/SendAttachment.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 28 | $this->chat = $chat; 29 | $this->recipient = $recipient; 30 | $this->attachment = $attachment; 31 | } 32 | 33 | public function handle(): void 34 | { 35 | $driver = $this->channel->getDriver(); 36 | $driver->sendAttachment($this->chat, $this->recipient, $this->attachment); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Foundation/Commands/SendMessage.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 39 | $this->chat = $chat; 40 | $this->recipient = $recipient; 41 | $this->text = $text; 42 | $this->template = $template; 43 | } 44 | 45 | public function handle(): void 46 | { 47 | $driver = $this->channel->getDriver(); 48 | $driver->sendMessage($this->chat, $this->recipient, $this->text, $this->template); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Foundation/Composer.php: -------------------------------------------------------------------------------- 1 | getProcess() 22 | ->setCommandLine($this->findComposer().' require '.$package) 23 | ->run($callback); 24 | } 25 | 26 | /** 27 | * Determine if package already installed. 28 | * 29 | * @param string $package 30 | * 31 | * @return bool 32 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 33 | */ 34 | public function installed(string $package): bool 35 | { 36 | $manifest = $this->files->get('composer.json'); 37 | $manifest = json_decode($manifest, true); 38 | 39 | return collect($manifest['require']) 40 | ->merge($manifest['require-dev']) 41 | ->keys() 42 | ->contains($package); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Foundation/Controller.php: -------------------------------------------------------------------------------- 1 | getChannel()->getDriver(); 21 | 22 | // If driver supports webhook verification 23 | // We need to check if current request belongs to verification process 24 | if ($driver instanceof WebhookVerification && $driver->isVerificationRequest($request)) { 25 | return $driver->verifyWebhook($request); 26 | } 27 | 28 | // Resolve event from driver and dispatch it 29 | $events->dispatch( 30 | $event = $driver->createEvent($request) 31 | ); 32 | 33 | return $driver->createResponse($request, $event); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Foundation/Kernel.php: -------------------------------------------------------------------------------- 1 | channel = $channel; 24 | } 25 | 26 | /** 27 | * Get current channel. 28 | * 29 | * @return Channel|null 30 | */ 31 | public function getChannel(): ?Channel 32 | { 33 | return $this->channel; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Foundation/Listeners/HandleConversation.php: -------------------------------------------------------------------------------- 1 | kernel = $kernel; 23 | $this->conversation = $conversation; 24 | } 25 | 26 | public function handle(MessageReceived $messageReceived): void 27 | { 28 | /** @var Context $context */ 29 | $context = $this->conversation->resolveContext( 30 | $this->kernel->getChannel(), 31 | $messageReceived->getChat(), 32 | $messageReceived->getFrom() 33 | ); 34 | 35 | // If there is no interaction in session 36 | // Try to match intent and run it 37 | // Otherwise, run interaction 38 | if (!$this->isInConversation($context)) { 39 | $conversable = $this->conversation->matchIntent($messageReceived); 40 | } else { 41 | $conversable = $context->getInteraction(); 42 | } 43 | 44 | $this->conversation->setReceivedMessage($messageReceived); 45 | $this->conversation->converse($conversable); 46 | } 47 | 48 | /** 49 | * Determine if conversation started. 50 | * 51 | * @param Context $context 52 | * 53 | * @return bool 54 | */ 55 | private function isInConversation(Context $context): bool 56 | { 57 | return $context->getInteraction() !== null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Foundation/Middleware/InitializeKernel.php: -------------------------------------------------------------------------------- 1 | kernel = $kernel; 21 | $this->channelManager = $channelManager; 22 | } 23 | 24 | public function handle(Request $request, Closure $next) 25 | { 26 | $channel = $this->resolveChannel($request->route('channel')); 27 | 28 | if ($channel->getSecret() !== null && $request->route('secret') !== $channel->getSecret()) { 29 | abort(403); 30 | } 31 | 32 | $this->kernel->initialize($channel); 33 | 34 | return $next($request); 35 | } 36 | 37 | private function resolveChannel($value): Channel 38 | { 39 | if (is_string($value)) { 40 | $value = $this->channelManager->create($value); 41 | } 42 | 43 | return $value; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Foundation/Providers/ChannelServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerManager(); 23 | } 24 | 25 | /** 26 | * Boot application services. 27 | */ 28 | public function boot(): void 29 | { 30 | /** @var ChannelManager $manager */ 31 | $manager = $this->app[Manager::class]; 32 | 33 | $manager->register( 34 | collect($this->channels()) 35 | ->mapWithKeys(function (array $parameters, string $name) { 36 | return [$name => $parameters]; 37 | }) 38 | ->toArray() 39 | ); 40 | } 41 | 42 | /** 43 | * Define bot channels. 44 | * 45 | * @return array 46 | */ 47 | protected function channels(): array 48 | { 49 | return []; 50 | } 51 | 52 | private function registerManager(): void 53 | { 54 | $this->app->singleton(Manager::class, function () { 55 | return new ChannelManager($this->app); 56 | }); 57 | 58 | $this->app->alias(Manager::class, ChannelManager::class); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Foundation/Providers/ConversationServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerManager(); 33 | } 34 | 35 | /** 36 | * Boot application services. 37 | */ 38 | public function boot(): void 39 | { 40 | /** @var Manager $manager */ 41 | $manager = $this->app['conversation']; 42 | 43 | foreach ($this->intents as $intent) { 44 | $manager->registerIntent($intent); 45 | } 46 | 47 | $manager->registerFallbackIntent($this->fallbackIntent); 48 | 49 | if (method_exists($this, 'configure')) { 50 | $this->configure(); 51 | } 52 | } 53 | 54 | /** 55 | * Load intents from path. 56 | * 57 | * @param string $path 58 | */ 59 | protected function load(string $path): void 60 | { 61 | $namespace = $this->app->getNamespace(); 62 | 63 | /** @var SplFileInfo[] $files */ 64 | $files = (new Finder)->in($path)->files(); 65 | 66 | foreach ($files as $file) { 67 | $file = $namespace.str_replace( 68 | ['/', '.php'], 69 | ['\\', ''], 70 | Str::after($file->getPathname(), app_path().DIRECTORY_SEPARATOR) 71 | ); 72 | 73 | if (is_subclass_of($file, Intent::class) && !(new ReflectionClass($file))->isAbstract()) { 74 | $this->app['conversation']->registerIntent($file); 75 | } 76 | } 77 | } 78 | 79 | private function registerManager(): void 80 | { 81 | $this->app->singleton('conversation', function () { 82 | return new ConversationManager($this->app, $this->app[Cache::class]); 83 | }); 84 | 85 | $this->app->alias('conversation', Manager::class); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Foundation/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['events']; 25 | 26 | $events->listen(MessageReceived::class, HandleConversation::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Foundation/Providers/FoundationServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(Kernel::class, function () { 15 | return new Kernel; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Foundation/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'fondbot.webhook'], function () { 19 | Route::get('webhook/{channel}/{secret?}', 'FondBot\Foundation\Controller@webhook')->name('fondbot.webhook'); 20 | Route::post('webhook/{channel}/{secret?}', 'FondBot\Foundation\Controller@webhook')->name('fondbot.webhook'); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Foundation/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | setName('FondBot Framework'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Framework/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | artisan === null) { 15 | return $this->artisan = (new Application($this->app, $this->events, $this->app->version())) 16 | ->resolveCommands($this->commands); 17 | } 18 | 19 | return $this->artisan; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Framework/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | routeIs('fondbot.webhook')) { 23 | return 'Something went wrong.'; 24 | } 25 | 26 | return parent::render($request, $e); 27 | } 28 | 29 | /** {@inheritdoc} */ 30 | protected function renderHttpException(HttpException $e): Response 31 | { 32 | return $this->convertExceptionToResponse($e); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Framework/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 14 | InitializeKernel::class, 15 | ], 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Templates/Attachment.php: -------------------------------------------------------------------------------- 1 | type = $type; 23 | $this->path = $path; 24 | $this->parameters = collect($parameters); 25 | } 26 | 27 | public static function make(string $type, string $path, array $parameters = []) 28 | { 29 | return new static($type, $path, $parameters); 30 | } 31 | 32 | public static function file(string $path, array $parameters = []) 33 | { 34 | return new static(self::TYPE_FILE, $path, $parameters); 35 | } 36 | 37 | public static function image(string $path, array $parameters = []) 38 | { 39 | return new static(self::TYPE_IMAGE, $path, $parameters); 40 | } 41 | 42 | public static function audio(string $path, array $parameters = []) 43 | { 44 | return new static(self::TYPE_AUDIO, $path, $parameters); 45 | } 46 | 47 | public static function video(string $path, array $parameters = []) 48 | { 49 | return new static(self::TYPE_VIDEO, $path, $parameters); 50 | } 51 | 52 | public function getType(): string 53 | { 54 | return $this->type; 55 | } 56 | 57 | public function setType(string $type): Attachment 58 | { 59 | $this->type = $type; 60 | 61 | return $this; 62 | } 63 | 64 | public function getPath(): string 65 | { 66 | return $this->path; 67 | } 68 | 69 | public function setPath(string $path): Attachment 70 | { 71 | $this->path = $path; 72 | 73 | return $this; 74 | } 75 | 76 | public function getParameters(): Collection 77 | { 78 | return $this->parameters; 79 | } 80 | 81 | public function setParameters(array $parameters): Attachment 82 | { 83 | $this->parameters = collect($parameters); 84 | 85 | return $this; 86 | } 87 | 88 | public static function possibleTypes(): array 89 | { 90 | return [ 91 | static::TYPE_FILE, 92 | static::TYPE_IMAGE, 93 | static::TYPE_AUDIO, 94 | static::TYPE_VIDEO, 95 | ]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Templates/Keyboard.php: -------------------------------------------------------------------------------- 1 | buttons = $buttons; 24 | $this->parameters = collect($parameters); 25 | } 26 | 27 | /** 28 | * @param Button[] $buttons 29 | * @param array $parameters 30 | * 31 | * @return static 32 | */ 33 | public static function make(array $buttons = [], array $parameters = []) 34 | { 35 | return new static($buttons, $parameters); 36 | } 37 | 38 | public function getName(): string 39 | { 40 | return 'Keyboard'; 41 | } 42 | 43 | /** 44 | * @return Button[] 45 | */ 46 | public function getButtons(): array 47 | { 48 | return $this->buttons; 49 | } 50 | 51 | public function addButton(Button $button): Keyboard 52 | { 53 | $this->buttons[] = $button; 54 | 55 | return $this; 56 | } 57 | 58 | public function getParameters(): Collection 59 | { 60 | return $this->parameters; 61 | } 62 | 63 | public function setParameters(array $parameters): Keyboard 64 | { 65 | $this->parameters = $parameters; 66 | 67 | return $this; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Templates/Keyboard/Button.php: -------------------------------------------------------------------------------- 1 | label = $label; 18 | $this->parameters = collect($parameters); 19 | } 20 | 21 | /** 22 | * Get name. 23 | * 24 | * @return string 25 | */ 26 | public function getName(): string 27 | { 28 | return class_basename($this); 29 | } 30 | 31 | /** 32 | * Get label. 33 | * 34 | * @return string 35 | */ 36 | public function getLabel(): ?string 37 | { 38 | return $this->label; 39 | } 40 | 41 | /** 42 | * Set label. 43 | * 44 | * @param string $label 45 | * 46 | * @return static 47 | */ 48 | public function setLabel(string $label): Button 49 | { 50 | $this->label = $label; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Get button parameters. 57 | * 58 | * @return Collection 59 | */ 60 | public function getParameters(): Collection 61 | { 62 | return $this->parameters; 63 | } 64 | 65 | /** 66 | * Set parameters. 67 | * 68 | * @param array $parameters 69 | * 70 | * @return static 71 | */ 72 | public function setParameters(array $parameters): Button 73 | { 74 | $this->parameters = collect($parameters); 75 | 76 | return $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Templates/Keyboard/PayloadButton.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 16 | } 17 | 18 | /** 19 | * Make a new payload button instance. 20 | * 21 | * @param string $label 22 | * @param mixed $payload 23 | * @param array $parameters 24 | * 25 | * @return static 26 | */ 27 | public static function make(string $label, $payload, array $parameters = []) 28 | { 29 | return new static($label, $payload, $parameters); 30 | } 31 | 32 | /** 33 | * Get payload. 34 | * 35 | * @return mixed 36 | */ 37 | public function getPayload() 38 | { 39 | return $this->payload; 40 | } 41 | 42 | /** 43 | * Set payload. 44 | * 45 | * @param mixed $payload 46 | * 47 | * @return PayloadButton 48 | */ 49 | public function setPayload($payload): PayloadButton 50 | { 51 | $this->payload = $payload; 52 | 53 | return $this; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Templates/Keyboard/ReplyButton.php: -------------------------------------------------------------------------------- 1 | url = $url; 16 | } 17 | 18 | public static function make(string $label, string $url, array $parameters = []) 19 | { 20 | return new static($label, $url, $parameters); 21 | } 22 | 23 | /** 24 | * Get URL. 25 | * 26 | * @return string 27 | */ 28 | public function getUrl(): string 29 | { 30 | return $this->url; 31 | } 32 | 33 | /** 34 | * Set URL. 35 | * 36 | * @param mixed $url 37 | * 38 | * @return UrlButton 39 | */ 40 | public function setUrl($url): UrlButton 41 | { 42 | $this->url = $url; 43 | 44 | return $this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Templates/Location.php: -------------------------------------------------------------------------------- 1 | latitude = $latitude; 18 | $this->longitude = $longitude; 19 | $this->parameters = collect($parameters); 20 | } 21 | 22 | public static function make(float $latitude, float $longitude, array $parameters = []) 23 | { 24 | return new static($latitude, $longitude, $parameters); 25 | } 26 | 27 | public function getLatitude(): float 28 | { 29 | return $this->latitude; 30 | } 31 | 32 | public function setLatitude(float $latitude): Location 33 | { 34 | $this->latitude = $latitude; 35 | 36 | return $this; 37 | } 38 | 39 | public function getLongitude(): float 40 | { 41 | return $this->longitude; 42 | } 43 | 44 | public function setLongitude(float $longitude): Location 45 | { 46 | $this->longitude = $longitude; 47 | 48 | return $this; 49 | } 50 | 51 | public function getParameters(): Collection 52 | { 53 | return $this->parameters; 54 | } 55 | 56 | public function setParameters(array $parameters): Location 57 | { 58 | $this->parameters = collect($parameters); 59 | 60 | return $this; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Toolbelt/InstallDriverCommand.php: -------------------------------------------------------------------------------- 1 | argument('name'); 23 | $driver = $api->findDriver($this->argument('name')); 24 | 25 | if ($driver === null) { 26 | $this->error('"'.$name.'" is not found in the available drivers list or is not yet supported by current FondBot version ('.Kernel::VERSION.').'); 27 | 28 | exit(0); 29 | } 30 | 31 | if ($composer->installed($driver['package'])) { 32 | $this->error('Driver is already installed.'); 33 | 34 | return; 35 | } 36 | 37 | // Install driver 38 | $this->info('Installing driver...'); 39 | 40 | $result = $composer->install($driver['package'], function ($_, $line) use (&$output) { 41 | $output .= $line; 42 | }); 43 | 44 | if ($result !== 0) { 45 | $this->error($output); 46 | exit($result); 47 | } 48 | 49 | $this->info('Driver installed. ✔'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Toolbelt/ListChannelsCommand.php: -------------------------------------------------------------------------------- 1 | all()) 18 | ->transform(function ($item, $name) use ($manager) { 19 | return [$name, $manager->driver($item['driver'])->getName(), $manager->create($name)->getWebhookUrl()]; 20 | }) 21 | ->toArray(); 22 | 23 | $this->table(['Name', 'Driver', 'Webhook URL'], $rows); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Toolbelt/ListDriversCommand.php: -------------------------------------------------------------------------------- 1 | getDrivers())->keys()->toArray(); 21 | $availableDrivers = $api->getDrivers(); 22 | 23 | $rows = collect($availableDrivers) 24 | ->transform(function ($item) use ($installedDrivers) { 25 | return [ 26 | $item['name'], 27 | $item['package'], 28 | $item['official'] ? '✅' : '❌', 29 | in_array($item['name'], $installedDrivers, true) ? '✅' : '❌', 30 | ]; 31 | }) 32 | ->toArray(); 33 | 34 | $this->table(['Name', 'Package', 'Official', 'Installed'], $rows); 35 | } catch (ClientException $exception) { 36 | $this->error('Connection to FondBot API failed. Please check your internet connection and try again.'); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Toolbelt/ListIntentsCommand.php: -------------------------------------------------------------------------------- 1 | getIntents()) 18 | ->transform(function ($item) { 19 | return [$item]; 20 | }) 21 | ->toArray(); 22 | 23 | $this->table(['Class'], $rows); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Toolbelt/MakeActivatorCommand.php: -------------------------------------------------------------------------------- 1 | laravel['path'].'/Activators/'.$name.'.php'; 45 | } 46 | 47 | /** 48 | * Get the full namespace for a given class, without the class name. 49 | * 50 | * @param string $name 51 | * @return string 52 | */ 53 | protected function getNamespace($name): string 54 | { 55 | return 'Activators'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Toolbelt/MakeIntentCommand.php: -------------------------------------------------------------------------------- 1 | laravel['path'].'/Intents/'.$name.'.php'; 31 | } 32 | 33 | /** {@inheritdoc} */ 34 | protected function getNamespace($name): string 35 | { 36 | return 'Intents'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Toolbelt/MakeInteractionCommand.php: -------------------------------------------------------------------------------- 1 | laravel['path'].'/Interactions/'.$name.'.php'; 31 | } 32 | 33 | /** {@inheritdoc} */ 34 | protected function getNamespace($name): string 35 | { 36 | return 'Interactions'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Toolbelt/ToolbeltServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 17 | $this->commands([ 18 | MakeIntentCommand::class, 19 | MakeInteractionCommand::class, 20 | MakeActivatorCommand::class, 21 | ListDriversCommand::class, 22 | InstallDriverCommand::class, 23 | ListChannelsCommand::class, 24 | ListIntentsCommand::class, 25 | ]); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | getContext(); 35 | 36 | if ($key === null) { 37 | return $context; 38 | } 39 | 40 | return optional($context)->getItem($key, $default); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Mocks/FakeChannel.php: -------------------------------------------------------------------------------- 1 | getText() === 'foo'; 22 | }, 23 | ]; 24 | } 25 | 26 | /** 27 | * Run intent. 28 | * 29 | * @param MessageReceived $message 30 | */ 31 | public function run(MessageReceived $message): void 32 | { 33 | // TODO: Implement run() method. 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Mocks/FakeInteraction.php: -------------------------------------------------------------------------------- 1 | kernel = $this->mock(Kernel::class); 28 | } 29 | 30 | protected function getPackageProviders($app): array 31 | { 32 | return [ 33 | ServiceProvider::class, 34 | ChannelServiceProvider::class, 35 | ConversationServiceProvider::class, 36 | ]; 37 | } 38 | 39 | protected function setContext(Context $context) 40 | { 41 | $this->app->instance('fondbot.conversation.context', $context); 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * @param string $class 48 | * 49 | * @param array $args 50 | * 51 | * @return mixed|Mockery\Mock 52 | */ 53 | protected function mock(string $class, array $args = null) 54 | { 55 | if ($args !== null) { 56 | $instance = Mockery::mock($class, $args); 57 | } else { 58 | $instance = Mockery::mock($class); 59 | } 60 | 61 | $this->app->instance($class, $instance); 62 | 63 | return $instance; 64 | } 65 | 66 | protected function faker(): Generator 67 | { 68 | return Factory::create(); 69 | } 70 | 71 | protected function fakeChat(): Chat 72 | { 73 | return new Chat($this->faker()->uuid, $this->faker()->word); 74 | } 75 | 76 | protected function fakeUser(): User 77 | { 78 | return new User($this->faker()->uuid, $this->faker()->name, $this->faker()->userName); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Unit/Channels/ChannelManagerTest.php: -------------------------------------------------------------------------------- 1 | 'fake', 19 | 'token' => $this->faker()->sha1, 20 | ]; 21 | $driver = new FakeDriver; 22 | 23 | $manager = new ChannelManager($this->app); 24 | $manager->extend('fake', function () use (&$driver) { 25 | return $driver; 26 | }); 27 | $manager->register([$name => $parameters]); 28 | 29 | $result = $manager->create($name); 30 | 31 | $this->assertInstanceOf(Channel::class, $result); 32 | $this->assertSame($name, $result->getName()); 33 | $this->assertSame($driver, $result->getDriver()); 34 | } 35 | 36 | public function testAll(): void 37 | { 38 | $manager = new ChannelManager($this->app); 39 | $manager->register(['foo' => ['foo' => 'bar']]); 40 | 41 | $this->assertEquals(collect(['foo' => ['foo' => 'bar']]), $manager->all()); 42 | } 43 | 44 | public function testGetByDriver(): void 45 | { 46 | $manager = new ChannelManager($this->app); 47 | $manager->register(['foo' => ['driver' => 'foo'], 'bar' => ['driver' => FakeDriver::class]]); 48 | 49 | $this->assertEquals(collect(['foo' => ['driver' => 'foo']]), $manager->getByDriver('foo')); 50 | $this->assertEquals(collect(['bar' => ['driver' => FakeDriver::class]]), $manager->getByDriver(FakeDriver::class)); 51 | } 52 | 53 | /** 54 | * @expectedException \FondBot\Channels\Exceptions\ChannelNotFound 55 | * @expectedExceptionMessage Channel `fake` not found. 56 | */ 57 | public function testCreateException(): void 58 | { 59 | $manager = new ChannelManager($this->app); 60 | 61 | $manager->create('fake'); 62 | } 63 | 64 | public function testNoDefaultDriver(): void 65 | { 66 | $manager = new ChannelManager($this->app); 67 | 68 | $this->assertNull($manager->getDefaultDriver()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Unit/Channels/ChatTest.php: -------------------------------------------------------------------------------- 1 | faker()->uuid, $title = $this->faker()->title, 'foo'); 15 | 16 | $this->assertSame($id, $chat->getId()); 17 | $this->assertSame($title, $chat->getTitle()); 18 | $this->assertSame('foo', $chat->getType()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Unit/Channels/DriverTest.php: -------------------------------------------------------------------------------- 1 | guzzle = $this->mock(Client::class); 22 | } 23 | 24 | public function testInitialize(): void 25 | { 26 | $driver = new FakeDriver; 27 | $parameters = collect([ 28 | 'token' => str_random(), 29 | ]); 30 | 31 | $driver = $driver->initialize($parameters); 32 | 33 | $this->assertAttributeSame($parameters->get('token'), 'token', $driver); 34 | } 35 | 36 | public function testGetShortName() : void 37 | { 38 | $driver = new FakeDriver; 39 | $this->assertSame('FakeDriver', $driver->getShortName()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Channels/UserTest.php: -------------------------------------------------------------------------------- 1 | faker()->uuid, 16 | $name = $this->faker()->name, 17 | $username = $this->faker()->userName, 18 | $data = ['foo' => 'bar'] 19 | ); 20 | 21 | $this->assertSame($id, $user->getId()); 22 | $this->assertSame($name, $user->getName()); 23 | $this->assertSame($username, $user->getUsername()); 24 | $this->assertSame($data, $user->getData()->toArray()); 25 | } 26 | 27 | public function testAcceptsNullsForNameAndUsername(): void 28 | { 29 | $user = new User($id = $this->faker()->uuid, null, null); 30 | 31 | $this->assertSame($id, $user->getId()); 32 | $this->assertNull($user->getName()); 33 | $this->assertNull($user->getUsername()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Activators/AttachmentTest.php: -------------------------------------------------------------------------------- 1 | fakeChat(), $this->fakeUser(), '/start', null, Template::make('foo', 'bar')); 17 | 18 | $activator = Attachment::make(); 19 | 20 | $this->assertTrue($activator->matches($message)); 21 | } 22 | 23 | public function testDoesNotMatchWithoutType(): void 24 | { 25 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/start', null, null); 26 | 27 | $activator = Attachment::make(); 28 | 29 | $this->assertFalse($activator->matches($message)); 30 | } 31 | 32 | /** 33 | * @dataProvider types 34 | * 35 | * @param string $type 36 | */ 37 | public function testMatchesWithType(string $type): void 38 | { 39 | $activator = Attachment::make($type); 40 | $attachment = Template::make($type, 'bar'); 41 | 42 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/start', null, $attachment); 43 | 44 | $this->assertTrue($activator->matches($message)); 45 | } 46 | 47 | /** 48 | * @dataProvider types 49 | * 50 | * @param string $type 51 | */ 52 | public function testDoesNotMatchWithType(string $type): void 53 | { 54 | $activator = Attachment::make($type); 55 | 56 | /** @var string $otherType */ 57 | $otherType = collect(Template::possibleTypes()) 58 | ->filter(function ($item) use ($type) { 59 | return $item !== $type; 60 | }) 61 | ->random(); 62 | 63 | $attachment = Template::make($otherType, 'bar'); 64 | 65 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/start', null, $attachment); 66 | 67 | $this->assertFalse($activator->matches($message)); 68 | } 69 | 70 | public function types(): array 71 | { 72 | return collect(Template::possibleTypes()) 73 | ->map(function ($item) { 74 | return [$item]; 75 | }) 76 | ->toArray(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Activators/ContainsTest.php: -------------------------------------------------------------------------------- 1 | assertAttributeEquals(['foo', 'bar'], 'needles', $activator); 17 | } 18 | 19 | public function testMatches(): void 20 | { 21 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), 'this is foo'); 22 | 23 | $activator = Contains::make('foo'); 24 | 25 | $this->assertTrue($activator->matches($message)); 26 | } 27 | 28 | public function testDoesNotMatch(): void 29 | { 30 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), 'this is bar'); 31 | 32 | $activator = Contains::make('foo'); 33 | 34 | $this->assertFalse($activator->matches($message)); 35 | } 36 | 37 | public function testMessageDoesNotHaveText(): void 38 | { 39 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), ''); 40 | 41 | $activator = Contains::make('foo'); 42 | 43 | $this->assertFalse($activator->matches($message)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Activators/ExactTest.php: -------------------------------------------------------------------------------- 1 | fakeChat(), $this->fakeUser(), '/start'); 16 | 17 | $activator = Exact::make('/start'); 18 | 19 | $this->assertTrue($activator->matches($message)); 20 | } 21 | 22 | public function testDoesNotMatchCaseSensitive(): void 23 | { 24 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/Start'); 25 | 26 | $activator = Exact::make('/start')->caseSensitive(); 27 | 28 | $this->assertFalse($activator->matches($message)); 29 | } 30 | 31 | public function testMatchesCaseInsensitive(): void 32 | { 33 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/Start'); 34 | 35 | $activator = Exact::make('/start'); 36 | 37 | $this->assertTrue($activator->matches($message)); 38 | } 39 | 40 | public function testDoesNotMatchCaseInsensitive(): void 41 | { 42 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/Start'); 43 | 44 | $activator = Exact::make('/stop'); 45 | 46 | $this->assertFalse($activator->matches($message)); 47 | } 48 | 49 | public function testEmptyMessage(): void 50 | { 51 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), ''); 52 | 53 | $activator = Exact::make('/start'); 54 | 55 | $this->assertFalse($activator->matches($message)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Activators/InArrayTest.php: -------------------------------------------------------------------------------- 1 | assertAttributeEquals(['foo', 'bar'], 'values', $activator); 18 | } 19 | 20 | public function testArrayMatches(): void 21 | { 22 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/start'); 23 | 24 | $activator = In::make(['/bye', '/start', '/test']); 25 | $this->assertTrue( 26 | $activator->matches($message) 27 | ); 28 | } 29 | 30 | public function testArrayDoesNotMatch(): void 31 | { 32 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/stop'); 33 | 34 | $activator = In::make(['/bye', '/start', '/test']); 35 | 36 | $this->assertFalse($activator->matches($message)); 37 | } 38 | 39 | public function testCollectionMatches(): void 40 | { 41 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/start'); 42 | 43 | $activator = In::make(collect(['/bye', '/start', '/test'])); 44 | 45 | $this->assertTrue($activator->matches($message)); 46 | } 47 | 48 | public function testCollectionDoesNotMatch(): void 49 | { 50 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/stop'); 51 | 52 | $activator = In::make(collect(['/bye', '/start', '/test'])); 53 | 54 | $this->assertFalse($activator->matches($message)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Activators/PayloadTest.php: -------------------------------------------------------------------------------- 1 | fakeChat(), $this->fakeUser(), '/start', null, null, 'foo'); 16 | 17 | $activator = Payload::make('foo'); 18 | 19 | $this->assertTrue($activator->matches($message)); 20 | } 21 | 22 | public function testDoesNotMatch(): void 23 | { 24 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), '/start', null, null, 'foo'); 25 | 26 | $activator = Payload::make('bar'); 27 | 28 | $this->assertFalse($activator->matches($message)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Activators/RegexTest.php: -------------------------------------------------------------------------------- 1 | fakeChat(), $this->fakeUser(), 'abc'); 16 | 17 | $activator = Regex::make('abc'); 18 | 19 | $this->assertTrue($activator->matches($message)); 20 | } 21 | 22 | public function testStringDoesNotMatch(): void 23 | { 24 | $message = new MessageReceived($this->fakeChat(), $this->fakeUser(), 'ab'); 25 | 26 | $activator = Regex::make('abc'); 27 | 28 | $this->assertFalse($activator->matches($message)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Concerns/AuthorizationTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($class->passesAuthorization($this->mock(MessageReceived::class))); 18 | } 19 | 20 | public function testWithoutMethod() 21 | { 22 | $class = new AuthorizationTraitTestClassWithoutMethod(); 23 | 24 | $this->assertTrue($class->passesAuthorization($this->mock(MessageReceived::class))); 25 | } 26 | } 27 | 28 | class AuthorizationTraitTestClassWithMethod 29 | { 30 | use Authorization; 31 | 32 | /** 33 | * Determine if passes the authorization check. 34 | * 35 | * @param MessageReceived $message 36 | * 37 | * @return bool 38 | */ 39 | public function authorize(MessageReceived $message): bool 40 | { 41 | return false; 42 | } 43 | } 44 | 45 | class AuthorizationTraitTestClassWithoutMethod 46 | { 47 | use Authorization; 48 | } 49 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Concerns/InteractsWithContextTest.php: -------------------------------------------------------------------------------- 1 | mock(Channel::class); 21 | 22 | $this->setContext( 23 | new Context( 24 | $channel, 25 | $this->mock(Chat::class), 26 | $this->mock(User::class), 27 | ['foo' => 'bar'] 28 | ) 29 | ); 30 | 31 | $this->assertSame($channel, $this->getChannel()); 32 | } 33 | 34 | public function testGetChat() 35 | { 36 | $chat = $this->mock(Chat::class); 37 | 38 | $this->setContext( 39 | new Context( 40 | $this->mock(Channel::class), 41 | $chat, 42 | $this->mock(User::class), 43 | ['foo' => 'bar'] 44 | ) 45 | ); 46 | 47 | $this->assertSame($chat, $this->getChat()); 48 | } 49 | 50 | public function testGetUser() 51 | { 52 | $user = $this->mock(User::class); 53 | 54 | $this->setContext( 55 | new Context( 56 | $this->mock(Channel::class), 57 | $this->mock(Chat::class), 58 | $user, 59 | ['foo' => 'bar'] 60 | ) 61 | ); 62 | 63 | $this->assertSame($user, $this->getUser()); 64 | } 65 | 66 | public function testContext(): void 67 | { 68 | $this->setContext( 69 | new Context( 70 | $this->mock(Channel::class), 71 | $this->mock(Chat::class), 72 | $this->mock(User::class), 73 | ['foo' => 'bar'] 74 | ) 75 | ); 76 | 77 | $this->assertSame('bar', $this->context('foo')); 78 | $this->assertNull($this->context('bar')); 79 | $this->assertSame('foo', $this->context('bar', 'foo')); 80 | $this->assertInstanceOf(Context::class, $this->context()); 81 | } 82 | 83 | public function testRemember(): void 84 | { 85 | $this->setContext( 86 | new Context( 87 | $this->mock(Channel::class), 88 | $this->mock(Chat::class), 89 | $this->mock(User::class), 90 | ['foo' => 'bar'] 91 | ) 92 | ); 93 | 94 | $this->remember('some', 'value'); 95 | 96 | $this->assertSame('value', $this->context('some')); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/Concerns/SendsMessagesTest.php: -------------------------------------------------------------------------------- 1 | mock(Context::class); 25 | $context->shouldReceive('getChannel')->atLeast()->once(); 26 | $context->shouldReceive('getChat')->atLeast()->once(); 27 | $context->shouldReceive('getUser')->atLeast()->once(); 28 | 29 | $this->setContext($context); 30 | } 31 | 32 | public function testReply(): void 33 | { 34 | Bus::fake(); 35 | 36 | $this->reply($this->faker()->text)->template($this->mock(Template::class)); 37 | 38 | Bus::assertDispatched(SendMessage::class); 39 | } 40 | 41 | public function testReplyWithDelay(): void 42 | { 43 | Bus::fake(); 44 | 45 | $this->reply($this->faker()->text) 46 | ->template($this->mock(Template::class)) 47 | ->delay(5); 48 | 49 | Bus::assertDispatched(SendMessage::class, function (SendMessage $job) { 50 | return $job->delay === 5; 51 | }); 52 | } 53 | 54 | public function testSendAttachment(): void 55 | { 56 | Bus::fake(); 57 | 58 | $this->sendAttachment($this->mock(Attachment::class)); 59 | 60 | Bus::assertDispatched(SendAttachment::class); 61 | } 62 | 63 | public function testSendAttachmentWithDelay(): void 64 | { 65 | Bus::fake(); 66 | 67 | $this->sendAttachment($this->mock(Attachment::class))->delay(7); 68 | 69 | Bus::assertDispatched(SendAttachment::class, function (SendAttachment $job) { 70 | return $job->delay === 7; 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/ContextTest.php: -------------------------------------------------------------------------------- 1 | mock(Channel::class); 18 | $chat = $this->mock(Chat::class); 19 | $user = $this->mock(User::class); 20 | $items = ['foo' => 'bar']; 21 | 22 | $context = new Context($channel, $chat, $user, $items); 23 | 24 | $this->assertSame($channel, $context->getChannel()); 25 | $this->assertSame($chat, $context->getChat()); 26 | $this->assertSame($user, $context->getUser()); 27 | $this->assertNull($context->getIntent()); 28 | $this->assertNull($context->getInteraction()); 29 | 30 | $this->assertSame('bar', $context->getItem('foo')); 31 | $this->assertNull($context->getItem('bar')); 32 | 33 | $context->setItem('bar', 'foo'); 34 | $this->assertSame('foo', $context->getItem('bar')); 35 | 36 | $payload = [ 37 | 'intent' => null, 38 | 'interaction' => null, 39 | 'items' => [ 40 | 'foo' => 'bar', 41 | 'bar' => 'foo', 42 | ], 43 | 'attempts' => 0, 44 | ]; 45 | 46 | $this->assertSame($payload, $context->toArray()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/ConversationManagerTest.php: -------------------------------------------------------------------------------- 1 | manager = resolve(ConversationManager::class); 30 | } 31 | 32 | public function testRegisterIntent(): void 33 | { 34 | $this->manager->registerIntent('foo'); 35 | $this->manager->registerIntent('bar'); 36 | 37 | $this->assertAttributeEquals(['foo', 'bar'], 'intents', $this->manager); 38 | 39 | $this->assertSame(['foo', 'bar'], $this->manager->getIntents()); 40 | } 41 | 42 | public function testRegisterFallbackIntent(): void 43 | { 44 | $this->manager->registerFallbackIntent('foo'); 45 | 46 | $this->assertAttributeEquals('foo', 'fallbackIntent', $this->manager); 47 | } 48 | 49 | public function testMatchIntent(): void 50 | { 51 | $this->manager->registerIntent(FakeIntent::class); 52 | $this->manager->registerFallbackIntent(FallbackIntent::class); 53 | 54 | $messageReceived = new MessageReceived(new Chat('1'), new User('2'), 'foo'); 55 | 56 | $result = $this->manager->matchIntent($messageReceived); 57 | 58 | $this->assertInstanceOf(FakeIntent::class, $result); 59 | 60 | $messageReceived = new MessageReceived(new Chat('1'), new User('2'), 'bar'); 61 | 62 | $result = $this->manager->matchIntent($messageReceived); 63 | 64 | $this->assertInstanceOf(FallbackIntent::class, $result); 65 | } 66 | 67 | public function testMatchIntentWithClosureActivator(): void 68 | { 69 | $this->manager->registerIntent(FakeIntentWithClosureActivator::class); 70 | $this->manager->registerFallbackIntent(FallbackIntent::class); 71 | 72 | $messageReceived = new MessageReceived(new Chat('1'), new User('2'), 'foo'); 73 | 74 | $result = $this->manager->matchIntent($messageReceived); 75 | 76 | $this->assertInstanceOf(FakeIntentWithClosureActivator::class, $result); 77 | 78 | $messageReceived = new MessageReceived(new Chat('1'), new User('2'), 'bar'); 79 | 80 | $result = $this->manager->matchIntent($messageReceived); 81 | 82 | $this->assertInstanceOf(FallbackIntent::class, $result); 83 | } 84 | 85 | public function testResolveContext(): void 86 | { 87 | $key = 'context.foo-channel.foo-chat.foo-user'; 88 | 89 | cache([$key => [ 90 | 'intent' => FakeIntent::class, 91 | 'interaction' => FakeInteraction::class, 92 | 'items' => ['foo' => 'bar'], 93 | ]], 100); 94 | 95 | $result = $this->manager->resolveContext( 96 | new Channel('foo-channel', new FakeDriver), 97 | new Chat('foo-chat'), 98 | new User('foo-user') 99 | ); 100 | 101 | $this->assertSame('foo-channel', $result->getChannel()->getName()); 102 | $this->assertSame('foo-chat', $result->getChat()->getId()); 103 | $this->assertSame('foo-user', $result->getUser()->getId()); 104 | $this->assertInstanceOf(FakeIntent::class, $result->getIntent()); 105 | $this->assertInstanceOf(FakeInteraction::class, $result->getInteraction()); 106 | $this->assertSame('bar', $result->getItem('foo')); 107 | $this->assertTrue($this->app->has('fondbot.conversation.context')); 108 | $this->assertSame($result, resolve('fondbot.conversation.context')); 109 | } 110 | 111 | public function testSaveContext(): void 112 | { 113 | $key = 'context.foo-channel.foo-chat.foo-user'; 114 | 115 | $context = new Context( 116 | new Channel('foo-channel', new FakeDriver), 117 | new Chat('foo-chat'), 118 | new User('foo-user'), 119 | ['foo' => 'bar'] 120 | ); 121 | $context->setIntent(new FakeIntent)->setInteraction(new FakeInteraction); 122 | 123 | $this->manager->saveContext($context); 124 | 125 | $this->assertTrue(cache()->has($key)); 126 | $this->assertSame(FakeIntent::class, array_get(cache($key), 'intent')); 127 | $this->assertSame(FakeInteraction::class, array_get(cache($key), 'interaction')); 128 | $this->assertSame(['foo' => 'bar'], array_get(cache($key), 'items')); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/FallbackIntentTest.php: -------------------------------------------------------------------------------- 1 | assertSame([], (new FallbackIntent)->activators()); 22 | } 23 | 24 | public function testRun(): void 25 | { 26 | Bus::fake(); 27 | 28 | $context = $this->mock(Context::class); 29 | $message = $this->mock(MessageReceived::class); 30 | $channel = $this->mock(Channel::class); 31 | $chat = $this->mock(Chat::class); 32 | $user = $this->mock(User::class); 33 | 34 | $this->setContext($context); 35 | 36 | $context->shouldReceive('getChannel')->andReturn($channel)->atLeast()->once(); 37 | $context->shouldReceive('getChat')->andReturn($chat)->atLeast()->once(); 38 | $context->shouldReceive('getUser')->andReturn($user)->atLeast()->once(); 39 | 40 | (new FallbackIntent)->handle($message); 41 | 42 | Bus::assertDispatched(SendMessage::class); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/IntentTest.php: -------------------------------------------------------------------------------- 1 | mock(Intent::class)->shouldIgnoreMissing(); 17 | 18 | $intent->handle($this->mock(MessageReceived::class)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Unit/Conversation/InteractionTest.php: -------------------------------------------------------------------------------- 1 | mock(Context::class); 18 | /** @var Interaction|MockInterface $interaction */ 19 | $interaction = $this->mock(Interaction::class)->makePartial(); 20 | 21 | $this->setContext($context); 22 | 23 | $message = $this->mock(MessageReceived::class); 24 | 25 | $context->shouldReceive('getInteraction')->andReturn($interaction)->once(); 26 | 27 | $interaction->shouldReceive('process')->with($message)->once(); 28 | 29 | $interaction->handle($message); 30 | } 31 | 32 | public function testRunCurrentInteractionNotInSession(): void 33 | { 34 | $context = $this->mock(Context::class); 35 | /** @var Interaction|MockInterface $interaction */ 36 | $interaction = $this->mock(Interaction::class)->makePartial(); 37 | 38 | $this->setContext($context); 39 | 40 | $message = $this->mock(MessageReceived::class); 41 | 42 | $context->shouldReceive('getInteraction')->andReturn(null)->once(); 43 | $context->shouldReceive('setInteraction')->with($interaction)->once(); 44 | 45 | $interaction->shouldReceive('run')->with($message)->once(); 46 | 47 | $interaction->handle($message); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Unit/Drivers/TemplateCompilerTest.php: -------------------------------------------------------------------------------- 1 | mock(TemplateCompiler::class)->shouldAllowMockingProtectedMethods()->makePartial(); 21 | 22 | $compiler->shouldReceive('compileKeyboard')->with($template)->andReturn(['buttons' => ['foo', 'bar']])->once(); 23 | 24 | $result = $compiler->compile($template); 25 | $this->assertSame(['buttons' => ['foo', 'bar']], $result); 26 | } 27 | 28 | public function testCompileUsingMethodButMethodDoesNotExist(): void 29 | { 30 | $template = $this->mock(Template::class); 31 | 32 | /** @var TemplateCompiler|Mock $compiler */ 33 | $compiler = $this->mock(TemplateCompiler::class)->shouldAllowMockingProtectedMethods()->makePartial(); 34 | 35 | $template->shouldReceive('getName')->andReturn('foo')->atLeast()->once(); 36 | 37 | $this->assertNull($compiler->compile($template)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Unit/Foundation/Commands/SendAttachmentTest.php: -------------------------------------------------------------------------------- 1 | mock(Channel::class); 19 | $chat = $this->mock(Chat::class); 20 | $recipient = $this->mock(User::class); 21 | $attachment = $this->mock(Attachment::class); 22 | 23 | new SendAttachment($channel, $chat, $recipient, $attachment); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Unit/Foundation/Commands/SendMessageTest.php: -------------------------------------------------------------------------------- 1 | mock(Channel::class); 20 | $chat = $this->mock(Chat::class); 21 | $recipient = $this->mock(User::class); 22 | $text = $this->faker()->text; 23 | $template = $this->mock(Template::class); 24 | 25 | new SendMessage($channel, $chat, $recipient, $text, $template); 26 | } 27 | 28 | /** 29 | * @expectedException InvalidArgumentException 30 | * @expectedExceptionMessage Either text or template should be set. 31 | */ 32 | public function testTextAndTemplateNull(): void 33 | { 34 | new SendMessage( 35 | $this->mock(Channel::class), 36 | $this->mock(Chat::class), 37 | $this->mock(User::class) 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Unit/Foundation/KernelTest.php: -------------------------------------------------------------------------------- 1 | assertSame($this->kernel, kernel()); 19 | } 20 | 21 | public function testContext(): void 22 | { 23 | $this->assertNull(context()); 24 | 25 | $context = new Context( 26 | new Channel('foo', new FakeDriver()), 27 | new Chat('1'), 28 | new User('2') 29 | ); 30 | 31 | $context->setItem('bar', 'baz'); 32 | 33 | $this->app->bind('fondbot.conversation.context', function () use (&$context) { 34 | return $context; 35 | }); 36 | 37 | $this->assertSame($context, context()); 38 | $this->assertNull(context('foo')); 39 | $this->assertSame('baz', context('bar')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Templates/AttachmentTest.php: -------------------------------------------------------------------------------- 1 | faker()->url, $parameters = ['foo' => 'bar']); 20 | 21 | $this->assertSame($type, $attachment->getType()); 22 | $this->assertSame($path, $attachment->getPath()); 23 | $this->assertSame($parameters, $attachment->getParameters()->toArray()); 24 | } 25 | 26 | public function testPossibleTypes() 27 | { 28 | $this->assertSame(collect($this->types())->flatten()->toArray(), Attachment::possibleTypes()); 29 | } 30 | 31 | public function types() 32 | { 33 | return [ 34 | ['file'], 35 | ['image'], 36 | ['audio'], 37 | ['video'], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Unit/Templates/Keyboard/PayloadButtonTest.php: -------------------------------------------------------------------------------- 1 | faker()->word; 15 | $payload = $this->faker()->text; 16 | 17 | $button = PayloadButton::make($label, $payload); 18 | 19 | $this->assertSame('PayloadButton', $button->getName()); 20 | $this->assertSame($label, $button->getLabel()); 21 | $this->assertSame($payload, $button->getPayload()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Unit/Templates/Keyboard/ReplyButtonTest.php: -------------------------------------------------------------------------------- 1 | faker()->word; 15 | 16 | $button = ReplyButton::make($label); 17 | 18 | $this->assertSame('ReplyButton', $button->getName()); 19 | $this->assertSame($label, $button->getLabel()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Templates/Keyboard/UrlButtonTest.php: -------------------------------------------------------------------------------- 1 | faker()->word; 15 | $url = $this->faker()->url; 16 | $parameters = ['foo' => 'bar']; 17 | 18 | $button = UrlButton::make($label, $url, $parameters); 19 | 20 | $this->assertSame('UrlButton', $button->getName()); 21 | $this->assertSame($label, $button->getLabel()); 22 | $this->assertSame($url, $button->getUrl()); 23 | $this->assertEquals(collect($parameters), $button->getParameters()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Unit/Templates/KeyboardTest.php: -------------------------------------------------------------------------------- 1 | mock(Button::class), 17 | $this->mock(Button::class), 18 | ]; 19 | 20 | $keyboard = Keyboard::make($buttons, ['foo' => 'bar']); 21 | 22 | $this->assertInstanceOf(Keyboard::class, $keyboard); 23 | $this->assertSame('Keyboard', $keyboard->getName()); 24 | $this->assertSame($buttons, $keyboard->getButtons()); 25 | $this->assertEquals(collect(['foo' => 'bar']), $keyboard->getParameters()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Unit/Templates/LocationTest.php: -------------------------------------------------------------------------------- 1 | faker()->latitude, $longitude = $this->faker()->longitude); 15 | 16 | $this->assertSame($latitude, $location->getLatitude()); 17 | $this->assertSame($longitude, $location->getLongitude()); 18 | } 19 | } 20 | --------------------------------------------------------------------------------