Completed
Push — master ( 42f289...aa45ca )
by Dan
01:42
created

Yabot::run()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 0
1
<?php
2
3
namespace Nopolabs\Yabot;
4
5
use DateTime;
6
use Exception;
7
use Nopolabs\Yabot\Helpers\ConfigTrait;
8
use Nopolabs\Yabot\Helpers\LogTrait;
9
use Nopolabs\Yabot\Helpers\LoopTrait;
10
use Nopolabs\Yabot\Helpers\SlackTrait;
11
use Nopolabs\Yabot\Message\MessageFactory;
12
use Nopolabs\Yabot\Plugin\PluginInterface;
13
use Nopolabs\Yabot\Plugin\PluginManager;
14
use Nopolabs\Yabot\Slack\Client;
15
use Psr\Log\LoggerInterface;
16
use React\EventLoop\LoopInterface;
17
use React\EventLoop\Timer\TimerInterface;
18
use React\Promise\Timer;
19
use Slack\Payload;
20
use Slack\User;
21
use Throwable;
22
23
class Yabot
24
{
25
    use LogTrait;
26
    use LoopTrait;
27
    use SlackTrait;
28
    use ConfigTrait;
29
30
    /** @var MessageFactory */
31
    private $messageFactory;
32
33
    /** @var PluginManager */
34
    private $pluginManager;
35
36
    /** @var string */
37
    private $messageLog;
38
39
    /** @var TimerInterface */
40
    private $monitor;
41
42
    /** @var bool */
43
    private $pong;
44
45
    public function __construct(
46
        LoggerInterface $logger,
47
        LoopInterface $eventLoop,
48
        Client $slackClient,
49
        MessageFactory $messageFactory,
50
        PluginManager $pluginManager,
51
        array $config = []
52
    ) {
53
        $this->setLog($logger);
54
        $this->setLoop($eventLoop);
55
        $this->setSlack($slackClient);
56
        $this->setConfig($config);
57
        $this->messageFactory = $messageFactory;
58
        $this->pluginManager = $pluginManager;
59
        $this->messageLog = null;
60
    }
61
62
    public function getMessageLog()
63
    {
64
        return $this->messageLog;
65
    }
66
67
    public function setMessageLog(string $messageLog = null)
68
    {
69
        $this->messageLog = $messageLog ?? null;
70
    }
71
72
    public function init(array $plugins)
73
    {
74
        foreach ($plugins as $pluginId => $plugin) {
75
            /** @var PluginInterface $plugin */
76
77
            $this->info("loading $pluginId");
78
79
            try {
80
                $this->pluginManager->loadPlugin($pluginId, $plugin);
81
            } catch (Exception $e) {
82
                $this->warning("Unhandled Exception while loading $pluginId: ".$e->getMessage());
83
                $this->warning($e->getTraceAsString());
84
            }
85
        }
86
    }
87
88
    public function run()
89
    {
90
        $this->getSlack()->init();
91
92
        $this->getLog()->info('Connecting...');
93
94
        $this->connect();
95
96
        $this->addMemoryReporting();
97
98
        $this->getLoop()->run();
99
    }
100
101
    public function shutDown()
102
    {
103
        $this->getLog()->error('Shutting down...');
104
105
        $this->getSlack()->disconnect();
106
        $this->getLoop()->stop();
107
    }
108
109
    public function reconnect()
110
    {
111
        $this->getLog()->error('Reconnecting...');
112
113
        if ($this->monitor) {
114
            $this->loop->cancelTimer($this->monitor);
115
        }
116
117
        $this->getSlack()->reconnect()->then(
118
            function () {
119
                $this->getLog()->info('Reconnected');
120
                $this->monitor = $this->startConnectionMonitor();
121
            },
122
            function () {
123
                $this->getLog()->error('Reconnect failed, shutting down.');
124
                $this->shutDown();
125
            }
126
        );
127
    }
128
129
    public function connected()
130
    {
131
        $slack = $this->getSlack();
132
133
        $slack->update(function(User $authedUser) {
134
            $this->pluginManager->setAuthedUser($authedUser);
135
        });
136
137
        $slack->onEvent('message', [$this, 'onMessage']);
138
        $slack->onEvent('team_join', [$this, 'onTeamJoin']);
139
140
        $this->monitor = $this->startConnectionMonitor();
141
    }
142
143
    public function onMessage(Payload $payload)
144
    {
145
        $data = $payload->getData();
146
147
        $this->debug('Received message', $data);
148
149
        try {
150
            $this->logMessage($data);
151
            $message = $this->messageFactory->create($data);
152
        } catch (Throwable $throwable) {
153
            $errmsg = $throwable->getMessage()."\n"
154
                .$throwable->getTraceAsString()."\n"
155
                ."Payload data: ".json_encode($data);
156
            $this->warning($errmsg);
157
            return;
158
        }
159
160
        if ($message->isSelf()) {
161
            return;
162
        }
163
164
        $this->pluginManager->dispatchMessage($message);
165
    }
166
167
    public function onTeamJoin(Payload $payload)
0 ignored issues
show
Unused Code introduced by
The parameter $payload is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
168
    {
169
        $this->getSlack()->updateUsers();
170
    }
171
172
    public function getHelp() : string
173
    {
174
        return implode("\n", $this->pluginManager->getHelp());
175
    }
176
177
    public function getStatus() : string
178
    {
179
        $statuses = $this->pluginManager->getStatuses();
180
181
        array_unshift($statuses, $this->getFormattedMemoryUsage());
182
183
        return implode("\n", $statuses);
184
    }
185
186
    protected function addMemoryReporting()
187
    {
188
        $now = new DateTime();
189
        $then = new DateTime('+1 hour');
190
        $then->setTime($then->format('H'), 0, 0);
191
        $delay = $then->getTimestamp() - $now->getTimestamp();
192
193
        $this->addTimer($delay, function() {
194
            $this->info($this->getFormattedMemoryUsage());
195
            $this->addPeriodicTimer(3600, function() {
196
                $this->info($this->getFormattedMemoryUsage());
197
            });
198
        });
199
    }
200
201
    protected function getFormattedMemoryUsage() : string
202
    {
203
        $memory = memory_get_usage() / 1024;
204
        $formatted = number_format($memory, 3).'K';
205
        return "Current memory usage: {$formatted}";
206
    }
207
208
    protected function logMessage($data)
209
    {
210
        if ($this->messageLog !== null) {
211
            file_put_contents($this->messageLog, json_encode($data) . "\n", FILE_APPEND);
212
        }
213
    }
214
215
    /**
216
     * @return TimerInterface|null
217
     */
218
    protected function startConnectionMonitor()
219
    {
220
        if ($interval = $this->get('connection_monitor.interval')) {
221
222
            $this->getLog()->info("Monitoring websocket connection every $interval seconds.");
223
            $this->notify("Monitoring websocket connection every $interval seconds.");
224
225
            $this->ping();
226
227
            return $this->loop->addPeriodicTimer($interval, function () {
228
                $this->checkPong();
229
                $this->ping();
230
            });
231
        }
232
    }
233
234
    protected function checkPong()
235
    {
236
        if (!$this->pong) {
237
            $this->getLog()->error('No pong.');
238
239
            $failureStrategy = $this->get('connection_monitor.failure_strategy', 'reconnect');
240
241
            if ($failureStrategy === 'reconnect') {
242
                $this->reconnect();
243
                return;
244
            }
245
246
            if ($failureStrategy !== 'shutdown') {
247
                $this->getLog()->error("Unknown connection_monitor.failure_strategy '$failureStrategy'");
248
            }
249
250
            $this->shutDown();
251
        }
252
    }
253
254
    protected function ping()
255
    {
256
        $this->pong = false;
257
258
        $this->getSlack()->ping()
259
            ->then(
260
                function (Payload $payload) {
261
                    $this->getLog()->info($payload->toJson());
262
                    $this->pong = true;
263
                }
264
            );
265
    }
266
267
    protected function notify(string $message)
268
    {
269
        if ($user = $this->get('notify.user')) {
270
            $this->getSlack()->directMessage($message, $user);
271
        }
272
    }
273
274
    protected function connect()
275
    {
276
        Timer\timeout($this->getSlack()->connect(), 30, $this->getLoop())
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface React\Promise\PromiseInterface as the method otherwise() does only exist in the following implementations of said interface: React\Promise\FulfilledPromise, React\Promise\LazyPromise, React\Promise\Promise, React\Promise\RejectedPromise.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
277
            ->then(function () {
278
                $this->getLog()->info('Connected.');
279
                $this->connected();
280
            })
281
            ->otherwise(function (Timer\TimeoutException $error) {
282
                $this->getLog()->error($error->getMessage());
283
                $this->getLog()->error('Connection failed, shutting down.');
284
                $this->shutDown();
285
            })
286
            ->otherwise(function ($error) {
0 ignored issues
show
Unused Code introduced by
The parameter $error is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
287
                $this->getLog()->error('Connection failed, shutting down.');
288
                $this->shutDown();
289
            });
290
    }
291
}
292