├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json └── src └── PhpSlackBot ├── ActiveMessenger └── Push.php ├── Base.php ├── Bot.php ├── Command ├── BaseCommand.php ├── CountCommand.php ├── DateCommand.php ├── PingPongCommand.php └── PokerPlanningCommand.php └── Webhook ├── BaseWebhook.php └── OutputWebhook.php /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .idea/ 3 | bot.php 4 | composer.phar 5 | composer.lock 6 | vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.1' 4 | before_script: 5 | # PHP_CodeSniffer 6 | - pear install pear/PHP_CodeSniffer 7 | - phpenv rehash 8 | script: 9 | - phpcs --standard=PSR2 src/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jean-Charles Le Goff 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Slack Bot 2 | 3 | A simple bot user written in PHP using the Slack Real Time Messaging API https://api.slack.com/rtm 4 | 5 | ## Installation 6 | With Composer 7 | 8 | composer require jclg/php-slack-bot 9 | 10 | ## Usage 11 | 12 | Create a php file called `bot.php` with the following content 13 | 14 | ```php 15 | require 'vendor/autoload.php'; 16 | use PhpSlackBot\Bot; 17 | 18 | // Custom command 19 | class MyCommand extends \PhpSlackBot\Command\BaseCommand { 20 | 21 | protected function configure() { 22 | $this->setName('mycommand'); 23 | } 24 | 25 | protected function execute($message, $context) { 26 | $this->send($this->getCurrentChannel(), null, 'Hello !'); 27 | } 28 | 29 | } 30 | 31 | $bot = new Bot(); 32 | $bot->setToken('TOKEN'); // Get your token here https://my.slack.com/services/new/bot 33 | $bot->loadCommand(new MyCommand()); 34 | $bot->loadInternalCommands(); // This loads example commands 35 | $bot->run(); 36 | ``` 37 | 38 | Then run `php bot.php` from the command line (terminal). 39 | 40 | ## Example commands 41 | 42 | Example commands are located in `src/PhpSlackBot/Command/` and can be loaded with `$bot->loadInternalCommands();` 43 | 44 | ##### Ping Pong Command 45 | 46 | Type `ping` in a channel and the bot should answer "Pong" to you. 47 | 48 | ##### Count Command 49 | 50 | Type `count` several times in a channel and the bot should answer with 1 then 2... 51 | 52 | ##### Date Command 53 | 54 | Type `date` in a channel and the current date. 55 | 56 | ##### Planning Poker Command 57 | 58 | https://en.wikipedia.org/wiki/Planning_poker 59 | 60 | Type `pokerp start` in a public channel with your team in order to start a planning poker session. 61 | 62 | Direct message the bot with `pokerp vote number`. The bot will record your vote. 63 | 64 | Type `pokerp status` to see the current status of the session (who has voted). 65 | 66 | Type `pokerp end` in a public channel and the bot will output each vote. 67 | 68 | ## Load your own commands 69 | 70 | You can load your own commands by implementing the \PhpSlackBot\Command\BaseCommand. 71 | 72 | Then call PhpSlackBot\Bot::loadCommand method for each command you have to load. 73 | 74 | ## "Catch All" command 75 | 76 | If you need to execute a command when an event occurs, you can set up a "catch all" command. 77 | 78 | This special command will be triggered on all events. 79 | 80 | ```php 81 | require 'vendor/autoload.php'; 82 | use PhpSlackBot\Bot; 83 | 84 | // This special command executes on all events 85 | class SuperCommand extends \PhpSlackBot\Command\BaseCommand { 86 | 87 | protected function configure() { 88 | // We don't have to configure a command name in this case 89 | } 90 | 91 | protected function execute($data, $context) { 92 | if ($data['type'] == 'message') { 93 | $channel = $this->getChannelNameFromChannelId($data['channel']); 94 | $username = $this->getUserNameFromUserId($data['user']); 95 | echo $username.' from '.($channel ? $channel : 'DIRECT MESSAGE').' : '.$data['text'].PHP_EOL; 96 | } 97 | } 98 | 99 | } 100 | 101 | $bot = new Bot(); 102 | $bot->setToken('TOKEN'); // Get your token here https://my.slack.com/services/new/bot 103 | $bot->loadCatchAllCommand(new SuperCommand()); 104 | $bot->run(); 105 | ``` 106 | 107 | ## Incoming webhooks 108 | 109 | The bot can also listen for incoming webhooks. 110 | 111 | Commands are triggered from users messages inside Slack and webhooks are triggered from web post requests. 112 | 113 | Custom webhooks can be loaded using the PhpSlackBot\Bot::loadWebhook method. 114 | 115 | This is useful if you need to control the bot from an external service. For example, with IFTTT https://ifttt.com/maker 116 | 117 | To enable webhooks, use the enableWebserver method before the run method. 118 | 119 | You can also set a secret token to prevent unauthorized requests. 120 | 121 | 122 | ```php 123 | $bot->loadInternalWebhooks(); // Load the internal "output" webhook 124 | $bot->enableWebserver(8080, 'secret'); // This will listen on port 8080 125 | $bot->run(); 126 | ``` 127 | 128 | Then, use the parameter "name" to trigger the corresponding webhook : 129 | 130 | ``` 131 | curl -X POST --data-urlencode 'auth=secret' --data-urlencode 'name=output' --data-urlencode 'payload={"type" : "message", "text": "This is a message", "channel": "#general"}' http://localhost:8080 132 | ``` 133 | 134 | ## Active Messaging 135 | 136 | The example provided in the *usage section* above sets the bot to be reactive. The reactive bot is not capable of sending messages to any user on its own. It must be given an input to get a response from. In other words, the bot can only **react** to inputs given to it. 137 | 138 | *Active Messaging* means that as a developer, you would be able to **send messages to your users without them sending a message to the bot first**. It is useful when you have to notify your users about something e.g. a bot which can check a user's birthday and wish them, or tell them weather outside every 2 hours without them having to type in a command everytime. There can be many other uses. 139 | 140 | You can use active messaging this way: 141 | 142 | ```php 143 | $bot = new Bot(); 144 | $bot->setToken('TOKEN'); // Get your token here https://my.slack.com/services/new/bot 145 | $bot->loadInternalCommands(); // This loads example commands 146 | $bot->loadPushNotifier(function () { 147 | return [ 148 | 'channel' => '#general', 149 | 'username' => '@slacker', 150 | 'message' => "Happy Birthday!! Make sure you have fun today. :-)" 151 | ]; 152 | }); 153 | 154 | $bot->loadPushNotifier(function () { 155 | return [ 156 | 'channel' => '@slacker', 157 | 'username' => null, 158 | 'message' => "Current UNIX timestamp is: " . time() 159 | ]; 160 | }, 1800); 161 | $bot->run(); 162 | 163 | ``` 164 | 165 | In the example above, we have set two active messages. 166 | 167 | **First message** will send the following message in the '#general' channel: 168 | 169 | ``` 170 | @slacker Happy Birthday!! Make sure you have fun today. :-) 171 | ``` 172 | 173 | It will be triggered only **once**, 10 seconds after launching the script. Slack servers normally establish the connection by then. 174 | 175 | **Second message** is a periodic message. It will be sent to the user having the username *slacker* in the team. It won't mention anyone and will be repeated every 30 minutes (1800 seconds). The message should appear as: 176 | 177 | ``` 178 | Current UNIX timestamp is: 1489145707 179 | Current UNIX timestamp is: 1489147507 180 | ``` 181 | 182 | That should happen within 1 hour of launching. 183 | 184 | *NOTE*: The first message would appear 30 minutes after launching. 185 | 186 | The function you add using `loadPushNotifier` must return an array containing the following keys: 187 | 188 | - **channel**: Name of the channel or user to whom the message is to be sent. Channel names should have the `#` prefix while usernames must have the `@` prefix. If you do not set a prefix, the name is assumed to be a channel name. Using prefixes is recommended here. 189 | - **username**: Any username to be mentioned ahead of the message. You can specify the name without the `@` prefix here, though you might want to use the prefix to maintain uniformity within the code. 190 | - **message**: The actual message to be sent. 191 | 192 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jclg/php-slack-bot", 3 | "description": "Slack bot user written in PHP", 4 | "homepage": "https://github.com/jclg/php-slack-bot", 5 | "license": "MIT", 6 | "require": { 7 | "semako/phpws": "1.0.1", 8 | "react/http": "^0.4", 9 | "ext-curl": "*" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "jclg", 14 | "email": "jeancharleslegoff@gmail.com" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "PhpSlackBot\\": "src/PhpSlackBot" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/PhpSlackBot/ActiveMessenger/Push.php: -------------------------------------------------------------------------------- 1 | getUserIdFromUserName($channelOrUsername); 28 | $channelId = $this->getImIdFromUserId($userId); 29 | } elseif (strpos($channelOrUsername, '#') === 0) { 30 | // Channel requested 31 | $channelId = $this->getChannelIdFromChannelName($channelOrUsername); 32 | } else { 33 | // Neither user not channel requested 34 | // NOTE: We assume it to be a channel name 35 | $channelId = $this->getChannelIdFromChannelName($channelOrUsername); 36 | } 37 | 38 | $usernameToSend = $this->getUserIdFromUserName($usernameForMention); 39 | if (!$usernameToSend) { 40 | $usernameToSend = null; 41 | } 42 | 43 | if ($channelId) { 44 | $this->send($channelId, $usernameToSend, $message); 45 | } else { 46 | throw new \Exception('Cannot resolve channel ID to to send the message'); 47 | } 48 | } 49 | 50 | /** 51 | * This method is defined here only to satisfy the requirements for extending an abstract class 52 | */ 53 | protected function configure() 54 | { 55 | } 56 | 57 | /** 58 | * This method is defined here only to satisfy the requirements for extending an abstract class 59 | */ 60 | public function execute($message, $context) 61 | { 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Base.php: -------------------------------------------------------------------------------- 1 | configure(); 19 | return $this->name; 20 | } 21 | 22 | public function getClient() 23 | { 24 | return $this->client; 25 | } 26 | 27 | public function getMentionOnly() 28 | { 29 | return $this->mentionOnly; 30 | } 31 | 32 | public function setMentionOnly($mentionOnly) 33 | { 34 | $this->mentionOnly = $mentionOnly; 35 | } 36 | 37 | public function setName($name) 38 | { 39 | $this->name = $name; 40 | } 41 | 42 | public function setClient($client) 43 | { 44 | $this->client = $client; 45 | } 46 | 47 | public function setChannel($channel) 48 | { 49 | $this->channel = $channel; 50 | } 51 | 52 | public function setUser($user) 53 | { 54 | $this->user = $user; 55 | } 56 | 57 | public function getCurrentUser() 58 | { 59 | return $this->user; 60 | } 61 | 62 | public function setContext($context) 63 | { 64 | $this->context = $context; 65 | } 66 | 67 | public function getCurrentContext() 68 | { 69 | return $this->context; 70 | } 71 | 72 | public function getCurrentChannel() 73 | { 74 | return $this->channel; 75 | } 76 | 77 | public function setCaseInsensitive($caseInsensitive) 78 | { 79 | $this->caseInsensitive = $caseInsensitive; 80 | } 81 | 82 | public function getCaseInsensitive() 83 | { 84 | return $this->caseInsensitive; 85 | } 86 | 87 | protected function send($channel, $username, $message, $parent_thread = null) 88 | { 89 | $response = array( 90 | 'id' => time(), 91 | 'type' => 'message', 92 | 'channel' => $channel, 93 | 'text' => (!is_null($username) ? '<@'.$username.'> ' : '').$message 94 | ); 95 | if ($parent_thread) { 96 | $response['thread_ts'] = $parent_thread; 97 | } 98 | $this->client->send(json_encode($response)); 99 | } 100 | 101 | protected function getUserNameFromUserId($userId) 102 | { 103 | $username = 'unknown'; 104 | foreach ($this->context['users'] as $user) { 105 | if ($user['id'] == $userId) { 106 | $username = $user['name']; 107 | } 108 | } 109 | return $username; 110 | } 111 | 112 | protected function getUserIdFromUserName($userName) 113 | { 114 | $userId = ''; 115 | $userName = str_replace('@', '', $userName); 116 | foreach ($this->context['users'] as $user) { 117 | if ($user['name'] == $userName) { 118 | $userId = $user['id']; 119 | } 120 | } 121 | return $userId; 122 | } 123 | 124 | protected function getChannelIdFromChannelName($channelName) 125 | { 126 | $channelName = str_replace('#', '', $channelName); 127 | foreach ($this->context['channels'] as $channel) { 128 | if ($channel['name'] == $channelName) { 129 | return $channel['id']; 130 | } 131 | } 132 | foreach ($this->context['groups'] as $group) { 133 | if ($group['name'] == $channelName) { 134 | return $group['id']; 135 | } 136 | } 137 | return false; 138 | } 139 | 140 | protected function getChannelNameFromChannelId($channelId) 141 | { 142 | foreach ($this->context['channels'] as $channel) { 143 | if ($channel['id'] == $channelId) { 144 | return $channel['name']; 145 | } 146 | } 147 | foreach ($this->context['groups'] as $group) { 148 | if ($group['id'] == $channelId) { 149 | return $group['name']; 150 | } 151 | } 152 | return false; 153 | } 154 | 155 | protected function getImIdFromUserId($userId) 156 | { 157 | foreach ($this->context['ims'] as $im) { 158 | if ($im['user'] == $userId) { 159 | return $im['id']; 160 | } 161 | } 162 | return false; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Bot.php: -------------------------------------------------------------------------------- 1 | params = array('token' => $token); 23 | } 24 | 25 | public function loadCommand($command) 26 | { 27 | if ($command instanceof Command\BaseCommand) { 28 | $this->commands[$command->getName()] = $command; 29 | } else { 30 | throw new \Exception('Command must implement PhpSlackBot\Command\BaseCommand'); 31 | } 32 | } 33 | 34 | public function loadWebhook($webhook) 35 | { 36 | if ($webhook instanceof Webhook\BaseWebhook) { 37 | $this->webhooks[$webhook->getName()] = $webhook; 38 | } else { 39 | throw new \Exception('Webhook must implement PhpSlackBot\Webhook\BaseWebhook'); 40 | } 41 | } 42 | 43 | public function loadCatchAllCommand($command) 44 | { 45 | if ($command instanceof Command\BaseCommand) { 46 | $this->catchAllCommands[] = $command; 47 | } else { 48 | throw new \Exception('Command must implement PhpSlackBot\Command\BaseCommand'); 49 | } 50 | } 51 | 52 | public function enableWebserver($port, $authentificationToken = null, $host = '127.0.0.1') 53 | { 54 | $this->webserverPort = $port; 55 | $this->webserverAuthentificationToken = $authentificationToken; 56 | $this->webserverHost = $host; 57 | } 58 | 59 | public function loadPushNotifier($method, $repeatInterval = null) 60 | { 61 | if (is_callable($method)) { 62 | $this->pushNotifiers[] = ['interval' => (int)$repeatInterval, 'method' => $method]; 63 | } else { 64 | throw new \Exception('Closure passed as push notifier is not callable.'); 65 | } 66 | } 67 | 68 | public function run() 69 | { 70 | if (!isset($this->params['token'])) { 71 | throw new \Exception('A token must be set. Please see https://my.slack.com/services/new/bot'); 72 | } 73 | 74 | $this->init(); 75 | $logger = $this->logger; 76 | 77 | $loop = \React\EventLoop\Factory::create(); 78 | $client = new \Devristo\Phpws\Client\WebSocket($this->wsUrl, $loop, $logger); 79 | 80 | $client->on("request", function ($headers) use ($logger) { 81 | $logger->notice("Request object created!"); 82 | }); 83 | 84 | $client->on("handshake", function () use ($logger) { 85 | $logger->notice("Handshake received!"); 86 | }); 87 | 88 | $client->on("connect", function () use ($logger, $client) { 89 | $logger->notice("Connected!"); 90 | }); 91 | 92 | $client->on("message", function ($message) use ($client, $logger) { 93 | $data = $message->getData(); 94 | $logger->notice("Got message: ".$data); 95 | $data = json_decode($data, true); 96 | 97 | if (count($this->catchAllCommands)) { 98 | foreach ($this->catchAllCommands as $command) { 99 | $command->setClient($client); 100 | $command->setContext($this->context); 101 | $command->executeCommand($data, $this->context); 102 | } 103 | } 104 | $command = $this->getCommand($data); 105 | if ($command instanceof Command\BaseCommand) { 106 | $command->setClient($client); 107 | if (isset($data['channel'])) { 108 | $command->setChannel($data['channel']); 109 | } 110 | if (isset($data['user'])) { 111 | $command->setUser($data['user']); 112 | } 113 | $command->setContext($this->context); 114 | $command->executeCommand($data, $this->context); 115 | } 116 | }); 117 | 118 | /* Webserver */ 119 | if (null !== $this->webserverPort) { 120 | $logger->notice("Listening on port ".$this->webserverPort); 121 | $socket = new \React\Socket\Server($loop); 122 | $http = new \React\Http\Server($socket); 123 | $http->on('request', function ($request, $response) use ($client) { 124 | $request->on('data', function ($data) use ($client, $request, $response) { 125 | parse_str($data, $post); 126 | if ($this->webserverAuthentificationToken === null || 127 | ($this->webserverAuthentificationToken !== null && 128 | isset($post['auth']) && 129 | $post['auth'] === $this->webserverAuthentificationToken)) { 130 | if (isset($post['name']) && is_string($post['name']) && isset($this->webhooks[$post['name']])) { 131 | $hook = $this->webhooks[$post['name']]; 132 | $hook->setClient($client); 133 | $hook->setContext($this->context); 134 | $hook->executeWebhook(json_decode($post['payload'], true), $this->context); 135 | $response->writeHead(200, array('Content-Type' => 'text/plain')); 136 | $response->end("Ok\n"); 137 | } else { 138 | $response->writeHead(404, array('Content-Type' => 'text/plain')); 139 | $response->end("No webhook found\n"); 140 | } 141 | } else { 142 | $response->writeHead(403, array('Content-Type' => 'text/plain')); 143 | $response->end(""); 144 | } 145 | }); 146 | }); 147 | $socket->listen($this->webserverPort, $this->webserverHost); 148 | } 149 | 150 | /* Notifiers */ 151 | 152 | if (!$this->activeMessenger) { 153 | $this->activeMessenger = new ActiveMessenger\Push(); 154 | $this->activeMessenger->setContext($this->context); 155 | $this->activeMessenger->setClient($client); 156 | } 157 | foreach ($this->pushNotifiers as $notifierArray) { 158 | if ($notifierArray['interval'] != 0) { 159 | $loop->addPeriodicTimer($notifierArray['interval'], function () use ($notifierArray) { 160 | if ($this->activeMessenger instanceof ActiveMessenger\Push) { 161 | $resultArray = call_user_func($notifierArray['method']); 162 | $this->activeMessenger->sendMessage( 163 | $resultArray['channel'], 164 | $resultArray['username'], 165 | $resultArray['message'] 166 | ); 167 | } 168 | }); 169 | } else { 170 | $loop->addTimer(10, function () use ($notifierArray) { 171 | if ($this->activeMessenger instanceof ActiveMessenger\Push) { 172 | $resultArray = call_user_func($notifierArray['method']); 173 | $this->activeMessenger->sendMessage( 174 | $resultArray['channel'], 175 | $resultArray['username'], 176 | $resultArray['message'] 177 | ); 178 | } 179 | }); 180 | } 181 | } 182 | 183 | $client->open(); 184 | 185 | $loop->run(); 186 | } 187 | 188 | public function initLogger(\Zend\Log\LoggerInterface $logger = null) 189 | { 190 | if (! is_null($logger)) { 191 | $this->logger = $logger; 192 | } else { 193 | $this->logger = new \Zend\Log\Logger(); 194 | $writer = new \Zend\Log\Writer\Stream("php://output"); 195 | $this->logger->addWriter($writer); 196 | } 197 | } 198 | 199 | private function init() 200 | { 201 | $url = 'https://slack.com/api/rtm.start'; 202 | $ch = curl_init(); 203 | curl_setopt($ch, CURLOPT_URL, $url.'?'.http_build_query($this->params)); 204 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 205 | $body = curl_exec($ch); 206 | if ($body === false) { 207 | throw new \Exception('Error when requesting '.$url.' '.curl_error($ch)); 208 | } 209 | curl_close($ch); 210 | $response = json_decode($body, true); 211 | if (is_null($response)) { 212 | throw new \Exception('Error when decoding body ('.$body.').'); 213 | } 214 | $this->context = $response; 215 | if (isset($response['error'])) { 216 | throw new \Exception($response['error']); 217 | } 218 | $this->wsUrl = $response['url']; 219 | 220 | if (is_null($this->logger)) { 221 | $this->initLogger(); 222 | } 223 | } 224 | 225 | public function loadInternalCommands() 226 | { 227 | $commands = array( 228 | new \PhpSlackBot\Command\PingPongCommand, 229 | new \PhpSlackBot\Command\CountCommand, 230 | new \PhpSlackBot\Command\DateCommand, 231 | new \PhpSlackBot\Command\PokerPlanningCommand, 232 | ); 233 | foreach ($commands as $command) { 234 | if (!isset($this->commands[$command->getName()])) { 235 | $this->commands[$command->getName()] = $command; 236 | } 237 | } 238 | } 239 | 240 | public function loadInternalWebhooks() 241 | { 242 | $webhooks = array( 243 | new \PhpSlackBot\Webhook\OutputWebhook, 244 | ); 245 | foreach ($webhooks as $webhook) { 246 | if (!isset($this->webhooks[$webhook->getName()])) { 247 | $this->webhooks[$webhook->getName()] = $webhook; 248 | } 249 | } 250 | } 251 | 252 | private function getCommand($data) 253 | { 254 | if (empty($data['text'])) { 255 | return null; 256 | } 257 | 258 | // Check if bot is mentioned 259 | $botMention = false; 260 | if (strpos($data['text'], '<@'.$this->context['self']['id'].'>') !== false) { 261 | $botMention = true; 262 | } 263 | 264 | $find = '/^'.preg_quote('<@'.$this->context['self']['id'].'>', '/').'[ ]*/'; 265 | $text = preg_replace($find, '', $data['text']); 266 | 267 | if (empty($text)) { 268 | return null; 269 | } 270 | 271 | foreach ($this->commands as $commandName => $availableCommand) { 272 | $find = '/^'.preg_quote($commandName).'/'; 273 | if ($availableCommand->getCaseInsensitive()) { 274 | $find .= 'i'; 275 | } 276 | 277 | if (preg_match($find, $text) && 278 | (!$availableCommand->getMentionOnly() || $botMention)) { 279 | return $availableCommand; 280 | } 281 | } 282 | 283 | return null; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Command/BaseCommand.php: -------------------------------------------------------------------------------- 1 | execute($message, $context); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Command/CountCommand.php: -------------------------------------------------------------------------------- 1 | setName('count'); 12 | } 13 | 14 | protected function execute($message, $context) 15 | { 16 | $this->send($this->getCurrentChannel(), null, $this->count); 17 | $this->count++; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Command/DateCommand.php: -------------------------------------------------------------------------------- 1 | setName('date'); 10 | } 11 | 12 | protected function execute($message, $context) 13 | { 14 | $this->send($this->getCurrentChannel(), null, date("D M j G:i:s T Y")); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Command/PingPongCommand.php: -------------------------------------------------------------------------------- 1 | setName('ping'); 10 | } 11 | 12 | protected function execute($message, $context) 13 | { 14 | $this->send($this->getCurrentChannel(), $this->getCurrentUser(), 'Pong'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Command/PokerPlanningCommand.php: -------------------------------------------------------------------------------- 1 | setName('pokerp'); 15 | } 16 | 17 | protected function execute($message, $context) 18 | { 19 | $args = $this->getArgs($message); 20 | $command = isset($args[1]) ? $args[1] : ''; 21 | 22 | switch ($command) { 23 | case 'start': 24 | $this->start($args); 25 | break; 26 | case 'status': 27 | $this->status(); 28 | break; 29 | case 'vote': 30 | $this->vote($args); 31 | break; 32 | case 'end': 33 | $this->end(); 34 | break; 35 | default: 36 | $this->send( 37 | $this->getCurrentChannel(), 38 | $this->getCurrentUser(), 39 | 'No comprendo. Use "'.$this->getName().' start" or "'.$this->getName().' status"' 40 | ); 41 | } 42 | } 43 | 44 | private function start($args) 45 | { 46 | if ($this->status == 'free') { 47 | $this->subject = isset($args[2]) ? $args[2] : null; 48 | if (!is_null($this->subject)) { 49 | $this->subject = str_replace(array('<', '>'), '', $this->subject); 50 | } 51 | $this->status = 'running'; 52 | $this->initiator = $this->getCurrentUser(); 53 | $this->scores = array(); 54 | $this->send( 55 | $this->getCurrentChannel(), 56 | null, 57 | 'Poker planning sessions start by '.$this->getUserNameFromUserId($this->initiator)."\n". 58 | 'Please vote'.(!is_null($this->subject) ? ' for '.$this->subject : '') 59 | ); 60 | $this->send( 61 | $this->getCurrentChannel(), 62 | $this->getCurrentUser(), 63 | 'Use "'.$this->getName().' end" to end the session' 64 | ); 65 | } else { 66 | $this->send( 67 | $this->getCurrentChannel(), 68 | $this->getCurrentUser(), 69 | 'A poker session is still active' 70 | ); 71 | } 72 | } 73 | 74 | private function status() 75 | { 76 | $message = 'Current status : '.$this->status; 77 | if ($this->status == 'running') { 78 | $message .= "\n".'Initiator : '.$this->getUserNameFromUserId($this->initiator); 79 | } 80 | $this->send($this->getCurrentChannel(), null, $message); 81 | if ($this->status == 'running') { 82 | if (empty($this->scores)) { 83 | $this->send($this->getCurrentChannel(), null, 'No one has voted yet'); 84 | } else { 85 | $message = ''; 86 | foreach ($this->scores as $user => $score) { 87 | $message .= $this->getUserNameFromUserId($user).' has voted'."\n"; 88 | } 89 | $this->send($this->getCurrentChannel(), null, $message); 90 | } 91 | } 92 | } 93 | 94 | private function vote($args) 95 | { 96 | if ($this->status == 'running') { 97 | $score = isset($args[2]) ? $args[2] : -1; 98 | $sequence = $this->getSequence(); 99 | if (!in_array($score, $sequence)) { 100 | $this->send( 101 | $this->getCurrentChannel(), 102 | $this->getCurrentUser(), 103 | 'Use "'.$this->getName().' vote [number]". Choose [number] between '.implode(', ', $sequence) 104 | ); 105 | } else { 106 | $this->scores[$this->getCurrentUser()] = (int) $score; 107 | $this->send( 108 | $this->getCurrentChannel(), 109 | $this->getCurrentUser(), 110 | 'Thank you! Your vote ('.$score. 111 | ') has been recorded You can still change your vote until the end of the session' 112 | ); 113 | } 114 | } else { 115 | $this->send( 116 | $this->getCurrentChannel(), 117 | $this->getCurrentUser(), 118 | 'There is no poker session. You can start one with "'.$this->getName().' start"' 119 | ); 120 | } 121 | } 122 | 123 | private function end() 124 | { 125 | if ($this->status == 'running') { 126 | if ($this->getCurrentUser() == $this->initiator) { 127 | $message = 'Ending session'. 128 | (!is_null($this->subject) ? ' for '.$this->subject : '')."\n".'Results : '."\n"; 129 | if (empty($this->scores)) { 130 | $message .= 'No vote !'; 131 | } else { 132 | foreach ($this->scores as $user => $score) { 133 | $message .= $this->getUserNameFromUserId($user).' => '.$score."\n"; 134 | } 135 | $message .= '------------------'."\n"; 136 | $message .= 'Average score : '.$this->getAverageScore()."\n"; 137 | $message .= 'Median score : '.$this->getMedianScore()."\n"; 138 | $message .= 'Max score : '.$this->getMaxScore()."\n"; 139 | $message .= 'Min score : '.$this->getMinScore(); 140 | } 141 | $this->send($this->getCurrentChannel(), null, $message); 142 | $this->status = 'free'; 143 | } else { 144 | $this->send( 145 | $this->getCurrentChannel(), 146 | $this->getCurrentUser(), 147 | 'Only '.$this->getUserNameFromUserId($this->initiator).' can end the session' 148 | ); 149 | } 150 | } else { 151 | $this->send( 152 | $this->getCurrentChannel(), 153 | $this->getCurrentUser(), 154 | 'There is no poker session. You can start one with "'.$this->getName().' start"' 155 | ); 156 | } 157 | } 158 | 159 | private function getArgs($message) 160 | { 161 | $args = array(); 162 | if (isset($message['text'])) { 163 | $args = array_values(array_filter(explode(' ', $message['text']))); 164 | } 165 | $commandName = $this->getName(); 166 | // Remove args which are before the command name 167 | $finalArgs = array(); 168 | $remove = true; 169 | foreach ($args as $arg) { 170 | if ($commandName == $arg) { 171 | $remove = false; 172 | } 173 | if (!$remove) { 174 | $finalArgs[] = $arg; 175 | } 176 | } 177 | return $finalArgs; 178 | } 179 | 180 | private function getAverageScore() 181 | { 182 | return array_sum($this->scores) / count($this->scores); 183 | } 184 | 185 | private function getMedianScore() 186 | { 187 | $arr = $this->scores; 188 | sort($arr); 189 | $count = count($arr); 190 | $middleval = floor(($count-1)/2); 191 | if ($count % 2) { 192 | $median = $arr[$middleval]; 193 | } else { 194 | $low = $arr[$middleval]; 195 | $high = $arr[$middleval+1]; 196 | $median = (($low+$high)/2); 197 | } 198 | return $median; 199 | } 200 | 201 | private function getSequence() 202 | { 203 | return array(0, 1, 2, 3, 5, 8, 13, 20, 40, 100); 204 | } 205 | 206 | private function getMaxScore() 207 | { 208 | return max($this->scores); 209 | } 210 | 211 | private function getMinScore() 212 | { 213 | return min($this->scores); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Webhook/BaseWebhook.php: -------------------------------------------------------------------------------- 1 | execute($payload, $context); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/PhpSlackBot/Webhook/OutputWebhook.php: -------------------------------------------------------------------------------- 1 | setName('output'); 10 | } 11 | 12 | protected function execute($payload, $context) 13 | { 14 | $payload['channel'] = $this->getChannelIdFromChannelName($payload['channel']); 15 | $this->getClient()->send(json_encode($payload)); 16 | } 17 | } 18 | --------------------------------------------------------------------------------