Completed
Push — master ( cc24d0...5b3757 )
by Marcel
08:43
created

BotMan::say()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 16
nc 13
nop 4
1
<?php
2
3
namespace BotMan\BotMan;
4
5
use Closure;
6
use Illuminate\Support\Collection;
7
use BotMan\BotMan\Commands\Command;
8
use BotMan\BotMan\Messages\Matcher;
9
use BotMan\BotMan\Drivers\DriverManager;
10
use BotMan\BotMan\Traits\ProvidesStorage;
11
use BotMan\BotMan\Interfaces\UserInterface;
12
use BotMan\BotMan\Messages\Incoming\Answer;
13
use BotMan\BotMan\Traits\HandlesExceptions;
14
use BotMan\BotMan\Handlers\ExceptionHandler;
15
use BotMan\BotMan\Interfaces\CacheInterface;
16
use BotMan\BotMan\Messages\Attachments\File;
17
use BotMan\BotMan\Interfaces\DriverInterface;
18
use BotMan\BotMan\Messages\Attachments\Audio;
19
use BotMan\BotMan\Messages\Attachments\Image;
20
use BotMan\BotMan\Messages\Attachments\Video;
21
use BotMan\BotMan\Messages\Outgoing\Question;
22
use BotMan\BotMan\Interfaces\StorageInterface;
23
use BotMan\BotMan\Traits\HandlesConversations;
24
use Symfony\Component\HttpFoundation\Response;
25
use BotMan\BotMan\Commands\ConversationManager;
26
use BotMan\BotMan\Middleware\MiddlewareManager;
27
use BotMan\BotMan\Messages\Attachments\Location;
28
use BotMan\BotMan\Exceptions\Base\BotManException;
29
use BotMan\BotMan\Interfaces\DriverEventInterface;
30
use BotMan\BotMan\Messages\Incoming\IncomingMessage;
31
use BotMan\BotMan\Messages\Outgoing\OutgoingMessage;
32
use BotMan\BotMan\Interfaces\ExceptionHandlerInterface;
33
use BotMan\BotMan\Exceptions\Core\BadMethodCallException;
34
use BotMan\BotMan\Exceptions\Core\UnexpectedValueException;
35
use BotMan\BotMan\Messages\Conversations\InlineConversation;
36
37
/**
38
 * Class BotMan.
39
 */
