Passed
Push — master ( 3847de...717744 )
by Korvin
02:25
created

Bot   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 20
eloc 68
dl 0
loc 236
rs 10
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A logger() 0 3 1
A getConnectedTime() 0 3 1
A run() 0 7 3
A api() 0 3 1
A stop() 0 4 1
A feignTyping() 0 24 3
A handleInterrupt() 0 4 1
A __construct() 0 8 1
A getUptime() 0 10 2
A connect() 0 22 1
A getLoop() 0 3 1
A getId() 0 3 1
A rtm() 0 3 1
A startPingLoop() 0 12 2
1
<?php
2
3
namespace PortlandLabs\Slackbot;
4
5
use Carbon\Carbon;
6
use DateTime;
7
use PortlandLabs\Slackbot\Slack\Api\Payload\RtmConnectPayloadResponse;
8
use PortlandLabs\Slackbot\Slack\Api\Client as ApiClient;
9
use PortlandLabs\Slackbot\Slack\Rtm\Client as RtmClient;
10
use PortlandLabs\Slackbot\Slack\Rtm\Event;
11
use PortlandLabs\Slackbot\Slack\Rtm\Event\Middleware\CommandMiddleware;
12
use PortlandLabs\Slackbot\Slack\Rtm\WebsocketNegotiator;
13
use Psr\Log\LoggerInterface;
14
use React\EventLoop\LoopInterface;
15
use React\EventLoop\TimerInterface;
16
use React\Promise\Deferred;
17
18
class Bot
19
{
20
21
    /** @var TimerInterface[] */
22
    protected $typing = [];
23
24
    /**
25
     * @var ApiClient
26
     */
27
    protected $apiClient;
28
29
    /**
30
     * @var RtmClient
31
     */
32
    protected $rtmClient;
33
34
    /**
35
     * @var LoggerInterface
36
     */
37
    protected $logger;
38
39
    /**
40
     * @var LoopInterface
41
     */
42
    protected $loop;
43
44
    /**
45
     * @var WebsocketNegotiator
46
     */
47
    protected $negotiator;
48
49
    /** @var Carbon */
50
    protected $connected;
51
52
    /** @var bool */
53
    protected $connecting;
54
55
    /** @var string */
56
    protected $id;
57
58
    /**
59
     * @var string[]
60
     */
61
    protected $eventMiddlewares = [
62
        CommandMiddleware::class
63
    ];
64
65
    public function __construct(ApiClient $apiClient, RtmClient $rtmClient, LoggerInterface $logger, LoopInterface $loop, WebsocketNegotiator $negotiator)
66
    {
67
        $this->apiClient = $apiClient;
68
        $this->rtmClient = $rtmClient;
69
        $this->logger = $logger;
70
        $this->loop = $loop;
71
        $this->negotiator = $negotiator;
72
        $this->id = bin2hex(random_bytes(4));
73
    }
74
75
    /**
76
     * Connect to slack
77
     *
78
     * @return \React\Promise\Promise
79
     * @throws \GuzzleHttp\Exception\GuzzleException
80
     */
81
    public function connect()
82
    {
83
        $this->connecting = true;
84
        $payload = $this->negotiator->resolveUrl($this->api(), 10);
85
        $promise = $this->rtmClient->listen($this->loop, $payload, $this->eventMiddlewares, function(Event $event) {
86
            $this->logger()->debug('[BOT.RCV] Received Event: ' . get_class($event));
87
        });
88
89
        // Record when we connected
90
        $promise->done(function($result) {
91
            /** @var RtmConnectPayloadResponse $payload */
92
            [$connection, $payload] = $result;
93
94
            $this->api()->setUsername($payload->getUserName());
95
            $this->connecting = false;
96
            $this->connected = Carbon::now();
97
98
            // Start sending Pings
99
            $this->startPingLoop();
100
        });
101
102
        return $promise;
103
    }
104
105
    /**
106
     * Run the bot
107
     * This method must be called in order to start running the bot
108
     */
109
    public function run()
110
    {
111
        if (!$this->connected && !$this->connecting) {
112
            $this->connect();
113
        }
114
115
        $this->loop->run();
116
    }
117
118
    /**
119
     * Stop a running bot
120
     */
121
    public function stop()
122
    {
123
        $this->rtm()->disconnect();
124
        $this->loop->stop();
125
    }
126
127
    /**
128
     * @return ApiClient
129
     */
130
    public function api(): ApiClient
131
    {
132
        return $this->apiClient;
133
    }
134
135
    /**
136
     * @return RtmClient
137
     */
138
    public function rtm(): RtmClient
139
    {
140
        return $this->rtmClient;
141
    }
142
143
    /**
144
     * @return LoggerInterface
145
     */
146
    public function logger(): LoggerInterface
147
    {
148
        return $this->logger;
149
    }
150
151
    /**
152
     * @return LoopInterface
153
     */
154
    public function getLoop(): LoopInterface
155
    {
156
        return $this->loop;
157
    }
158
159
    /**
160
     * @return DateTime
161
     */
162
    public function getConnectedTime(): DateTime
163
    {
164
        return $this->connected;
165
    }
166
167
    public function getId(): string
168
    {
169
        return $this->id;
170
    }
171
172
    /**
173
     * Pretend to type
174
     *
175
     * @param string $channel
176
     * @param string $message
177
     * @param float $duration
178
     *
179
     * @return \React\Promise\Promise|\React\Promise\Promise
180
     */
181
    public function feignTyping(string $channel, string $message = null, float $duration = .1)
182
    {
183
        $deferred = new Deferred();
184
        $duration = max(0, $duration);
185
186
        $this->logger->info('[<bold>BOT.TYP</bold>] Beginning to type');
187
188
        if ($duration) {
189
            $this->rtmClient->typing($channel);
190
            $this->getLoop()->addTimer($duration, function () use ($channel, $message, $deferred) {
191
                if ($message) {
192
                    $this->rtmClient
193
                        ->sendMessage($message, $channel)
194
                        ->done(function (array $result) use ($deferred) {
195
                            $this->logger->info('[<bold>BOT.TYP</bold>] Done Typing');
196
                            $deferred->resolve($result);
197
                        });
198
                } else {
199
                    $deferred->resolve();
200
                }
201
            });
202
        }
203
204
        return $deferred->promise();
205
    }
206
207
    /**
208
     * Get a string representing how long we've been up
209
     *
210
     * @param DateTime|null $now
211
     *
212
     * @return string
213
     */
214
    public function getUptime(DateTime $now = null): string
215
    {
216
        if (!$now) {
217
            $now = Carbon::now();
218
        }
219
220
        $diffString = $this->connected->diffForHumans($now, true, false, 6);
221
222
        // Return the time string with "and" between the last two segments
223
        return preg_replace('/(\d+ \D+?) (\d+ \D+?)$/', '$1 and $2', $diffString);
224
225
    }
226
227
    /**
228
     * Start sending pings over RTM to keep us alive
229
     */
230
    protected function startPingLoop()
231
    {
232
        // Track how many times pings fail
233
        $fails = 0;
234
235
        $this->getLoop()->addPeriodicTimer(10, function() use (&$fails) {
236
            $this->rtmClient->sendPing()->otherwise(function() use (&$fails) {
237
                $fails++;
238
239
                if ($fails > 3) {
240
                    $this->handleInterrupt();
241
                    $fails = 0;
242
                }
243
            });
244
        });
245
    }
246
247
    /**
248
     * Handle the RTM session getting interrupted
249
     */
250
    protected function handleInterrupt()
251
    {
252
        $this->logger->critical('-- Disconnected from RTM, Ping timeout --');
253
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
254
    }
255
256
}
257