Test Setup Failed
Pull Request — master (#46)
by
unknown
07:36
created

RealTimeClient::handlePayload()   D

Complexity

Conditions 22
Paths 21

Size

Total Lines 99
Code Lines 69

Duplication

Lines 12
Ratio 12.12 %

Code Coverage

Tests 0
CRAP Score 506

Importance

Changes 0
Metric Value
dl 12
loc 99
ccs 0
cts 67
cp 0
rs 4.6625
c 0
b 0
f 0
cc 22
eloc 69
nc 21
nop 1
crap 506

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use Throwable;
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 (Throwable $throwable) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
374
            $this->logger->warning('Payload handling error: '.$throwable->getMessage());
375
            $this->logger->warning('Payload: '.$payload->toJson());
376
            $this->logger->warning($throwable->getTraceAsString());
377
        }
378
    }
379
380
    private function handlePayload(Payload $payload)
381
    {
382
        if (isset($payload['type'])) {
383
            switch ($payload['type']) {
384
                case 'hello':
385
                    $this->connected = true;
386
                    break;
387
388
                case 'team_rename':
389
                    $this->team->data['name'] = $payload['name'];
390
                    break;
391
392
                case 'team_domain_change':
393
                    $this->team->data['domain'] = $payload['domain'];
394
                    break;
395
396
                case 'channel_joined':
397
                    $channel = new Channel($this, $payload['channel']);
398
                    $this->channels[$channel->getId()] = $channel;
399
                    break;
400
401
                case 'channel_created':
402
                    $this->getChannelById($payload['channel']['id'])->then(function (Channel $channel) {
403
                        $this->channels[$channel->getId()] = $channel;
404
                    });
405
                    break;
406
407
                case 'channel_deleted':
408
                    unset($this->channels[$payload['channel']['id']]);
409
                    break;
410
411
                case 'channel_rename':
412
                    $this->channels[$payload['channel']['id']]->data['name']
413
                        = $payload['channel']['name'];
414
                    break;
415
416 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...
417
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = true;
418
                    break;
419
420 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...
421
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = false;
422
                    break;
423
424
                case 'group_joined':
425
                    $group = new Group($this, $payload['channel']);
426
                    $this->groups[$group->getId()] = $group;
427
                    break;
428
429
                case 'group_rename':
430
                    $this->groups[$payload['group']['id']]->data['name']
431
                        = $payload['channel']['name'];
432
                    break;
433
434 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...
435
                    $this->groups[$payload['group']['id']]->data['is_archived'] = true;
436
                    break;
437
438 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...
439
                    $this->groups[$payload['group']['id']]->data['is_archived'] = false;
440
                    break;
441
442
                case 'im_created':
443
                    $dm = new DirectMessageChannel($this, $payload['channel']);
444
                    $this->dms[$dm->getId()] = $dm;
445
                    break;
446
447
                case 'bot_added':
448
                    $bot = new Bot($this, $payload['bot']);
449
                    $this->bots[$bot->getId()] = $bot;
450
                    break;
451
452
                case 'bot_changed':
453
                    $bot = new Bot($this, $payload['bot']);
454
                    $this->bots[$bot->getId()] = $bot;
455
                    break;
456
            }
457
458
            // emit an event with the attached json
459
            $this->emit($payload['type'], [$payload]);
460
        } else {
461
            // If reply_to is set, then it is a server confirmation for a previously
462
            // sent message
463
            if (isset($payload['reply_to'])) {
464
                if (isset($this->pendingMessages[$payload['reply_to']])) {
465
                    $deferred = $this->pendingMessages[$payload['reply_to']];
466
467
                    // Resolve or reject the promise that was waiting for the reply.
468
                    if (isset($payload['ok']) && $payload['ok'] === true) {
469
                        $deferred->resolve();
470
                    } else {
471
                        $deferred->reject($payload['error']);
472
                    }
473
474
                    unset($this->pendingMessages[$payload['reply_to']]);
475
                }
476
            }
477
        }
478
    }
479
}
480