Passed
Push — master ( 49600b...32b362 )
by Ehsan
03:58
created

Slackbot   C

Complexity

Total Complexity 49

Size/Duplication

Total Lines 386
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 95.68%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 16
dl 0
loc 386
ccs 133
cts 139
cp 0.9568
rs 6.4223
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A setTimezone() 0 5 1
A getRequest() 0 4 1
A handleMessageActions() 0 15 2
B handleSendResponse() 0 41 5
B checkAccessControl() 0 23 4
A run() 0 13 4
A handleOAuth() 0 4 1
A handleUrlVerification() 0 10 2
A preProcessRequest() 0 14 2
B respond() 0 24 4
A getPluginClassByCommand() 0 14 2
B getCommandByMessage() 0 58 8
A getCommands() 0 8 2
A setCommands() 0 4 1
A getLastError() 0 4 1
A setLastError() 0 4 1
A getCurrentCommand() 0 4 1
A setCurrentCommand() 0 4 1
A youTalkingToMe() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like Slackbot often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Slackbot, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Botonomous;
4
5
use Botonomous\listener\EventListener;
6
use Botonomous\plugin\AbstractPlugin;
7
8
/**
9
 * Class Botonomous.
10
 */
11
class Slackbot extends AbstractBot
12
{
13
    private $commands;
14
    private $lastError;
15
    private $currentCommand;
16
17
    /**
18
     * Botonomous constructor.
19
     *
20
     * @param Config|null $config
21
     *
22
     * @throws \Exception
23
     */
24 42
    public function __construct(Config $config = null)
25
    {
26 42
        if ($config !== null) {
27 6
            $this->setConfig($config);
28
        }
29
30 42
        $this->setTimezone();
31 42
    }
32
33
    /**
34
     * Set the timezone.
35
     */
36 42
    private function setTimezone()
37
    {
38
        // set timezone
39 42
        date_default_timezone_set($this->getConfig()->get('timezone'));
40 42
    }
41
42
    /**
43
     * @param null|string $key
44
     *
45
     * @return mixed
46
     */
47 10
    public function getRequest($key = null)
48
    {
49 10
        return $this->getListener()->getRequest($key);
50
    }
51
52
    /**
53
     * @return AbstractBaseSlack|null|void
54
     */
55 1
    private function handleMessageActions()
56
    {
57 1
        $post = $this->getRequestUtility()->getPost();
58
59
        // ignore if payload is not set
60 1
        if (!isset($post['payload'])) {
61
            /* @noinspection PhpInconsistentReturnPointsInspection */
62 1
            return;
63
        }
64
65
        // posted payload is in JSON
66 1
        $payload = json_decode($post['payload'], true);
67
68 1
        return (new MessageAction())->load($payload);
69
    }
70
71
    /**
72
     * @throws \Exception
73
     */
74 5
    private function handleSendResponse()
75
    {
76
        // 1. Start listening
77 5
        $this->getListener()->listen();
78
79
        // 2. verify the request
80
        try {
81 5
            $verificationResult = $this->getListener()->verifyRequest();
82
83 5
            if ($verificationResult['success'] !== true) {
84 5
                throw new \Exception($verificationResult['message']);
85
            }
86 2
        } catch (\Exception $e) {
87 2
            throw $e;
88
        }
89
90
        // 3. pre process the request
91 3
        $this->preProcessRequest();
92
93
        // 4. check access control
94 3
        if ($this->checkAccessControl() !== true) {
95 2
            return;
96
        }
97
98
        // 5. set the current command
99 1
        $message = $this->getListener()->getMessage();
100 1
        $this->setCurrentCommand($this->getMessageUtility()->extractCommandName($message));
101
102
        // 6. log the message
103 1
        $this->getLoggerUtility()->logChat(__METHOD__, $message);
104
105
        // 7. send confirmation message if is enabled
106 1
        $this->getSender()->sendConfirmation();
107
108
        // 8. And send the response to the channel, only if the response is not empty
109 1
        $response = $this->respond($message);
110
111 1
        if (!empty($response)) {
112 1
            $this->getSender()->send($response);
113
        }
114 1
    }
115
116
    /**
117
     * @return bool
118
     */
119 3
    private function checkAccessControl()
120
    {
121
        // if accessControlEnabled is not set true ignore the check and return true
122 3
        if ($this->getConfig()->get('accessControlEnabled') !== true) {
123 1
            return true;
124
        }
125
126 2
        if ($this->getBlackList()->isBlackListed() !== false) {
127
            // found in blacklist
128 1
            $this->getSender()->send($this->getDictionary()->get('generic-messages')['blacklistedMessage']);
129
130 1
            return false;
131
        }
132
133 1
        if ($this->getWhiteList()->isWhiteListed() !== true) {
134
            // not found in whitelist
135 1
            $this->getSender()->send($this->getDictionary()->get('generic-messages')['whitelistedMessage']);
136
137 1
            return false;
138
        }
139
140
        return true;
141
    }
142
143
    /**
144
     * @throws \Exception
145
     */
146 8
    public function run()
147
    {
148 8
        switch ($this->getListener()->determineAction()) {
149 8
            case 'oauth':
150 1
                return $this->handleOAuth();
151 7
            case 'message_actions':
152 1
                return $this->handleMessageActions();
153 6
            case 'url_verification':
154 1
                return $this->handleUrlVerification();
155
            default:
156 5
                return $this->handleSendResponse();
157
        }
158
    }
159
160
    /**
161
     * handle OAuth.
162
     */
163 1
    private function handleOAuth()
164
    {
165 1
        return $this->getOauth()->doOauth();
166
    }
167
168
    /**
169
     * @throws \Exception
170
     */
171 1
    private function handleUrlVerification()
172
    {
173 1
        $request = $this->getRequestUtility()->getPostedBody();
174
175 1
        if (empty($request['challenge'])) {
176
            throw new \Exception('Challenge is missing for URL verification');
177
        }
178
179 1
        echo $request['challenge'];
180 1
    }
181
182
    /**
183
     * Pre-process the request.
184
     */
185 3
    private function preProcessRequest()
186
    {
187 3
        $request = $this->getListener()->getRequest();
188
189
        // remove the trigger_word from beginning of the message
190 3
        if (!empty($request['trigger_word'])) {
191 3
            $request['text'] = $this->getMessageUtility()->removeTriggerWord(
192 3
                $request['text'],
193 3
                $request['trigger_word']
194
            );
195
196 3
            $this->getListener()->setRequest($request);
197
        }
198 3
    }
199
200
    /**
201
     * @param null $message
202
     *
203
     * @throws \Exception
204
     *
205
     * @return mixed
206
     */
207 4
    public function respond($message = null)
208
    {
209
        try {
210 4
            $command = $this->getCommandByMessage($message);
211
212 4
            if (!$command instanceof Command) {
213
                // something went wrong, error will tell us!
214 2
                return $this->getLastError();
215
            }
216
217 3
            $pluginClass = $this->getPluginClassByCommand($command);
218
219
            // check action exists
220 3
            $action = $command->getAction();
221 3
            if (!method_exists($pluginClass, $action)) {
222 1
                $className = get_class($pluginClass);
223 1
                throw new \Exception("Action / function: '{$action}' does not exist in '{$className}'");
224
            }
225
226 2
            return $pluginClass->$action();
227 1
        } catch (\Exception $e) {
228 1
            throw $e;
229
        }
230
    }
231
232
    /**
233
     * Get plugin class by command
234
     *
235
     * @param Command $command
236
     *
237
     * @return AbstractPlugin
238
     * @throws \Exception
239
     */
240 3
    private function getPluginClassByCommand(Command $command)
241
    {
242
        // create the class
243 3
        $pluginClassFile = $command->getClass();
244 3
        $pluginClass = new $pluginClassFile($this);
245
246
        // check class is valid
247 3
        if (!$pluginClass instanceof AbstractPlugin) {
248
            $className = get_class($pluginClass);
249
            throw new \Exception("Couldn't create class: '{$className}'");
250
        }
251
252 3
        return $pluginClass;
253
    }
254
255
    /**
256
     * @param null $message
257
     *
258
     * @throws \Exception
259
     *
260
     * @return bool|Command
261
     */
262 7
    public function getCommandByMessage($message = null)
263
    {
264
        // If message is not set, get it from the current request
265 7
        if ($message === null) {
266 4
            $message = $this->getListener()->getMessage();
267
        }
268
269 7
        if (empty($message)) {
270 1
            $this->setLastError('Message is empty');
271
272 1
            return false;
273
        }
274
275
        /**
276
         * Process the message.
277
         */
278 6
        $command = $this->getMessageUtility()->extractCommandName($message);
279
280 6
        $config = $this->getConfig();
281
282
        // check command name
283 6
        if (empty($command)) {
284
            // get the default command if no command is find in the message
285 2
            $command = $config->get('defaultCommand');
286
287 2
            if (empty($command)) {
288 2
                $this->setLastError($this->getDictionary()->get('generic-messages')['noCommandMessage']);
289
290 2
                return false;
291
            }
292
        }
293
294 4
        $commandObject = $this->getCommandContainer()->getAsObject($command);
295
296
        // check command details
297 4
        if (empty($commandObject)) {
298 1
            $this->setLastError(
299 1
                $this->getDictionary()->getValueByKey(
300 1
                    'generic-messages',
301 1
                    'unknownCommandMessage',
302 1
                    ['command' => $command]
303
                )
304
            );
305
306 1
            return false;
307
        }
308
309 4
        if (!$commandObject instanceof Command) {
310
            throw new \Exception('Command is not an object');
311
        }
312
313
        // check the plugin for the command
314 4
        if (empty($commandObject->getPlugin())) {
315
            throw new \Exception('Plugin is not set for this command');
316
        }
317
318 4
        return $commandObject;
319
    }
320
321
    /**
322
     * @return array
323
     */
324 2
    public function getCommands()
325
    {
326 2
        if (!isset($this->commands)) {
327 1
            $this->setCommands($this->getCommandContainer()->getAllAsObject());
328
        }
329
330 2
        return $this->commands;
331
    }
332
333
    /**
334
     * @param array $commands
335
     */
336 2
    public function setCommands(array $commands)
337
    {
338 2
        $this->commands = $commands;
339 2
    }
340
341
    /**
342
     * @return string
343
     */
344 4
    public function getLastError()
345
    {
346 4
        return $this->lastError;
347
    }
348
349
    /**
350
     * @param string $lastError
351
     */
352 4
    public function setLastError($lastError)
353
    {
354 4
        $this->lastError = $lastError;
355 4
    }
356
357
    /**
358
     * Return the current command.
359
     *
360
     * @return string
361
     */
362 1
    public function getCurrentCommand()
363
    {
364 1
        return $this->currentCommand;
365
    }
366
367
    /**
368
     * @param string $currentCommand
369
     */
370 2
    public function setCurrentCommand($currentCommand)
371
    {
372 2
        $this->currentCommand = $currentCommand;
373 2
    }
374
375
    /**
376
     * Determine if bot user id is mentioned in the message.
377
     *
378
     * @return bool
379
     */
380 1
    public function youTalkingToMe()
381
    {
382 1
        $message = $this->getListener()->getMessage();
383
384 1
        if (empty($message)) {
385 1
            return false;
386
        }
387
388 1
        if ($this->getMessageUtility()->isBotMentioned($message) === true) {
389 1
            return true;
390
        }
391
392 1
        $listener = $this->getListener();
393
        // check direct messages
394 1
        return $listener instanceof EventListener && $listener->getEvent()->isDirectMessage() === true;
395
    }
396
}
397