Test Setup Failed
Pull Request — master (#46)
by Dan
02:32
created

RealTimeClient::onMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 6
Bugs 0 Features 0
Metric Value
c 6
b 0
f 0
dl 0
loc 14
ccs 0
cts 10
cp 0
rs 9.4285
cc 2
eloc 9
nc 2
nop 1
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 Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Slack\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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