Passed
Pull Request — master (#17)
by Michael
03:58
created

RealTimeClient::newWebSocket()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 0
cts 7
cp 0
rs 9.4285
cc 1
eloc 5
nc 1
nop 1
crap 2
1
<?php
2
namespace Slack;
3
4
use Evenement\EventEmitterTrait;
5
use GuzzleHttp;
6
use Ratchet\Client\Connector;
7
use Ratchet\Client\WebSocket;
8
use React\EventLoop\LoopInterface;
9
use React\Promise;
10
use Slack\Message\Message;
11
12
/**
13
 * A client for the Slack real-time messaging API.
14
 */
15
class RealTimeClient extends ApiClient
16
{
17
    use EventEmitterTrait;
18
19
    /**
20
     * @var Connector Factory to create WebSocket connections.
21
     */
22
    protected $connector;
23
24
    /**
25
     * @var WebSocket A websocket connection to the Slack API.
26
     */
27
    protected $websocket;
28
29
    /**
30
     * @var int The ID of the last payload sent to Slack.
31
     */
32
    protected $lastMessageId = 0;
33
34
    /**
35
     * @var array An array of pending messages waiting for successful confirmation
36
     *            from Slack.
37
     */
38
    protected $pendingMessages = [];
39
40
    /**
41
     * @var bool Indicates if the client is connected.
42
     */
43
    protected $connected = false;
44
45
    /**
46
     * @var Team The team logged in to.
47
     */
48
    protected $team;
49
50
    /**
51
     * @var array A map of users.
52
     */
53
    protected $users = [];
54
55
    /**
56
     * @var array A map of channels.
57
     */
58
    protected $channels = [];
59
60
    /**
61
     * @var array A map of groups.
62
     */
63
    protected $groups = [];
64
65
    /**
66
     * @var array A map of direct message channels.
67
     */
68
    protected $dms = [];
69
70
    /**
71
     * RealTimeClient Constructor.
72
     *
73
     * @param LoopInterface $loop Event Loop.
74
     * @param GuzzleHttp\ClientInterface $httpClient Guzzle HTTP Client.
75
     * @param Connector $connector Connects to Slack RTM.
76
     */
77
    public function __construct(
78
        LoopInterface $loop,
79
        GuzzleHttp\ClientInterface $httpClient = null,
80
        Connector $connector = null
81
    ) {
82
        parent::__construct($loop, $httpClient);
83
84
        $this->connector = $connector ?: new Connector($loop);
85
    }
86
87
    /**
88
     * Connects to the real-time messaging server.
89
     *
90
     * @return \React\Promise\PromiseInterface
91
     */
92
    public function connect()
93
    {
94
        $deferred = new Promise\Deferred();
95
96
        // Request a real-time connection...
97
        $this->apiCall('rtm.start')
98
99
        // then connect to the socket...
100
        ->then(function (Payload $response) {
101
            $responseData = $response->getData();
102
            // get the team info
103
            $this->team = new Team($this, $responseData['team']);
104
105
            // Populate self user.
106
            $this->users[$responseData['self']['id']] = new User($this, $responseData['self']);
107
108
            // populate list of users
109
            foreach ($responseData['users'] as $data) {
110
                $this->users[$data['id']] = new User($this, $data);
111
            }
112
113
            // populate list of channels
114
            foreach ($responseData['channels'] as $data) {
115
                $this->channels[$data['id']] = new Channel($this, $data);
116
            }
117
118
            // populate list of groups
119
            foreach ($responseData['groups'] as $data) {
120
                $this->groups[$data['id']] = new Group($this, $data);
121
            }
122
123
            // populate list of dms
124
            foreach ($responseData['ims'] as $data) {
125
                $this->dms[$data['id']] = new DirectMessageChannel($this, $data);
126
            }
127
128
            // initiate the websocket connection
129
            return $this->newWebSocket($responseData['url']);
130
        }, function($exception) use ($deferred) {
131
            // if connection was not successful
132
            $deferred->reject(new ConnectionException(
133
                'Could not connect to Slack API: '. $exception->getMessage(),
134
                $exception->getCode()
135
            ));
136
        })
137
138
        // then wait for the connection to be ready.
139
        ->then(function (WebSocket $socket) use ($deferred) {
140
            $this->websocket = $socket;
141
142
            $this->once('hello', function () use ($deferred) {
143
                $deferred->resolve();
144
            });
145
146
            $this->once('error', function ($data) use ($deferred) {
147
                $deferred->reject(new ConnectionException(
148
                    'Could not connect to WebSocket: '.$data['error']['msg'],
149
                    $data['error']['code']));
150
            });
151
        });
152
153
        return $deferred->promise();
154
    }
155
156
    /**
157
     * Disconnects the client.
158
     */
159
    public function disconnect()
160
    {
161
        if (!$this->connected) {
162
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
163
        }
164
165
        $this->websocket->close();
166
        $this->connected = false;
167
    }
168
169
    /**
170
     * {@inheritDoc}
171
     */
172
    public function getTeam()
173
    {
174
        if (!$this->connected) {
175
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
176
        }
177
178
        return Promise\resolve($this->team);
179
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184 View Code Duplication
    public function getChannels()
185
    {
186
        if (!$this->connected) {
187
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
188
        }
189
190
        return Promise\resolve(array_values($this->channels));
191
    }
192
193
    /**
194
     * {@inheritDoc}
195
     */
196 View Code Duplication
    public function getChannelById($id)
197
    {
198
        if (!$this->connected) {
199
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
200
        }
201
202
        if (!isset($this->channels[$id])) {
203
            return Promise\reject(new ApiException("No channel exists for ID '$id'."));
204
        }
205
206
        return Promise\resolve($this->channels[$id]);
207
    }
208
209
    /**
210
     * {@inheritDoc}
211
     */
212 View Code Duplication
    public function getGroups()
213
    {
214
        if (!$this->connected) {
215
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
216
        }
217
218
        return Promise\resolve(array_values($this->groups));
219
    }
220
221
    /**
222
     * {@inheritDoc}
223
     */
224 View Code Duplication
    public function getGroupById($id)
225
    {
226
        if (!$this->connected) {
227
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
228
        }
229
230
        if (!isset($this->groups[$id])) {
231
            return Promise\reject(new ApiException("No group exists for ID '$id'."));
232
        }
233
234
        return Promise\resolve($this->groups[$id]);
235
    }
236
237
    /**
238
     * {@inheritDoc}
239
     */
240 View Code Duplication
    public function getDMs()
241
    {
242
        if (!$this->connected) {
243
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
244
        }
245
246
        return Promise\resolve(array_values($this->dms));
247
    }
248
249
    /**
250
     * {@inheritDoc}
251
     */
252 View Code Duplication
    public function getDMById($id)
253
    {
254
        if (!$this->connected) {
255
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
256
        }
257
258
        if (!isset($this->dms[$id])) {
259
            return Promise\reject(new ApiException("No DM exists for ID '$id'."));
260
        }
261
262
        return Promise\resolve($this->dms[$id]);
263
    }
264
265
    /**
266
     * {@inheritDoc}
267
     */
268 View Code Duplication
    public function getUsers()
269
    {
270
        if (!$this->connected) {
271
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
272
        }
273
274
        return Promise\resolve(array_values($this->users));
275
    }
276
277
    /**
278
     * {@inheritDoc}
279
     */
280 View Code Duplication
    public function getUserById($id)
281
    {
282
        if (!$this->connected) {
283
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
284
        }
285
286
        if (!isset($this->users[$id])) {
287
            return Promise\reject(new ApiException("No user exists for ID '$id'."));
288
        }
289
290
        return Promise\resolve($this->users[$id]);
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     */
296
    public function postMessage(Message $message)
297
    {
298
        if (!$this->connected) {
299
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
300
        }
301
302
        // We can't send attachments using the RTM API, so revert to the web API
303
        // to send the message
304
        if ($message->hasAttachments()) {
305
            return parent::postMessage($message);
306
        }
307
308
        $data = [
309
            'id' => ++$this->lastMessageId,
310
            'type' => 'message',
311
            'channel' => $message->data['channel'],
312
            'text' => $message->getText(),
313
        ];
314
        $this->websocket->send(json_encode($data));
315
316
        // Create a deferred object and add message to pending list so when a
317
        // success message arrives, we can de-queue it and resolve the promise.
318
        $deferred = new Promise\Deferred();
319
        $this->pendingMessages[$this->lastMessageId] = $deferred;
320
321
        return $deferred->promise();
322
    }
323
324
    /**
325
     * Creates a new WebSocket for the given URL.
326
     *
327
     * @param string $url WebSocket URL.
328
     * @return \React\Promise\PromiseInterface
329
     */
330
    private function newWebSocket($url)
331
    {
332
        return $this->connector->__invoke($url)->then(function (WebSocket $socket) {
333
            $socket->on('message', function ($data) {
334
                // parse the message and get the event name
335
                $this->onMessage(Payload::fromJson($data));
336
            });
337
338
            return $socket;
339
        });
340
    }
341
342
    /**
343
     * Handles incoming websocket messages and emits them as remote events.
344
     *
345
     * @param Payload $payload A websocket message.
346
     */
347
    private function onMessage(Payload $payload)
348
    {
349
        if (isset($payload['type'])) {
350
            switch ($payload['type']) {
351
                case 'hello':
352
                    $this->connected = true;
353
                    break;
354
355
                case 'team_rename':
356
                    $this->team->data['name'] = $payload['name'];
357
                    break;
358
359
                case 'team_domain_change':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
360
361
                    $this->team->data['domain'] = $payload['domain'];
362
                    break;
363
364
                case 'channel_created':
365
                    $this->getChannelById($payload['channel']['id'])->then(function (Channel $channel) {
366
                        $this->channels[$channel->getId()] = $channel;
367
                    });
368
                    break;
369
370
                case 'channel_deleted':
371
                    unset($this->channels[$payload['channel']['id']]);
372
                    break;
373
374
                case 'channel_rename':
375
                    $this->channels[$payload['channel']['id']]->data['name']
376
                        = $payload['channel']['name'];
377
                    break;
378
379 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...
380
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = true;
381
                    break;
382
383 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...
384
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = false;
385
                    break;
386
387
                case 'group_joined':
388
                    $group = new Group($this, $payload['channel']);
389
                    $this->groups[$group->getId()] = $group;
390
                    break;
391
392
                case 'group_rename':
393
                    $this->groups[$payload['group']['id']]->data['name']
394
                        = $payload['channel']['name'];
395
                    break;
396
397 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...
398
                    $this->groups[$payload['group']['id']]->data['is_archived'] = true;
399
                    break;
400
401 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...
402
                    $this->groups[$payload['group']['id']]->data['is_archived'] = false;
403
                    break;
404
405
                case 'im_created':
406
                    $dm = new DirectMessageChannel($this, $payload['channel']);
407
                    $this->dms[$dm->getId()] = $dm;
408
                    break;
409
            }
410
411
            // emit an event with the attached json
412
            $this->emit($payload['type'], [$payload]);
413
        } else {
414
            // If reply_to is set, then it is a server confirmation for a previously
415
            // sent message
416
            if (isset($payload['reply_to'])) {
417
                if (isset($this->pendingMessages[$payload['reply_to']])) {
418
                    $deferred = $this->pendingMessages[$payload['reply_to']];
419
420
                    // Resolve or reject the promise that was waiting for the reply.
421
                    if (isset($payload['ok']) && $payload['ok'] === true) {
422
                        $deferred->resolve();
423
                    } else {
424
                        $deferred->reject($payload['error']);
425
                    }
426
427
                    unset($this->pendingMessages[$payload['reply_to']]);
428
                }
429
            }
430
        }
431
    }
432
}
433