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

src/BotMan.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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);
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();
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))
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
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));
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