Passed
Push — master ( 486903...6ccc61 )
by Stephen
02:36
created

RealTimeClient::getBots()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 8
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 8
loc 8
ccs 0
cts 7
cp 0
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
crap 6
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
 * A client for the Slack real-time messaging API.
12
 */
13
class RealTimeClient extends ApiClient
14
{
15
    use EventEmitterTrait;
16
17
    /**
18
     * @var WebSocket A websocket connection to the Slack API.
19
     */
20
    protected $websocket;
21
22
    /**
23
     * @var int The ID of the last payload sent to Slack.
24
     */
25
    protected $lastMessageId = 0;
26
27
    /**
28
     * @var array An array of pending messages waiting for successful confirmation
29
     *            from Slack.
30
     */
31
    protected $pendingMessages = [];
32
33
    /**
34
     * @var bool Indicates if the client is connected.
35
     */
36
    protected $connected = false;
37
38
    /**
39
     * @var Team The team logged in to.
40
     */
41
    protected $team;
42
43
    /**
44
     * @var array A map of users.
45
     */
46
    protected $users = [];
47
48
    /**
49
     * @var array A map of channels.
50
     */
51
    protected $channels = [];
52
53
    /**
54
     * @var array A map of groups.
55
     */
56
    protected $groups = [];
57
58
    /**
59
     * @var array A map of direct message channels.
60
     */
61
    protected $dms = [];
62
63
    /**
64
     * @var array A map of bots.
65
     */
66
    protected $bots = [];
67
68
    /**
69
     * Connects to the real-time messaging server.
70
     *
71
     * @return \React\Promise\PromiseInterface
72
     */
73
    public function connect()
74
    {
75
        $deferred = new Promise\Deferred();
76
77
        // Request a real-time connection...
78
        $this->apiCall('rtm.start')
79
80
        // then connect to the socket...
81
        ->then(function (Payload $response) {
82
            $responseData = $response->getData();
83
            // get the team info
84
            $this->team = new Team($this, $responseData['team']);
85
86
            // Populate self user.
87
            $this->users[$responseData['self']['id']] = new User($this, $responseData['self']);
88
89
            // populate list of users
90
            foreach ($responseData['users'] as $data) {
91
                $this->users[$data['id']] = new User($this, $data);
92
            }
93
94
            // populate list of channels
95
            foreach ($responseData['channels'] as $data) {
96
                $this->channels[$data['id']] = new Channel($this, $data);
97
            }
98
99
            // populate list of groups
100
            foreach ($responseData['groups'] as $data) {
101
                $this->groups[$data['id']] = new Group($this, $data);
102
            }
103
104
            // populate list of dms
105
            foreach ($responseData['ims'] as $data) {
106
                $this->dms[$data['id']] = new DirectMessageChannel($this, $data);
107
            }
108
109
            // populate list of bots
110
            foreach ($responseData['bots'] as $data) {
111
                $this->bots[$data['id']] = new Bot($this, $data);
112
            }
113
114
            // Make a dummy log to make PHPWS happy
115
            $logger = new \Zend\Log\Logger();
116
            $logger->addWriter(new \Zend\Log\Writer\Noop());
117
118
            // initiate the websocket connection
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
            // if connection was not succesfull
127
            $deferred->reject(new ConnectionException(
128
                'Could not connect to Slack API: '. $exception->getMessage(),
129
                $exception->getCode()
130
            ));
131
        })
132
133
        // then wait for the connection to be ready.
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
     * Disconnects the client.
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
     * {@inheritDoc}
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
     * {@inheritDoc}
176
     */
177 View Code Duplication
    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
     * {@inheritDoc}
188
     */
189 View Code Duplication
    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
     * {@inheritDoc}
204
     */
205 View Code Duplication
    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
     * {@inheritDoc}
216
     */
217 View Code Duplication
    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
     * {@inheritDoc}
232
     */
233 View Code Duplication
    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
     * {@inheritDoc}
244
     */
245 View Code Duplication
    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
     * {@inheritDoc}
260
     */
261 View Code Duplication
    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
     * {@inheritDoc}
272
     */
273 View Code Duplication
    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
     * Gets all bots in the Slack team.
288
     *
289
     * @return \React\Promise\PromiseInterface A promise for an array of bots.
290
     */
291 View Code Duplication
    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
     * Gets a bot by its ID.
302
     *
303
     * @param string $id A bot ID.
304
     *
305
     * @return \React\Promise\PromiseInterface A promise for a bot object.
306
     */
307 View Code Duplication
    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
     * {@inheritDoc}
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
        // We can't send attachments using the RTM API, so revert to the web API
330
        // to send the message
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
        // Create a deferred object and add message to pending list so when a
344
        // success message arrives, we can de-queue it and resolve the promise.
345
        $deferred = new Promise\Deferred();
346
        $this->pendingMessages[$this->lastMessageId] = $deferred;
347
348
        return $deferred->promise();
349
    }
350
351
    /**
352
     * Handles incoming websocket messages, parses them, and emits them as remote events.
353
     *
354
     * @param WebSocketMessageInterface $messageRaw A websocket message.
0 ignored issues
show
Documentation introduced by
There is no parameter named $messageRaw. Did you maybe mean $message?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
355
     */
356
    private function onMessage(WebSocketMessageInterface $message)
357
    {
358
        // parse the message and get the event name
359
        $payload = Payload::fromJson($message->getData());
360
361
        if (isset($payload['type'])) {
362
            switch ($payload['type']) {
363
                case 'hello':
364
                    $this->connected = true;
365
                    break;
366
367
                case 'team_rename':
368
                    $this->team->data['name'] = $payload['name'];
369
                    break;
370
371
                case 'team_domain_change':
372
                    $this->team->data['domain'] = $payload['domain'];
373
                    break;
374
375
                case 'channel_created':
376
                    $this->getChannelById($payload['channel']['id'])->then(function (Channel $channel) {
377
                        $this->channels[$channel->getId()] = $channel;
378
                    });
379
                    break;
380
381
                case 'channel_deleted':
382
                    unset($this->channels[$payload['channel']['id']]);
383
                    break;
384
385
                case 'channel_rename':
386
                    $this->channels[$payload['channel']['id']]->data['name']
387
                        = $payload['channel']['name'];
388
                    break;
389
390 View Code Duplication
                case 'channel_archive':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
391
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = true;
392
                    break;
393
394 View Code Duplication
                case 'channel_unarchive':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
395
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = false;
396
                    break;
397
398
                case 'group_joined':
399
                    $group = new Group($this, $payload['channel']);
400
                    $this->groups[$group->getId()] = $group;
401
                    break;
402
403
                case 'group_rename':
404
                    $this->groups[$payload['group']['id']]->data['name']
405
                        = $payload['channel']['name'];
406
                    break;
407
408 View Code Duplication
                case 'group_archive':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
409
                    $this->groups[$payload['group']['id']]->data['is_archived'] = true;
410
                    break;
411
412 View Code Duplication
                case 'group_unarchive':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
413
                    $this->groups[$payload['group']['id']]->data['is_archived'] = false;
414
                    break;
415
416
                case 'im_created':
417
                    $dm = new DirectMessageChannel($this, $payload['channel']);
418
                    $this->dms[$dm->getId()] = $dm;
419
                    break;
420
421
                case 'bot_added':
422
                    $bot = new Bot($this, $payload['bot']);
423
                    $this->bots[$bot->getId()] = $bot;
424
                    break;
425
426
                case 'bot_changed':
427
                    $bot = new Bot($this, $payload['bot']);
428
                    $this->bots[$bot->getId()] = $bot;
429
                    break;
430
            }
431
432
            // emit an event with the attached json
433
            $this->emit($payload['type'], [$payload]);
434
        } else {
435
            // If reply_to is set, then it is a server confirmation for a previously
436
            // sent message
437
            if (isset($payload['reply_to'])) {
438
                if (isset($this->pendingMessages[$payload['reply_to']])) {
439
                    $deferred = $this->pendingMessages[$payload['reply_to']];
440
441
                    // Resolve or reject the promise that was waiting for the reply.
442
                    if (isset($payload['ok']) && $payload['ok'] === true) {
443
                        $deferred->resolve();
444
                    } else {
445
                        $deferred->reject($payload['error']);
446
                    }
447
448
                    unset($this->pendingMessages[$payload['reply_to']]);
449
                }
450
            }
451
        }
452
    }
453
}
454