1 <?php
2 namespace Slack;
3
4 use Devristo\Phpws\Client\WebSocket;
5 use Devristo\Phpws\Messaging\WebSocketMessageInterface;
6 use Evenement\EventEmitterTrait;
7 use React\Promise;
8 use Slack\Message\Message;
9
10 11 12
13 class RealTimeClient extends ApiClient
14 {
15 use EventEmitterTrait;
16
17 18 19
20 protected $websocket;
21
22 23 24
25 protected $lastMessageId = 0;
26
27 28 29 30
31 protected $pendingMessages = [];
32
33 34 35
36 protected $connected = false;
37
38 39 40
41 protected $team;
42
43 44 45
46 protected $users = [];
47
48 49 50
51 protected $channels = [];
52
53 54 55
56 protected $groups = [];
57
58 59 60
61 protected $dms = [];
62
63 64 65
66 protected $bots = [];
67
68 69 70 71 72
73 public function connect()
74 {
75 $deferred = new Promise\Deferred();
76
77
78 $this->apiCall('rtm.start')
79
80
81 ->then(function (Payload $response) {
82 $responseData = $response->getData();
83
84 $this->team = new Team($this, $responseData['team']);
85
86
87 $this->users[$responseData['self']['id']] = new User($this, $responseData['self']);
88
89
90 foreach ($responseData['users'] as $data) {
91 $this->users[$data['id']] = new User($this, $data);
92 }
93
94
95 foreach ($responseData['channels'] as $data) {
96 $this->channels[$data['id']] = new Channel($this, $data);
97 }
98
99
100 foreach ($responseData['groups'] as $data) {
101 $this->groups[$data['id']] = new Group($this, $data);
102 }
103
104
105 foreach ($responseData['ims'] as $data) {
106 $this->dms[$data['id']] = new DirectMessageChannel($this, $data);
107 }
108
109
110 foreach ($responseData['bots'] as $data) {
111 $this->bots[$data['id']] = new Bot($this, $data);
112 }
113
114
115 $logger = new \Zend\Log\Logger();
116 $logger->addWriter(new \Zend\Log\Writer\Stream('php://stderr'));
117
118
119 $this->websocket = new WebSocket($responseData['url'], $this->loop, $logger);
120 $this->websocket->on('message', function ($message) {
121 $this->onMessage($message);
122 });
123
124 return $this->websocket->open();
125 }, function($exception) use ($deferred) {
126
127 $deferred->reject(new ConnectionException(
128 'Could not connect to Slack API: '. $exception->getMessage(),
129 $exception->getCode()
130 ));
131 })
132
133
134 ->then(function () use ($deferred) {
135 $this->once('hello', function () use ($deferred) {
136 $deferred->resolve();
137 });
138
139 $this->once('error', function ($data) use ($deferred) {
140 $deferred->reject(new ConnectionException(
141 'Could not connect to WebSocket: '.$data['error']['msg'],
142 $data['error']['code']));
143 });
144 });
145
146 return $deferred->promise();
147 }
148
149 150 151
152 public function disconnect()
153 {
154 if (!$this->connected) {
155 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
156 }
157
158 $this->websocket->close();
159 $this->connected = false;
160 }
161
162 163 164
165 public function getTeam()
166 {
167 if (!$this->connected) {
168 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
169 }
170
171 return Promise\resolve($this->team);
172 }
173
174 175 176
177 public function getChannels()
178 {
179 if (!$this->connected) {
180 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
181 }
182
183 return Promise\resolve(array_values($this->channels));
184 }
185
186 187 188
189 public function getChannelById($id)
190 {
191 if (!$this->connected) {
192 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
193 }
194
195 if (!isset($this->channels[$id])) {
196 return Promise\reject(new ApiException("No channel exists for ID '$id'."));
197 }
198
199 return Promise\resolve($this->channels[$id]);
200 }
201
202 203 204
205 public function getGroups()
206 {
207 if (!$this->connected) {
208 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
209 }
210
211 return Promise\resolve(array_values($this->groups));
212 }
213
214 215 216
217 public function getGroupById($id)
218 {
219 if (!$this->connected) {
220 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
221 }
222
223 if (!isset($this->groups[$id])) {
224 return Promise\reject(new ApiException("No group exists for ID '$id'."));
225 }
226
227 return Promise\resolve($this->groups[$id]);
228 }
229
230 231 232
233 public function getDMs()
234 {
235 if (!$this->connected) {
236 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
237 }
238
239 return Promise\resolve(array_values($this->dms));
240 }
241
242 243 244
245 public function getDMById($id)
246 {
247 if (!$this->connected) {
248 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
249 }
250
251 if (!isset($this->dms[$id])) {
252 return Promise\reject(new ApiException("No DM exists for ID '$id'."));
253 }
254
255 return Promise\resolve($this->dms[$id]);
256 }
257
258 259 260
261 public function getUsers()
262 {
263 if (!$this->connected) {
264 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
265 }
266
267 return Promise\resolve(array_values($this->users));
268 }
269
270 271 272
273 public function getUserById($id)
274 {
275 if (!$this->connected) {
276 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
277 }
278
279 if (!isset($this->users[$id])) {
280 return Promise\reject(new ApiException("No user exists for ID '$id'."));
281 }
282
283 return Promise\resolve($this->users[$id]);
284 }
285
286 287 288 289 290
291 public function getBots()
292 {
293 if (!$this->connected) {
294 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
295 }
296
297 return Promise\resolve(array_values($this->bots));
298 }
299
300 301 302 303 304 305 306
307 public function getBotById($id)
308 {
309 if (!$this->connected) {
310 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
311 }
312
313 if (!isset($this->bots[$id])) {
314 return Promise\reject(new ApiException("No bot exists for ID '$id'."));
315 }
316
317 return Promise\resolve($this->bots[$id]);
318 }
319
320 321 322
323 public function postMessage(Message $message)
324 {
325 if (!$this->connected) {
326 return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
327 }
328
329
330
331 if ($message->hasAttachments()) {
332 return parent::postMessage($message);
333 }
334
335 $data = [
336 'id' => ++$this->lastMessageId,
337 'type' => 'message',
338 'channel' => $message->data['channel'],
339 'text' => $message->getText(),
340 ];
341 $this->websocket->send(json_encode($data));
342
343
344
345 $deferred = new Promise\Deferred();
346 $this->pendingMessages[$this->lastMessageId] = $deferred;
347
348 return $deferred->promise();
349 }
350
351 352 353 354 355
356 public function isConnected()
357 {
358 return $this->connected;
359 }
360
361 362 363 364 365
366 private function onMessage(WebSocketMessageInterface $message)
367 {
368
369 $payload = Payload::fromJson($message->getData());
370
371 if (isset($payload['type'])) {
372 switch ($payload['type']) {
373 case 'hello':
374 $this->connected = true;
375 break;
376
377 case 'team_rename':
378 $this->team->data['name'] = $payload['name'];
379 break;
380
381 case 'team_domain_change':
382 $this->team->data['domain'] = $payload['domain'];
383 break;
384
385 case 'channel_joined':
386 $channel = new Channel($this, $payload['channel']);
387 $this->channels[$channel->getId()] = $channel;
388 break;
389
390 case 'channel_created':
391 $this->getChannelById($payload['channel']['id'])->then(function (Channel $channel) {
392 $this->channels[$channel->getId()] = $channel;
393 });
394 break;
395
396 case 'channel_deleted':
397 unset($this->channels[$payload['channel']['id']]);
398 break;
399
400 case 'channel_rename':
401 $this->channels[$payload['channel']['id']]->data['name']
402 = $payload['channel']['name'];
403 break;
404
405 case 'channel_archive':
406 $this->channels[$payload['channel']['id']]->data['is_archived'] = true;
407 break;
408
409 case 'channel_unarchive':
410 $this->channels[$payload['channel']['id']]->data['is_archived'] = false;
411 break;
412
413 case 'group_joined':
414 $group = new Group($this, $payload['channel']);
415 $this->groups[$group->getId()] = $group;
416 break;
417
418 case 'group_rename':
419 $this->groups[$payload['group']['id']]->data['name']
420 = $payload['channel']['name'];
421 break;
422
423 case 'group_archive':
424 $this->groups[$payload['group']['id']]->data['is_archived'] = true;
425 break;
426
427 case 'group_unarchive':
428 $this->groups[$payload['group']['id']]->data['is_archived'] = false;
429 break;
430
431 case 'im_created':
432 $dm = new DirectMessageChannel($this, $payload['channel']);
433 $this->dms[$dm->getId()] = $dm;
434 break;
435
436 case 'bot_added':
437 $bot = new Bot($this, $payload['bot']);
438 $this->bots[$bot->getId()] = $bot;
439 break;
440
441 case 'bot_changed':
442 $bot = new Bot($this, $payload['bot']);
443 $this->bots[$bot->getId()] = $bot;
444 break;
445 }
446
447
448 $this->emit($payload['type'], [$payload]);
449 } else {
450
451
452 if (isset($payload['reply_to'])) {
453 if (isset($this->pendingMessages[$payload['reply_to']])) {
454 $deferred = $this->pendingMessages[$payload['reply_to']];
455
456
457 if (isset($payload['ok']) && $payload['ok'] === true) {
458 $deferred->resolve();
459 } else {
460 $deferred->reject($payload['error']);
461 }
462
463 unset($this->pendingMessages[$payload['reply_to']]);
464 }
465 }
466 }
467 }
468 }
469