40
class BotMan
41
{
42
    use ProvidesStorage,
43
        HandlesConversations,
44
        HandlesExceptions;
45
46
    /** @var \Illuminate\Support\Collection */
47
    protected $event;
48
49
    /** @var Command */
50
    protected $command;
51
52
    /** @var IncomingMessage */
53
    protected $message;
54
55
    /** @var OutgoingMessage|Question */
56
    protected $outgoingMessage;
57
58
    /** @var string */
59
    protected $driverName;
60
61
    /** @var array|null */
62
    protected $currentConversationData;
63
64
    /** @var ExceptionHandlerInterface */
65
    protected $exceptionHandler;
66
67
    /**
68
     * IncomingMessage service events.
69
     * @var array
70
     */
71
    protected $events = [];
72
73
    /**
74
     * The fallback message to use, if no match
75
     * could be heard.
76
     * @var callable|null
77
     */
78
    protected $fallbackMessage;
79
80
    /** @var array */
81
    protected $groupAttributes = [];
82
83
    /** @var array */
84
    protected $matches = [];
85
86
    /** @var DriverInterface */
87
    protected $driver;
88
89
    /** @var array */
90
    protected $config = [];
91
92
    /** @var MiddlewareManager */
93
    public $middleware;
94
95
    /** @var ConversationManager */
96
    protected $conversationManager;
97
98
    /** @var CacheInterface */
99
    private $cache;
100
101
    /** @var StorageInterface */
102
    protected $storage;
103
104
    /** @var Matcher */
105
    protected $matcher;
106
107
    /** @var bool */
108
    protected $loadedConversation = false;
109
110
    /** @var bool */
111
    protected $firedDriverEvents = false;
112
113
    /** @var bool */
114
    protected $runsOnSocket = false;
115
116
    /**
117
     * BotMan constructor.
118
     * @param CacheInterface $cache
119
     * @param DriverInterface $driver
120
     * @param array $config
121
     * @param StorageInterface $storage
122
     */
123
    public function __construct(CacheInterface $cache, DriverInterface $driver, $config, StorageInterface $storage)
124
    {
125
        $this->cache = $cache;
126
        $this->message = new IncomingMessage('', '', '');
127
        $this->driver = $driver;
128
        $this->config = $config;
129
        $this->storage = $storage;
130
        $this->matcher = new Matcher();
131
        $this->middleware = new MiddlewareManager($this);
132
        $this->conversationManager = new ConversationManager();
133
        $this->exceptionHandler = new ExceptionHandler();
134
    }
135
136
    /**
137
     * Set a fallback message to use if no listener matches.
138
     *
139
     * @param callable $callback
140
     */
141
    public function fallback($callback)
142
    {
143
        $this->fallbackMessage = $callback;
144
    }
145
146
    /**
147
     * @param string $name The Driver name or class
148
     */
149
    public function loadDriver($name)
150
    {
151
        $this->driver = DriverManager::loadFromName($name, $this->config);
152
    }
153
154
    /**
155
     * @param DriverInterface $driver
156
     */
157
    public function setDriver(DriverInterface $driver)
158
    {
159
        $this->driver = $driver;
160
    }
161
162
    /**
163
     * @return DriverInterface
164
     */
165
    public function getDriver()
166
    {
167
        return $this->driver;
168
    }
169
170
    /**
171
     * Retrieve the chat message.
172
     *
173
     * @return array
174
     */
175
    public function getMessages()
176
    {
177
        return $this->getDriver()->getMessages();
178
    }
179
180
    /**
181
     * Retrieve the chat message that are sent from bots.
182
     *
183
     * @return array
184
     */
185
    public function getBotMessages()
186
    {
187
        return Collection::make($this->getDriver()->getMessages())->filter(function (IncomingMessage $message) {
188
            return $message->isFromBot();
189
        })->toArray();
190
    }
191
192
    /**
193
     * @return Answer
194
     */
195
    public function getConversationAnswer()
196
    {
197
        return $this->getDriver()->getConversationAnswer($this->message);
198
    }
199
200
    /**
201
     * @param bool $running
202
     * @return bool
203
     */
204
    public function runsOnSocket($running = null)
205
    {
206
        if (is_bool($running)) {
207
            $this->runsOnSocket = $running;
208
        }
209
210
        return $this->runsOnSocket;
211
    }
212
213
    /**
214
     * @return UserInterface
215
     */
216
    public function getUser()
217
    {
218
        if ($user = $this->cache->get('user_'.$this->driver->getName().'_'.$this->getMessage()->getSender())) {
219
            return $user;
220
        }
221
222
        $user = $this->getDriver()->getUser($this->getMessage());
223
        $this->cache->put('user_'.$this->driver->getName().'_'.$user->getId(), $user, $this->config['user_cache_time'] ?? 30);
224
225
        return $user;
226
    }
227
228
    /**
229
     * Get the parameter names for the route.
230
     *
231
     * @param $value
232
     * @return array
233
     */
234
    protected function compileParameterNames($value)
235
    {
236
        preg_match_all(Matcher::PARAM_NAME_REGEX, $value, $matches);
237
238
        return array_map(function ($m) {
239
            return trim($m, '?');
240
        }, $matches[1]);
241
    }
242
243
    /**
244
     * @param string $pattern the pattern to listen for
245
     * @param Closure|string $callback the callback to execute. Either a closure or a Class@method notation
246
     * @param string $in the channel type to listen to (either direct message or public channel)
247
     * @return Command
248
     */
249
    public function hears($pattern, $callback, $in = null)
250
    {
251
        $command = new Command($pattern, $callback, $in);
0 ignored issues
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 249 can also be of type object<Closure>; however, BotMan\BotMan\Commands\Command::__construct() does only seem to accept object<BotMan\BotMan\Closure>|string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
252
        $command->applyGroupAttributes($this->groupAttributes);
253
254
        $this->conversationManager->listenTo($command);
255
256
        return $command;
257
    }
258
259
    /**
260
     * Listen for messaging service events.
261
     *
262
     * @param string $name
263
     * @param Closure|string $callback
264
     */
265
    public function on($name, $callback)
266
    {
267
        $this->events[] = [
268
            'name' => $name,
269
            'callback' => $this->getCallable($callback),
270
        ];
271
    }
272
273
    /**
274
     * Listening for image files.
275
     *
276
     * @param $callback
277
     * @return Command
278
     */
279
    public function receivesImages($callback)
280
    {
281
        return $this->hears(Image::PATTERN, $callback);
282
    }
283
284
    /**
285
     * Listening for image files.
286
     *
287
     * @param $callback
288
     * @return Command
289
     */
290
    public function receivesVideos($callback)
291
    {
292
        return $this->hears(Video::PATTERN, $callback);
293
    }
294
295
    /**
296
     * Listening for audio files.
297
     *
298
     * @param $callback
299
     * @return Command
300
     */
301
    public function receivesAudio($callback)
302
    {
303
        return $this->hears(Audio::PATTERN, $callback);
304
    }
305
306
    /**
307
     * Listening for location attachment.
308
     *
309
     * @param $callback
310
     * @return Command
311
     */
312
    public function receivesLocation($callback)
313
    {
314
        return $this->hears(Location::PATTERN, $callback);
315
    }
316
317
    /**
318
     * Listening for files attachment.
319
     *
320
     * @param $callback
321
     * @return Command
322
     */
323
    public function receivesFiles($callback)
324
    {
325
        return $this->hears(File::PATTERN, $callback);
326
    }
327
328
    /**
329
     * Create a command group with shared attributes.
330
     *
331
     * @param  array $attributes
332
     * @param  \Closure $callback
333
     */
334
    public function group(array $attributes, Closure $callback)
335
    {
336
        $previousGroupAttributes = $this->groupAttributes;
337
        $this->groupAttributes = array_merge_recursive($previousGroupAttributes, $attributes);
338
339
        call_user_func($callback, $this);
340
341
        $this->groupAttributes = $previousGroupAttributes;
342
    }
343
344
    /**
345
     * Fire potential driver event callbacks.
346
     */
347
    protected function fireDriverEvents()
348
    {
349
        $driverEvent = $this->getDriver()->hasMatchingEvent();
350
        if ($driverEvent instanceof DriverEventInterface) {
351
            $this->firedDriverEvents = true;
352
353
            Collection::make($this->events)->filter(function ($event) use ($driverEvent) {
354
                return $driverEvent->getName() === $event['name'];
355
            })->each(function ($event) use ($driverEvent) {
356
                /**
357
                 * Load the message, so driver events can reply.
358
                 */
359
                $messages = $this->getDriver()->getMessages();
360
                if (isset($messages[0])) {
361
                    $this->message = $messages[0];
362
                }
363
364
                call_user_func_array($event['callback'], [$driverEvent->getPayload(), $this]);
365
            });
366
        }
367
    }
368
369
    /**
370
     * Try to match messages with the ones we should
371
     * listen to.
372
     */
373
    public function listen()
374
    {
375
        try {
376
            $this->verifyServices();
377
378
            $this->fireDriverEvents();
379
380
            if ($this->firedDriverEvents === false) {
381
                $this->loadActiveConversation();
382
383
                if ($this->loadedConversation === false) {
384
                    $this->callMatchingMessages();
385
                }
386
387
                /*
388
                 * If the driver has a  "messagesHandled" method, call it.
389
                 * This method can be used to trigger driver methods
390
                 * once the messages are handles.
391
                 */
392
                if (method_exists($this->getDriver(), 'messagesHandled')) {
393
                    $this->getDriver()->messagesHandled();
0 ignored issues
show
Bug introduced by
The method messagesHandled() does not seem to exist on object<BotMan\BotMan\Interfaces\DriverInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
394
                }
395
            }
396
397
            $this->firedDriverEvents = false;
398
        } catch (\Throwable $e) {
399
            $this->exceptionHandler->handleException($e, $this);
400
        }
401
    }
402
403
    /**
404
     * Call matching message callbacks.
405
     */
406
    protected function callMatchingMessages()
407
    {
408
        $matchingMessages = $this->conversationManager->getMatchingMessages($this->getMessages(), $this->middleware, $this->getConversationAnswer(), $this->getDriver());
409
410
        foreach ($matchingMessages as $matchingMessage) {
411
            $this->command = $matchingMessage->getCommand();
412
            $callback = $this->command->getCallback();
413
414
            $callback = $this->getCallable($callback);
415
416
            // Set the message first, so it's available for middlewares
417
            $this->message = $matchingMessage->getMessage();
418
419
            $this->message = $this->middleware->applyMiddleware('heard', $matchingMessage->getMessage(), $this->command->getMiddleware());
420
            $parameterNames = $this->compileParameterNames($this->command->getPattern());
421
422
            $parameters = $matchingMessage->getMatches();
423
            if (count($parameterNames) !== count($parameters)) {
424
                $parameters = array_merge(
425
                    //First, all named parameters (eg. function ($a, $b, $c))
0 ignored issues
show
Unused Code Comprehensibility introduced by
44% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
426
                    array_filter(
427
                        $parameters,
428
                        'is_string',
429
                        ARRAY_FILTER_USE_KEY
430
                    ),
431
                    //Then, all other unsorted parameters (regex non named results)
432
                    array_filter(
433
                        $parameters,
434
                        'is_integer',
435
                        ARRAY_FILTER_USE_KEY
436
                    )
437
                );
438
            }
439
440
            $this->matches = $parameters;
441
            array_unshift($parameters, $this);
442
443
            $parameters = $this->conversationManager->addDataParameters($this->message, $parameters);
444
445
            call_user_func_array($callback, $parameters);
446
        }
447
448
        if (empty($matchingMessages) && empty($this->getBotMessages()) && ! is_null($this->fallbackMessage)) {
449
            $this->callFallbackMessage();
450
        }
451
    }
452
453
    /**
454
     * Call the fallback method.
455
     */
456
    protected function callFallbackMessage()
457
    {
458
        $this->message = $this->getMessages()[0];
459
460
        $this->fallbackMessage = $this->getCallable($this->fallbackMessage);
461
462
        call_user_func($this->fallbackMessage, $this);
463
    }
464
465
    /**
466
     * Verify service webhook URLs.
467
     *
468
     * @return null|Response
469
     */
470
    protected function verifyServices()
471
    {
472
        return DriverManager::verifyServices($this->config);
473
    }
474
475
    /**
476
     * @param string|Question $message
477
     * @param string|array $recipients
478
     * @param DriverInterface|null $driver
479
     * @param array $additionalParameters
480
     * @return Response
481
     * @throws BotManException
482
     */
483
    public function say($message, $recipients, $driver = null, $additionalParameters = [])
484
    {
485
        if ($driver === null && $this->driver === null) {
486
            throw new BotManException('The current driver can\'t be NULL');
487
        }
488
489
        $previousDriver = $this->driver;
490
        $previousMessage = $this->message;
491
492
        if ($driver instanceof DriverInterface) {
493
            $this->setDriver($driver);
494
        } elseif (is_string($driver)) {
495
            $this->setDriver(DriverManager::loadFromName($driver, $this->config));
496
        }
497
498
        $recipients = is_array($recipients) ? $recipients : [$recipients];
499
500
        foreach ($recipients as $recipient) {
501
            $this->message = new IncomingMessage('', $recipient, '');
502
            $response = $this->reply($message, $additionalParameters);
503
        }
504
505
        $this->message = $previousMessage;
506
        $this->driver = $previousDriver;
507
508
        return $response;
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
509
    }
510
511
    /**
512
     * @param string|Question $question
513
     * @param array|Closure $next
514
     * @param array $additionalParameters
515
     * @param null|string $recipient
516
     * @param null|string $driver
517
     * @return Response
518
     */
519
    public function ask($question, $next, $additionalParameters = [], $recipient = null, $driver = null)
520
    {
521
        if (! is_null($recipient) && ! is_null($driver)) {
522
            if (is_string($driver)) {
523
                $driver = DriverManager::loadFromName($driver, $this->config);
524
            }
525
            $this->message = new IncomingMessage('', $recipient, '');
526
            $this->setDriver($driver);
527
        }
528
529
        $response = $this->reply($question, $additionalParameters);
530
        $this->storeConversation(new InlineConversation, $next, $question, $additionalParameters);
531
532
        return $response;
533
    }
534
535
    /**
536
     * @return $this
537
     */
538
    public function types()
539
    {
540
        $this->getDriver()->types($this->message);
541
542
        return $this;
543
    }
544
545
    /**
546
     * @param int $seconds Number of seconds to wait
547
     * @return $this
548
     */
549
    public function typesAndWaits($seconds)
550
    {
551
        $this->getDriver()->types($this->message);
552
        sleep($seconds);
553
554
        return $this;
555
    }
556
557
    /**
558
     * Low-level method to perform driver specific API requests.
559
     *
560
     * @param string $endpoint
561
     * @param array $additionalParameters
562
     * @return $this
563
     * @throws BadMethodCallException
564
     */
565
    public function sendRequest($endpoint, $additionalParameters = [])
566
    {
567
        $driver = $this->getDriver();
568
        if (method_exists($driver, 'sendRequest')) {
569
            return $driver->sendRequest($endpoint, $additionalParameters, $this->message);
570
        } else {
571
            throw new BadMethodCallException('The driver '.$this->getDriver()->getName().' does not support low level requests.');
572
        }
573
    }
574
575
    /**
576
     * @param string|Question $message
577
     * @param array $additionalParameters
578
     * @return mixed
579
     */
580
    public function reply($message, $additionalParameters = [])
581
    {
582
        $this->outgoingMessage = is_string($message) ? OutgoingMessage::create($message) : $message;
583
584
        return $this->sendPayload($this->getDriver()->buildServicePayload($this->outgoingMessage, $this->message, $additionalParameters));
0 ignored issues
show
Bug introduced by
It seems like $this->outgoingMessage can also be of type object<BotMan\BotMan\Mes...tgoing\OutgoingMessage>; however, BotMan\BotMan\Interfaces...::buildServicePayload() does only seem to accept string|object<BotMan\Bot...ages\Outgoing\Question>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
585
    }
586
587
    /**
588
     * @param $payload
589
     * @return mixed
590
     */
591
    public function sendPayload($payload)
592
    {
593
        return $this->middleware->applyMiddleware('sending', $payload, [], function ($payload) {
594
            $this->outgoingMessage = null;
595
596
            return $this->getDriver()->sendPayload($payload);
597
        });
598
    }
599
600
    /**
601
     * Return a random message.
602
     * @param array $messages
603
     * @return $this
604
     */
605
    public function randomReply(array $messages)
606
    {
607
        return $this->reply($messages[array_rand($messages)]);
608
    }
609
610
    /**
611
     * Make an action for an invokable controller.
612
     *
613
     * @param string $action
614
     * @return string
615
     * @throws UnexpectedValueException
616
     */
617
    protected function makeInvokableAction($action)
618
    {
619
        if (! method_exists($action, '__invoke')) {
620
            throw new UnexpectedValueException(sprintf(
621
                'Invalid hears action: [%s]', $action
622
            ));
623
        }
624
625
        return $action.'@__invoke';
626
    }
627
628
    /**
629
     * @param $callback
630
     * @return array|string|Closure
631
     * @throws UnexpectedValueException
632
     */
633
    protected function getCallable($callback)
634
    {
635
        if ($callback instanceof Closure) {
636
            return $callback;
637
        }
638
639
        if (is_array($callback)) {
640
            return $callback;
641
        }
642
643
        if (strpos($callback, '@') === false) {
644
            $callback = $this->makeInvokableAction($callback);
645
        }
646
647
        list($class, $method) = explode('@', $callback);
648
649
        return [new $class($this), $method];
650
    }
651
652
    /**
653
     * @return array
654
     */
655
    public function getMatches()
656
    {
657
        return $this->matches;
658
    }
659
660
    /**
661
     * @return IncomingMessage
662
     */
663
    public function getMessage()
664
    {
665
        return $this->message;
666
    }
667
668
    /**
669
     * @return OutgoingMessage|Question
670
     */
671
    public function getOutgoingMessage()
672
    {
673
        return $this->outgoingMessage;
674
    }
675
676
    /**
677
     * @param string $name
678
     * @param array $arguments
679
     * @return mixed
680
     * @throws BadMethodCallException
681
     */
682
    public function __call($name, $arguments)
683
    {
684
        if (method_exists($this->getDriver(), $name)) {
685
            // Add the current message to the passed arguments
686
            $arguments[] = $this->getMessage();
687
            $arguments[] = $this;
688
689
            return call_user_func_array([$this->getDriver(), $name], $arguments);
690
        }
691
692
        throw new BadMethodCallException('Method ['.$name.'] does not exist.');
693
    }
694
695
    /**
696
     * Load driver on wakeup.
697
     */
698
    public function __wakeup()
699
    {
700
        $this->driver = DriverManager::loadFromName($this->driverName, $this->config);
701
    }
702
703
    /**
704
     * @return array
705
     */
706
    public function __sleep()
707
    {
708
        $this->driverName = $this->driver->getName();
709
710
        return [
711
            'event',
712
            'exceptionHandler',
713
            'driverName',
714
            'storage',
715
            'message',
716
            'cache',
717
            'matches',
718
            'matcher',
719
            'config',
720
            'middleware',
721
        ];
722
    }
723
}
724