├── .gitignore ├── README.md ├── composer.json └── src ├── Contracts └── Http │ ├── Interactor.php │ ├── Response.php │ └── ResponseFactory.php ├── Core └── Commander.php └── Http ├── CurlInteractor.php ├── SlackResponse.php └── SlackResponseFactory.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP Slack 2 | ========= 3 | 4 | > A lightweight PHP implementation of Slack's API. 5 | 6 | ### Provides 7 | 8 | * Frlnc\Slack\Contracts 9 | 10 | A small set of contracts to allow for the consumption of the Slack API. **Interactor**, **Response** and **ResponseFactory**. 11 | 12 | * **Interactor** is in charge of providing the Http GET/POST methods. 13 | * **Response** is in charge of providing a simple Http response wrapper for holding the body, headers and status code. 14 | * **ResponseFactory** is in charge of providing a factory to instantiate and build the **Response**. 15 | 16 | To use this package, it's simple. Though _please note_ that this implementation is very lightweight meaning you'll need to do some more work than usual. This package doesn't provide methods such as `Chat::postMessage(string message)`, it literally provides one method (`Commander::execute(string command, array parameters = [])`). 17 | 18 | Here is a very simple example of using this package: 19 | ```php 20 | setResponseFactory(new SlackResponseFactory); 28 | 29 | $commander = new Commander('xoxp-some-token-for-slack', $interactor); 30 | 31 | $response = $commander->execute('chat.postMessage', [ 32 | 'channel' => '#general', 33 | 'text' => 'Hello, world!' 34 | ]); 35 | 36 | if ($response['ok']) 37 | { 38 | // Command worked 39 | } 40 | else 41 | { 42 | // Command didn't work 43 | } 44 | ``` 45 | 46 | Note that Commander will automatically format most inputs to Slack's requirements but attachments are not supported - you will need to manually call `$text = Commander::format($text)` to convert it. 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frlnc/php-slack", 3 | "description": "A lightweight PHP implementation of Slack's API.", 4 | "keywords": ["slack"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Connor Parks", 9 | "email": "connor@connorvg.tv" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.4.0" 14 | }, 15 | "require-dev": { 16 | "mockery/mockery": "~0.9", 17 | "phpunit/phpunit": "~4.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Frlnc\\Slack\\": "src/" 22 | } 23 | }, 24 | "minimum-stability": "dev" 25 | } 26 | -------------------------------------------------------------------------------- /src/Contracts/Http/Interactor.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'endpoint' => '/api.test', 23 | 'token' => false 24 | ], 25 | 'auth.test' => [ 26 | 'endpoint' => '/auth.test', 27 | 'token' => true 28 | ], 29 | 'channels.archive' => [ 30 | 'token' => true, 31 | 'endpoint' => '/channels.archive' 32 | ], 33 | 'channels.create' => [ 34 | 'token' => true, 35 | 'endpoint' => '/channels.create' 36 | ], 37 | 'channels.history' => [ 38 | 'token' => true, 39 | 'endpoint' => '/channels.history' 40 | ], 41 | 'channels.info' => [ 42 | 'token' => true, 43 | 'endpoint' => '/channels.info' 44 | ], 45 | 'channels.invite' => [ 46 | 'token' => true, 47 | 'endpoint' => '/channels.invite' 48 | ], 49 | 'channels.join' => [ 50 | 'token' => true, 51 | 'endpoint' => '/channels.join' 52 | ], 53 | 'channels.kick' => [ 54 | 'token' => true, 55 | 'endpoint' => '/channels.kick' 56 | ], 57 | 'channels.leave' => [ 58 | 'token' => true, 59 | 'endpoint' => '/channels.leave' 60 | ], 61 | 'channels.list' => [ 62 | 'token' => true, 63 | 'endpoint' => '/channels.list' 64 | ], 65 | 'channels.mark' => [ 66 | 'token' => true, 67 | 'endpoint' => '/channels.mark' 68 | ], 69 | 'channels.rename' => [ 70 | 'token' => true, 71 | 'endpoint' => '/channels.rename' 72 | ], 73 | 'channels.setPurpose' => [ 74 | 'token' => true, 75 | 'endpoint' => '/channels.setPurpose', 76 | 'format' => [ 77 | 'purpose' 78 | ] 79 | ], 80 | 'channels.setTopic' => [ 81 | 'token' => true, 82 | 'endpoint' => '/channels.setTopic', 83 | 'format' => [ 84 | 'topic' 85 | ] 86 | ], 87 | 'channels.unarchive' => [ 88 | 'token' => true, 89 | 'endpoint' => '/channels.unarchive' 90 | ], 91 | 'chat.delete' => [ 92 | 'token' => true, 93 | 'endpoint' => '/chat.delete' 94 | ], 95 | 'chat.postMessage' => [ 96 | 'token' => true, 97 | 'endpoint' => '/chat.postMessage', 98 | 'format' => [ 99 | 'text', 100 | 'username' 101 | ] 102 | ], 103 | 'chat.update' => [ 104 | 'token' => true, 105 | 'endpoint' => '/chat.update', 106 | 'format' => [ 107 | 'text' 108 | ] 109 | ], 110 | 'dnd.endDnd' => [ 111 | 'token' => true, 112 | 'endpoint' => '/dnd.endDnd' 113 | ], 114 | 'dnd.endSnooze' => [ 115 | 'token' => true, 116 | 'endpoint' => '/dnd.endSnooze' 117 | ], 118 | 'dnd.info' => [ 119 | 'token' => true, 120 | 'endpoint' => '/dnd.info' 121 | ], 122 | 'dnd.setSnooze' => [ 123 | 'token' => true, 124 | 'endpoint' => '/dnd.setSnooze' 125 | ], 126 | 'dnd.teamInfo' => [ 127 | 'token' => true, 128 | 'endpoint' => '/dnd.teamInfo' 129 | ], 130 | 'emoji.list' => [ 131 | 'token' => true, 132 | 'endpoint' => '/emoji.list' 133 | ], 134 | 'files.comments.add' => [ 135 | 'token' => true, 136 | 'endpoint' => '/files.comments.add' 137 | ], 138 | 'files.comments.delete' => [ 139 | 'token' => true, 140 | 'endpoint' => '/files.comments.delete' 141 | ], 142 | 'files.comments.edit' => [ 143 | 'token' => true, 144 | 'endpoint' => '/files.comments.edit' 145 | ], 146 | 'files.delete' => [ 147 | 'token' => true, 148 | 'endpoint' => '/files.delete' 149 | ], 150 | 'files.info' => [ 151 | 'token' => true, 152 | 'endpoint' => '/files.info' 153 | ], 154 | 'files.list' => [ 155 | 'token' => true, 156 | 'endpoint' => '/files.list' 157 | ], 158 | 'files.revokePublicURL' => [ 159 | 'token' => true, 160 | 'endpoint' => '/files.revokePublicURL' 161 | ], 162 | 'files.sharedPublcURL' => [ 163 | 'token' => true, 164 | 'endpoint' => '/files.sharedPublcURL' 165 | ], 166 | 'files.upload' => [ 167 | 'token' => true, 168 | 'endpoint' => '/files.upload', 169 | 'post' => true, 170 | 'headers' => [ 171 | 'Content-Type' => 'multipart/form-data' 172 | ], 173 | 'format' => [ 174 | 'filename', 175 | 'title', 176 | 'initial_comment' 177 | ] 178 | ], 179 | 'groups.archive' => [ 180 | 'token' => true, 181 | 'endpoint' => '/groups.archive' 182 | ], 183 | 'groups.close' => [ 184 | 'token' => true, 185 | 'endpoint' => '/groups.close' 186 | ], 187 | 'groups.create' => [ 188 | 'token' => true, 189 | 'endpoint' => '/groups.create', 190 | 'format' => [ 191 | 'name' 192 | ] 193 | ], 194 | 'groups.createChild' => [ 195 | 'token' => true, 196 | 'endpoint' => '/groups.createChild' 197 | ], 198 | 'groups.history' => [ 199 | 'token' => true, 200 | 'endpoint' => '/groups.history' 201 | ], 202 | 'groups.info' => [ 203 | 'token' => true, 204 | 'endpoint' => '/groups.info' 205 | ], 206 | 'groups.invite' => [ 207 | 'token' => true, 208 | 'endpoint' => '/groups.invite' 209 | ], 210 | 'groups.kick' => [ 211 | 'token' => true, 212 | 'endpoint' => '/groups.kick' 213 | ], 214 | 'groups.leave' => [ 215 | 'token' => true, 216 | 'endpoint' => '/groups.leave' 217 | ], 218 | 'groups.list' => [ 219 | 'token' => true, 220 | 'endpoint' => '/groups.list' 221 | ], 222 | 'groups.mark' => [ 223 | 'token' => true, 224 | 'endpoint' => '/groups.mark' 225 | ], 226 | 'groups.open' => [ 227 | 'token' => true, 228 | 'endpoint' => '/groups.open' 229 | ], 230 | 'groups.rename' => [ 231 | 'token' => true, 232 | 'endpoint' => '/groups.rename' 233 | ], 234 | 'groups.setPurpose' => [ 235 | 'token' => true, 236 | 'endpoint' => '/groups.setPurpose', 237 | 'format' => [ 238 | 'purpose' 239 | ] 240 | ], 241 | 'groups.setTopic' => [ 242 | 'token' => true, 243 | 'endpoint' => '/groups.setTopic', 244 | 'format' => [ 245 | 'topic' 246 | ] 247 | ], 248 | 'groups.unarchive' => [ 249 | 'token' => true, 250 | 'endpoint' => '/groups.unarchive' 251 | ], 252 | 'im.close' => [ 253 | 'token' => true, 254 | 'endpoint' => '/im.close' 255 | ], 256 | 'im.history' => [ 257 | 'token' => true, 258 | 'endpoint' => '/im.history' 259 | ], 260 | 'im.list' => [ 261 | 'token' => true, 262 | 'endpoint' => '/im.list' 263 | ], 264 | 'im.mark' => [ 265 | 'token' => true, 266 | 'endpoint' => '/im.mark' 267 | ], 268 | 'im.open' => [ 269 | 'token' => true, 270 | 'endpoint' => '/im.open' 271 | ], 272 | 'mpim.close' => [ 273 | 'token' => true, 274 | 'endpoint' => '/mpim.close' 275 | ], 276 | 'mpmim.history' => [ 277 | 'token' => true, 278 | 'endpoint' => '/mpmim.history' 279 | ], 280 | 'mpim.list' => [ 281 | 'token' => true, 282 | 'endpoint' => '/mpim.list' 283 | ], 284 | 'mpim.mark' => [ 285 | 'token' => true, 286 | 'endpoint' => '/mpim.mark' 287 | ], 288 | 'mpim.open' => [ 289 | 'token' => true, 290 | 'endpoint' => '/mpim.open' 291 | ], 292 | 'oauth.access' => [ 293 | 'token' => false, 294 | 'endpoint' => '/oauth.access' 295 | ], 296 | 'pins.add' => [ 297 | 'token' => true, 298 | 'endpoint' => '/pins.add' 299 | ], 300 | 'pins.list' => [ 301 | 'token' => true, 302 | 'endpoint' => '/pins.list' 303 | ], 304 | 'pins.remove' => [ 305 | 'token' => true, 306 | 'endpoint' => '/pins.remove' 307 | ], 308 | 'reactions.add' => [ 309 | 'token' => true, 310 | 'endpoint' => '/reactions.add' 311 | ], 312 | 'reactions.get' => [ 313 | 'token' => true, 314 | 'endpoint' => '/reactions.get' 315 | ], 316 | 'reactions.list' => [ 317 | 'token' => true, 318 | 'endpoint' => '/reactions.list' 319 | ], 320 | 'reactions.remove' => [ 321 | 'token' => true, 322 | 'endpoint' => '/reactions.remove' 323 | ], 324 | 'rtm.start' => [ 325 | 'token' => true, 326 | 'endpoint' => '/rtm.start' 327 | ], 328 | 'search.all' => [ 329 | 'token' => true, 330 | 'endpoint' => '/search.all' 331 | ], 332 | 'search.files' => [ 333 | 'token' => true, 334 | 'endpoint' => '/search.files' 335 | ], 336 | 'search.messages' => [ 337 | 'token' => true, 338 | 'endpoint' => '/search.messages' 339 | ], 340 | 'stars.add' => [ 341 | 'token' => true, 342 | 'endpoint' => '/stars.add' 343 | ], 344 | 'stars.list' => [ 345 | 'token' => true, 346 | 'endpoint' => '/stars.list' 347 | ], 348 | 'stars.remove' => [ 349 | 'token' => true, 350 | 'endpoint' => '/stars.remove' 351 | ], 352 | 'team.accessLogs' => [ 353 | 'token' => true, 354 | 'endpoint' => '/team.accessLogs' 355 | ], 356 | 'team.info' => [ 357 | 'token' => true, 358 | 'endpoint' => '/team.info' 359 | ], 360 | 'team.integrationLogs' => [ 361 | 'token' => true, 362 | 'endpoint' => '/team.integrationLogs' 363 | ], 364 | 'usergroups.create' => [ 365 | 'token' => true, 366 | 'endpoint' => '/usergroups.create' 367 | ], 368 | 'usergroups.disable' => [ 369 | 'token' => true, 370 | 'endpoint' => '/usergroups.disable' 371 | ], 372 | 'usergroups.enable' => [ 373 | 'token' => true, 374 | 'endpoint' => '/usergroups.enable' 375 | ], 376 | 'usergroups.list' => [ 377 | 'token' => true, 378 | 'endpoint' => '/usergroups.list' 379 | ], 380 | 'usergroups.update' => [ 381 | 'token' => true, 382 | 'endpoint' => '/usergroups.update' 383 | ], 384 | 'usergroups.users.list' => [ 385 | 'token' => true, 386 | 'endpoint' => '/usergroups.users.list' 387 | ], 388 | 'usergroups.users.update' => [ 389 | 'token' => true, 390 | 'endpoint' => '/usergroups.users.update' 391 | ], 392 | 'users.getPresence' => [ 393 | 'token' => true, 394 | 'endpoint' => '/users.getPresence' 395 | ], 396 | 'users.info' => [ 397 | 'token' => true, 398 | 'endpoint' => '/users.info' 399 | ], 400 | 'users.list' => [ 401 | 'token' => true, 402 | 'endpoint' => '/users.list' 403 | ], 404 | 'users.setActive' => [ 405 | 'token' => true, 406 | 'endpoint' => '/users.setActive' 407 | ], 408 | 'users.setPresence' => [ 409 | 'token' => true, 410 | 'endpoint' => '/users.setPresence' 411 | ], 412 | 'users.admin.invite' => [ 413 | 'token' => true, 414 | 'endpoint' => '/users.admin.invite' 415 | ] 416 | ]; 417 | 418 | /** 419 | * The base URL. 420 | * 421 | * @var string 422 | */ 423 | protected static $baseUrl = 'https://slack.com/api'; 424 | 425 | /** 426 | * The API token. 427 | * 428 | * @var string 429 | */ 430 | protected $token; 431 | 432 | /** 433 | * The Http interactor. 434 | * 435 | * @var \Frlnc\Slack\Contracts\Http\Interactor 436 | */ 437 | protected $interactor; 438 | 439 | /** 440 | * @param string $token 441 | * @param \Frlnc\Slack\Contracts\Http\Interactor $interactor 442 | */ 443 | public function __construct($token, Interactor $interactor) 444 | { 445 | $this->token = $token; 446 | $this->interactor = $interactor; 447 | } 448 | 449 | /** 450 | * Executes a command. 451 | * 452 | * @param string $command 453 | * @param array $parameters 454 | * @return \Frlnc\Slack\Contracts\Http\Response 455 | */ 456 | public function execute($command, array $parameters = []) 457 | { 458 | if (!isset(self::$commands[$command])) 459 | throw new InvalidArgumentException("The command '{$command}' is not currently supported"); 460 | 461 | $command = self::$commands[$command]; 462 | 463 | if ($command['token']) 464 | $parameters = array_merge($parameters, ['token' => $this->token]); 465 | 466 | if (isset($command['format'])) 467 | foreach ($command['format'] as $format) 468 | if (isset($parameters[$format])) 469 | $parameters[$format] = self::format($parameters[$format]); 470 | 471 | $headers = []; 472 | if (isset($command['headers'])) 473 | $headers = $command['headers']; 474 | 475 | $url = self::$baseUrl . $command['endpoint']; 476 | 477 | if (isset($command['post']) && $command['post']) 478 | return $this->interactor->post($url, [], $parameters, $headers); 479 | 480 | return $this->interactor->get($url, $parameters, $headers); 481 | } 482 | 483 | /** 484 | * Sets the token. 485 | * 486 | * @param string $token 487 | */ 488 | public function setToken($token) 489 | { 490 | $this->token = $token; 491 | } 492 | 493 | /** 494 | * Formats a string for Slack. 495 | * 496 | * @param string $string 497 | * @return string 498 | */ 499 | public static function format($string) 500 | { 501 | $string = str_replace('&', '&', $string); 502 | $string = str_replace('<', '<', $string); 503 | $string = str_replace('>', '>', $string); 504 | 505 | return $string; 506 | } 507 | 508 | } 509 | -------------------------------------------------------------------------------- /src/Http/CurlInteractor.php: -------------------------------------------------------------------------------- 1 | prepareRequest($url, $parameters, $headers); 20 | 21 | return $this->executeRequest($request); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function post($url, array $urlParameters = [], array $postParameters = [], array $headers = []) 28 | { 29 | $request = $this->prepareRequest($url, $urlParameters, $headers); 30 | 31 | curl_setopt($request, CURLOPT_POST, count($postParameters)); 32 | curl_setopt($request, CURLOPT_POSTFIELDS, http_build_query($postParameters)); 33 | 34 | return $this->executeRequest($request); 35 | } 36 | 37 | /** 38 | * Prepares a request using curl. 39 | * 40 | * @param string $url [description] 41 | * @param array $parameters [description] 42 | * @param array $headers [description] 43 | * @return resource 44 | */ 45 | protected static function prepareRequest($url, $parameters = [], $headers = []) 46 | { 47 | $request = curl_init(); 48 | 49 | if ($query = http_build_query($parameters)) 50 | $url .= '?' . $query; 51 | 52 | curl_setopt($request, CURLOPT_URL, $url); 53 | curl_setopt($request, CURLOPT_RETURNTRANSFER, true); 54 | curl_setopt($request, CURLOPT_HTTPHEADER, $headers); 55 | curl_setopt($request, CURLINFO_HEADER_OUT, true); 56 | curl_setopt($request, CURLOPT_SSL_VERIFYPEER, false); 57 | 58 | return $request; 59 | } 60 | 61 | /** 62 | * Executes a curl request. 63 | * 64 | * @param resource $request 65 | * @return \Frlnc\Slack\Contracts\Http\Response 66 | */ 67 | public function executeRequest($request) 68 | { 69 | $body = curl_exec($request); 70 | $info = curl_getinfo($request); 71 | 72 | curl_close($request); 73 | 74 | $statusCode = $info['http_code']; 75 | $headers = $info['request_header']; 76 | 77 | if (function_exists('http_parse_headers')) 78 | $headers = http_parse_headers($headers); 79 | else 80 | { 81 | $header_text = substr($headers, 0, strpos($headers, "\r\n\r\n")); 82 | $headers = []; 83 | 84 | foreach (explode("\r\n", $header_text) as $i => $line) 85 | if ($i === 0) 86 | continue; 87 | else 88 | { 89 | list ($key, $value) = explode(': ', $line); 90 | 91 | $headers[$key] = $value; 92 | } 93 | } 94 | 95 | return $this->factory->build($body, $headers, $statusCode); 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function setResponseFactory(ResponseFactory $factory) 102 | { 103 | $this->factory = $factory; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/Http/SlackResponse.php: -------------------------------------------------------------------------------- 1 | body = json_decode($body, true); 34 | $this->headers = $headers; 35 | $this->statusCode = $statusCode; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function getBody() 42 | { 43 | return $this->body; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getHeaders() 50 | { 51 | return $this->headers; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function getStatusCode() 58 | { 59 | return $this->statusCode; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function jsonSerialize() 66 | { 67 | return $this->toArray(); 68 | } 69 | 70 | /** 71 | * Converts the response to an array. 72 | * 73 | * @return array 74 | */ 75 | public function toArray() 76 | { 77 | return [ 78 | 'status_code' => $this->getStatusCode(), 79 | 'headers' => $this->getHeaders(), 80 | 'body' => $this->getBody() 81 | ]; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Http/SlackResponseFactory.php: -------------------------------------------------------------------------------- 1 |