RealTimeClient::getUsers()   A
last analyzed

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 2
Bugs 0 Features 0
Metric Value
dl 8
loc 8
ccs 0
cts 7
cp 0
rs 9.4285
c 2
b 0
f 0
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
            // Log PHPWS things to stderr
115
            $logger = new \Zend\Log\Logger();
116
            $logger->addWriter(new \Zend\Log\Writer\Stream('php://stderr'));
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
     * Returns whether the client is connected.
353
     *
354
     * @return bool
355
     */
356
    public function isConnected()
357
    {
358
        return $this->connected;
359
    }
360
361
    /**
362
     * Handles incoming websocket messages, parses them, and emits them as remote events.
363
     *
364
     * @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...
365
     */
366
    private function onMessage(WebSocketMessageInterface $message)
367
    {
368
        // parse the message and get the event name
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 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...
406
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = true;
407
                    break;
408
409 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...
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 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...
424
                    $this->groups[$payload['group']['id']]->data['is_archived'] = true;
425
                    break;
426
427 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...
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
            // emit an event with the attached json
448
            $this->emit($payload['type'], [$payload]);
449
        } else {
450
            // If reply_to is set, then it is a server confirmation for a previously
451
            // sent message
452
            if (isset($payload['reply_to'])) {
453
                if (isset($this->pendingMessages[$payload['reply_to']])) {
454
                    $deferred = $this->pendingMessages[$payload['reply_to']];
455
456
                    // Resolve or reject the promise that was waiting for the reply.
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