Passed
Push — master ( f4586f...06e79d )
by Charlotte
02:31
created

Context::finalize()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 16
nc 20
nop 1
dl 0
loc 29
rs 8.0555
c 0
b 0
f 0
1
<?php
2
/**
3
 * Livia
4
 * Copyright 2017-2019 Charlotte Dunois, All Rights Reserved
5
 *
6
 * Website: https://charuru.moe
7
 * License: https://github.com/CharlotteDunois/Livia/blob/master/LICENSE
8
*/
9
10
namespace CharlotteDunois\Livia\Commands;
11
12
/**
13
 * Represents a command invocation context.
14
 *
15
 * @property \CharlotteDunois\Livia\LiviaClient            $client          The client which initiated the instance.
16
 * @property \CharlotteDunois\Yasmin\Models\Message        $message         The message that triggers the command.
17
 * @property \CharlotteDunois\Livia\Commands\Command|null  $command         The command that got triggered, if any.
18
 *
19
 * @property string|null                                   $argString       Argument string for the command.
20
 * @property string[]|null                                 $patternMatches  Pattern matches (if from a pattern trigger).
21
 */
22
class Context extends \CharlotteDunois\Yasmin\Models\ClientBase {
23
    /**
24
     * The client which initiated the instance.
25
     * @var \CharlotteDunois\Livia\LiviaClient
26
     */
27
    protected $client;
28
    
29
    /**
30
     * The message that triggers the command.
31
     * @var \CharlotteDunois\Yasmin\Models\Message
32
     */
33
    protected $message;
34
    
35
    /**
36
     * The command that got triggered, if any.
37
     * @var \CharlotteDunois\Livia\Commands\Command|null
38
     */
39
    protected $command;
40
    
41
    /**
42
     * Argument string for the command.
43
     * @var string|null
44
     */
45
    protected $argString;
46
    
47
    /**
48
     * Pattern matches (if from a pattern trigger).
49
     * @var string[]|null
50
     */
51
    protected $patternMatches;
52
    
53
    /**
54
     * Command responses, as multidimensional array (channelID|dm => Message[]).
55
     * @var array
56
     */
57
    protected $responses = array();
58
    
59
    /**
60
     * Internal command for serialization.
61
     * @var string|null
62
     */
63
    protected $internalCommand;
64
    
65
    /**
66
     * @internal
67
     */
68
    function __construct(\CharlotteDunois\Livia\LiviaClient $client, \CharlotteDunois\Yasmin\Models\Message $message, \CharlotteDunois\Livia\Commands\Command $command = null, string $argString = null, array $patternMatches = null) {
69
        $this->client = $client;
70
        $this->message = $message;
71
        $this->command = $command;
72
        
73
        $this->argString = ($argString !== null ? \trim($argString) : null);
74
        $this->patternMatches = $patternMatches;
75
    }
76
    
77
    /**
78
     * @param string  $name
79
     * @return bool
80
     * @throws \Exception
81
     * @internal
82
     */
83
    function __isset($name) {
84
        try {
85
            return $this->$name !== null;
86
        } catch (\RuntimeException $e) {
87
            if($e->getTrace()[0]['function'] === '__get') {
88
                return false;
89
            }
90
            
91
            throw $e;
92
        }
93
    }
94
    
95
    /**
96
     * @param string  $name
97
     * @return mixed
98
     * @throws \RuntimeException
99
     * @internal
100
     */
101
    function __get($name) {
102
        if(\property_exists($this, $name)) {
103
            return $this->$name;
104
        }
105
        
106
        return $this->message->$name;
107
    }
108
    
109
    /**
110
     * @return mixed
111
     * @throws \RuntimeException
112
     * @internal
113
     */
114
    function __call($name, $args) {
115
        if(\method_exists($this->message, $name)) {
116
            return $this->message->$name(...$args);
117
        }
118
        
119
        throw new \RuntimeException('Unknown method '.\get_class($this).'::'.$name);
120
    }
121
    
122
    /**
123
     * @return string
124
     * @internal
125
     */
126
    function serialize() {
127
        $cmd = $this->command;
128
        $this->command = null;
129
        
130
        if($cmd !== null) {
131
            $this->internalCommand = $cmd->groupID.':'.$cmd->name;
132
        }
133
        
134
        $str = parent::serialize();
135
        $this->command = $cmd;
136
        
137
        return $str;
138
    }
139
    
140
    /**
141
     * @return void
142
     * @internal
143
     */
144
    function unserialize($data) {
145
        if(self::$serializeClient === null) {
146
            throw new \Exception('Unable to unserialize a class without ClientBase::$serializeClient being set');
147
        }
148
        
149
        parent::unserialize($data);
150
        
151
        /** @var \CharlotteDunois\Livia\LiviaClient  $this->client */
152
        $this->client = self::$serializeClient;
153
        
154
        if($this->internalCommand !== null) {
155
            $this->command = $this->client->registry->resolveCommand($this->internalCommand);
156
            $this->internalCommand = null;
157
        }
158
    }
159
    
160
    /**
161
     * Parses the argString into usable arguments, based on the argsType and argsCount of the command.
162
     * @return string|string[]
163
     * @throws \LogicException
164
     * @throws \RangeException
165
     */
166
    function parseCommandArgs() {
167
        if($this->command === null) {
168
            throw new \LogicException('This message has no command');
169
        }
170
        
171
        switch($this->command->argsType) {
172
            case 'single':
173
                $args = $this->argString;
174
                return \preg_replace(($this->command->argsSingleQuotes ? '/^("|\')(.*)\1$/u' : '/^(")(.*)"$/u'), '$2', $args);
175
            case 'multiple':
176
                return self::parseArgs($this->argString, $this->command->argsCount, $this->command->argsSingleQuotes);
177
            default:
178
                throw new \RangeException('Unknown argsType "'.$this->command->argsType.'".');
179
        }
180
    }
181
    
182
    /**
183
     * Runs the command. Resolves with an instance of Message or an array of Message instances.
184
     * @return \React\Promise\ExtendedPromiseInterface
185
     * @throws \LogicException
186
     */
187
    function run() {
188
        if($this->command === null) {
189
            throw new \LogicException('This message has no command');
190
        }
191
        
192
        return (new \React\Promise\Promise(function (callable $resolve, callable $reject) {
193
            $promises = array();
194
            
195
            // Obtain the member if we don't have it
196
            if($this->message->guild !== null && !$this->message->guild->members->has($this->message->author->id) && $this->message->webhookID === null) {
197
                $promises[] = $this->message->guild->fetchMember($this->message->author->id);
198
            }
199
            
200
            // Obtain the member for the client user if we don't have it
201
            if($this->message->guild !== null && $this->message->guild->me === null) {
202
                $promises[] = $this->message->guild->fetchMember($this->client->user->id);
203
            }
204
            
205
            if($this->command->guildOnly && $this->message->guild === null) {
206
                $this->client->emit('commandBlocked', $this, 'guildOnly');
207
                return $this->client->dispatcher->throttleNegativeResponseMessage($this, 'The `'.$this->command->name.'` command must be used in a server channel.', $resolve, $reject);
208
            }
209
            
210
            if($this->command->nsfw && !($this->message->channel->nsfw ?? true)) {
211
                $this->client->emit('commandBlocked', $this, 'nsfw');
212
                return $this->client->dispatcher->throttleNegativeResponseMessage($this, 'The `'.$this->command->name.'` command must be used in NSFW channels.', $resolve, $reject);
213
            }
214
            
215
            $perms = $this->command->hasPermission($this);
216
            if($perms === false || \is_string($perms)) {
217
                $this->client->emit('commandBlocked', $this, 'permission');
218
                
219
                if($this->patternMatches !== null && !((bool) $this->client->getOption('commandBlockedMessagePattern', true))) {
220
                    return $resolve();
221
                }
222
                
223
                if($perms === false) {
224
                    $perms = 'You do not have permission to use the `'.$this->command->name.'` command.';
225
                }
226
                
227
                return $this->client->dispatcher->throttleNegativeResponseMessage($this, $perms, $resolve, $reject);
228
            }
229
            
230
            // Ensure the client user has the required permissions
231
            if($this->message->channel->guild !== null && !empty($this->command->clientPermissions)) {
232
                $perms = $this->message->channel->permissionsFor($this->message->guild->me);
233
                
234
                $missing = array();
235
                foreach($this->command->clientPermissions as $perm) {
236
                    if($perms->missing($perm)) {
237
                        $missing[] = $perm;
238
                    }
239
                }
240
                
241
                if(\count($missing) > 0) {
242
                    $this->client->emit('commandBlocked', $this, 'clientPermissions');
243
                    
244
                    if($this->patternMatches !== null && !((bool) $this->client->getOption('commandBlockedMessagePattern', true))) {
245
                        return $resolve();
246
                    }
247
                    
248
                    if(\count($missing) === 1) {
249
                        $msg = 'I need the permissions `'.$missing[0].'` permission for the `'.$this->command->name.'` command to work.';
250
                    } else {
251
                        $missing = \implode(', ', \array_map(function ($perm) {
252
                            return '`'.\CharlotteDunois\Yasmin\Models\Permissions::resolveToName($perm).'`';
253
                        }, $missing));
254
                        $msg = 'I need the following permissions for the `'.$this->command->name.'` command to work:'.\PHP_EOL.$missing;
255
                    }
256
                    
257
                    return $this->client->dispatcher->throttleNegativeResponseMessage($this, $msg, $resolve, $reject);
258
                }
259
            }
260
            
261
            // Throttle the command
262
            $throttle = $this->command->throttle($this->message->author->id);
263
            if($throttle && ($throttle['usages'] + 1) > ($this->command->throttling['usages'])) {
264
                $remaining = $throttle['start'] + $this->command->throttling['duration'] - \time();
265
                $this->client->emit('commandBlocked', $this, 'throttling');
266
                
267
                if($this->patternMatches !== null && !((bool) $this->client->getOption('commandThrottlingMessagePattern', true))) {
268
                    return $resolve();
269
                }
270
                
271
                return $this->client->dispatcher->throttleNegativeResponseMessage(
272
                    $this,
273
                    'You may not use the `'.$this->command->name.'` command again for another '.$remaining.' seconds.',
274
                    $resolve,
275
                    $reject
276
                );
277
            }
278
            
279
            // Figure out the command arguments
280
            $args = $this->patternMatches;
281
            $argmsgs = array();
282
            $countArgs = \count($this->command->args);
283
            
284
            if(!$args && $countArgs > 0) {
285
                $count = (!empty($this->command->args[($countArgs - 1)]['infinite']) ? \INF : $countArgs);
286
                $provided = self::parseArgs($this->argString, $count, $this->command->argsSingleQuotes);
287
                
288
                $promises[] = $this->command->argsCollector->obtain($this, $provided)->then(function ($result) use (&$args, &$argmsgs) {
289
                    if($result['cancelled']) {
290
                        if(\count($result['prompts']) === 0) {
291
                            throw new \CharlotteDunois\Livia\Exceptions\CommandFormatException($this);
292
                        }
293
                        
294
                        $argmsgs = $result['prompts'];
295
                        $this->client->emit('commandCancelled', $this, $result['cancelled']);
296
                        
297
                        throw new \CharlotteDunois\Livia\Exceptions\FriendlyException('Cancelled Command.');
298
                    }
299
                    
300
                    $args = $result['values'];
301
                    $argmsgs = $result['prompts'];
302
                    
303
                    if(!$args) {
304
                        $args = $this->parseCommandArgs();
305
                    }
306
                    
307
                    $args = new \ArrayObject(((array) $args), \ArrayObject::ARRAY_AS_PROPS);
308
                });
309
            } else {
310
                $args = new \ArrayObject(((array) $args), \ArrayObject::ARRAY_AS_PROPS);
311
            }
312
            
313
            // Run the command
314
            if($throttle) {
315
                $this->command->updateThrottle($this->message->author->id);
316
            }
317
            
318
            $typingCount = $this->message->channel->typingCount();
319
            
320
            \React\Promise\all($promises)->then(function () use (&$args, &$argmsgs) {
321
                $promise = $this->command->run($this, $args, ($this->patternMatches !== null));
322
                
323
                if(!($promise instanceof \React\Promise\PromiseInterface)) {
324
                    $promise = \React\Promise\resolve($promise);
325
                }
326
                
327
                $this->client->emit('commandRun', $this->command, $promise, $this, $args, ($this->patternMatches !== null));
328
                
329
                return $promise->then(function ($response) use (&$argmsgs) {
330
                    if(!($response instanceof \CharlotteDunois\Yasmin\Models\Message || $response instanceof \CharlotteDunois\Collect\Collection || \is_array($response) || $response === null)) {
331
                        throw new \RuntimeException('Command '.$this->command->name.'\'s run() resolved with an unknown type ('.\gettype($response).'). Command run methods must return a Promise that resolve with a Message, an array of Messages, a Collection of Messages, or null.');
332
                    }
333
                    
334
                    if(!\is_array($response) && !($response instanceof \CharlotteDunois\Collect\Collection)) {
335
                        if($response instanceof \CharlotteDunois\Yasmin\Models\Message) {
336
                            $argmsgs[] = $response;
337
                        }
338
                        
339
                        return $argmsgs;
340
                    }
341
                    
342
                    foreach($response as &$val) {
343
                        if(!($val instanceof \React\Promise\PromiseInterface)) {
344
                            $val = \React\Promise\resolve($val);
345
                        }
346
                    }
347
                    
348
                    return \React\Promise\all($response)->then(function ($msgs) use (&$argmsgs) {
349
                        return \array_merge($argmsgs, $msgs);
350
                    });
351
                });
352
            })->otherwise(function ($error) use (&$args, $typingCount, &$argmsgs) {
353
                if($this->message->channel->typingCount() > $typingCount) {
354
                    $this->message->channel->stopTyping();
355
                }
356
                
357
                if($error instanceof \CharlotteDunois\Livia\Exceptions\FriendlyException) {
358
                    return $this->reply($error->getMessage())->then(function (\CharlotteDunois\Yasmin\Models\Message $msg) use (&$argmsgs) {
359
                        $argmsgs[] = $msg;
360
                        return $argmsgs;
361
                    });
362
                }
363
                
364
                $this->client->emit('commandError', $this->command, $error, $this, $args, ($this->patternMatches !== null));
365
                
366
                $owners = $this->client->owners;
367
                $ownersLength = \count($owners);
368
                
369
                if($ownersLength > 0) {
370
                    $index = 0;
371
                    $owners = \array_map(function ($user) use ($index, $ownersLength) {
372
                        $or = ($ownersLength > 1 && $index === ($ownersLength - 1) ? 'or ' : '');
373
                        $index++;
374
                        
375
                        return $or.\CharlotteDunois\Yasmin\Utils\MessageHelpers::escapeMarkdown($user->tag);
376
                    }, $owners);
377
                    
378
                    $owners = \implode((\count($owners) > 2 ? ', ' : ' '), $owners);
379
                } else {
380
                    $owners = 'the bot owner';
381
                }
382
                
383
                return $this->reply('An error occurred while running the command: `'.\get_class($error).': '.\str_replace('`', '', $error->getMessage()).'`'.\PHP_EOL.
384
                        'You shouldn\'t ever receive an error like this.'.\PHP_EOL.
385
                        'Please contact '.$owners.($this->client->getOption('invite') ? ' in this server: '.$this->client->getOption('invite') : '.'))
386
                        ->then(function (\CharlotteDunois\Yasmin\Models\Message $msg) use (&$argmsgs) {
387
                            $argmsgs[] = $msg;
388
                            return $argmsgs;
389
                        });
390
            })->done($resolve, $reject);
391
        }));
392
    }
393
    
394
    /**
395
     * Responds to the command message
396
     * @param string  $type      One of plain, reply or direct.
397
     * @param string  $content
398
     * @param array   $options
399
     * @param bool    $fromEdit
400
     * @return \React\Promise\ExtendedPromiseInterface
401
     * @throws \RangeException
402
     * @throws \InvalidArgumentException
403
     */
404
    protected function respond(string $type, string $content, array $options = array(), bool $fromEdit = false) {
405
        if($type === 'reply' && $this->message->channel instanceof \CharlotteDunois\Yasmin\Interfaces\DMChannelInterface) {
406
            $type = 'plain';
407
        }
408
        
409
        if($type !== 'direct' && $this->message->guild !== null && !$this->message->channel->permissionsFor($this->client->user)->has('SEND_MESSAGES')) {
410
            $type = 'direct';
411
        }
412
        
413
        if(!empty($options['split']) && !\is_array($options['split'])) {
414
            $options['split'] = array();
415
        }
416
        
417
        $channelID = $this->getChannelIDOrDM($this->message->channel);
418
        $shouldEdit = (
419
            !empty($this->responses) &&
420
            (
421
                ($type === 'direct' && !empty($this->responses['dm'])) ||
422
                ($type !== 'direct' && !empty($this->responses[$channelID]))
423
            ) &&
424
            !$fromEdit &&
425
            empty($options['files'])
426
        );
427
        
428
        switch($type) {
429
            case 'plain':
430
                if($shouldEdit) {
431
                    return $this->editCurrentResponse($channelID, $type, $content, $options);
432
                } else {
433
                    return $this->message->channel->send($content, $options);
434
                }
435
            break;
436
            case 'reply':
437
                if($shouldEdit) {
438
                    return $this->editCurrentResponse($channelID, $type, $content, $options);
439
                } else {
440
                    if(!empty($options['split']) && empty($options['split']['prepend'])) {
441
                        $options['split']['prepend'] = $this->message->author->__toString().\CharlotteDunois\Yasmin\Models\Message::$replySeparator;
442
                    }
443
                    
444
                    return $this->message->reply($content, $options);
445
                }
446
            break;
447
            case 'direct':
448
                if($shouldEdit) {
449
                    return $this->editCurrentResponse($channelID, $type, $content, $options);
450
                } else {
451
                    return $this->message->author->createDM()->then(function ($channel) use ($content, $options) {
452
                        return $channel->send($content, $options);
453
                    });
454
                }
455
            break;
456
            default:
457
                throw new \RangeException('Unknown response type "'.$type.'"');
458
            break;
459
        }
460
    }
461
    
462
    /**
463
     * Edits a response to the command message. Resolves with an instance of Message or an array of Message instances.
464
     * @param \CharlotteDunois\Yasmin\Models\Message|\CharlotteDunois\Yasmin\Models\Message[]|null  $response
465
     * @param string                                                                                $type
466
     * @param string                                                                                $content
467
     * @param array                                                                                 $options
468
     * @return \React\Promise\ExtendedPromiseInterface
469
     */
470
    protected function editResponse($response, string $type, string $content, array $options = array()) {
471
        if(!$response) {
472
            return $this->respond($type, $content, $options);
473
        }
474
        
475
        if(!empty($options['split'])) {
476
            $content = \CharlotteDunois\Yasmin\Utils\MessageHelpers::splitMessage($content, (\is_array($options['split']) ? $options['split'] : array()));
477
            if(\count($content) === 1) {
478
                $content = $content[0];
479
            }
480
        }
481
        
482
        $prepend = '';
483
        if($type === 'reply') {
484
            $prepend = $this->message->author->__toString().\CharlotteDunois\Yasmin\Models\Message::$replySeparator;
485
        }
486
        
487
        if(\is_array($content)) {
488
            $promises = array();
489
            $clength = \count($content);
490
            
491
            if(\is_array($response)) {
492
                for($i = 0;  $i < $clength; $i++) {
493
                    if(!empty($response[$i])) {
494
                        $promises[] = $response[$i]->edit($prepend.$content[$i], $options);
495
                    } else {
496
                        $promises[] = $this->message->channel->send($prepend.$content[$i], $options);
497
                    }
498
                }
499
            } else {
500
                $promises[] = $response->edit($prepend.$content[0], $options);
501
                for($i = 1; $i < $clength; $i++) {
502
                    $promises[] = $this->message->channel->send($prepend.$content[$i], $options);
503
                }
504
            }
505
            
506
            return \React\Promise\all($promises);
507
        } else {
508
            if(\is_array($response)) {
509
                for($i = \count($response) - 1;  $i > 0; $i--) {
510
                    $response[$i]->delete()->done();
511
                }
512
                
513
                return $response[0]->edit($prepend.$content, $options);
514
            } else {
515
                return $response->edit($prepend.$content, $options);
516
            }
517
        }
518
    }
519
    
520
    /**
521
     * Edits the current response.
522
     * @param string  $id       The ID of the channel the response is in ("DM" for direct messages).
523
     * @param string  $type
524
     * @param string  $content
525
     * @param array   $options
526
     * @return \React\Promise\ExtendedPromiseInterface
527
     */
528
    protected function editCurrentResponse(string $id, string $type, string $content, array $options = array()) {
529
        if(empty($this->responses[$id])) {
530
            $this->responses[$id] = array();
531
        }
532
        
533
        if(!empty($this->responses[$id])) {
534
            $msg = \array_shift($this->responses[$id]);
535
        } else {
536
            $msg = null;
537
        }
538
        
539
        return $this->editResponse($msg, $type, $content, $options);
540
    }
541
    
542
    /**
543
     * Responds with a plain message. Resolves with an instance of Message or an array of Message instances.
544
     * @param string  $content
545
     * @param array   $options  Message Options.
546
     * @return \React\Promise\ExtendedPromiseInterface
547
     */
548
    function say(string $content, array $options = array()) {
549
        return $this->respond('plain', $content, $options);
550
    }
551
    
552
    /**
553
     * Responds with a reply message. Resolves with an instance of Message or an array of Message instances.
554
     * @param string  $content
555
     * @param array   $options  Message Options.
556
     * @return \React\Promise\ExtendedPromiseInterface
557
     */
558
    function reply(string $content, array $options = array()) {
559
        return $this->respond('reply', $content, $options);
560
    }
561
    
562
    /**
563
     * Responds with a direct message. Resolves with an instance of Message or an array of Message instances.
564
     * @param string  $content
565
     * @param array   $options  Message Options.
566
     * @return \React\Promise\ExtendedPromiseInterface
567
     */
568
    function direct(string $content, array $options = array()) {
569
        return $this->respond('direct', $content, $options);
570
    }
571
    
572
    /**
573
     * Shortcut to $this->message->edit.
574
     * @param string  $content
575
     * @param array   $options  Message Options.
576
     * @return \React\Promise\ExtendedPromiseInterface
577
     */
578
    function edit(string $content, array $options = array()) {
579
        return $this->message->edit($content, $options);
580
    }
581
    
582
    /**
583
     * Finalizes the command message by setting the responses and deleting any remaining prior ones.
584
     * @param \CharlotteDunois\Yasmin\Models\Message|\CharlotteDunois\Yasmin\Models\Message[]|null  $responses
585
     * @return void
586
     * @internal
587
     */
588
    function finalize($responses) {
589
        if(!empty($this->responses)) {
590
            $this->deleteRemainingResponses();
591
        }
592
        
593
        if(\is_array($responses)) {
594
            foreach($responses as $response) {
595
                /** @var \CharlotteDunois\Yasmin\Models\Message  $msg */
596
                $msg = (\is_array($response) ? $response[0] : $response);
597
                
598
                if(!($msg instanceof \CharlotteDunois\Yasmin\Models\Message)) {
599
                    continue;
600
                }
601
                
602
                $id = $this->getChannelIDOrDM($msg->channel);
603
                
604
                if(empty($this->responses[$id])) {
605
                    $this->responses[$id] = array();
606
                }
607
                
608
                $this->responses[$id][] = $response;
609
            }
610
        } elseif($responses !== null) {
611
            if(!($responses instanceof \CharlotteDunois\Yasmin\Models\Message)) {
612
                return;
613
            }
614
            
615
            $id = $this->getChannelIDOrDM($responses->channel);
616
            $this->responses[$id] = array($responses);
617
        }
618
    }
619
    
620
    /**
621
     * Deletes any prior responses that haven't been updated.
622
     * @return void
623
     * @internal
624
     */
625
    function deleteRemainingResponses() {
626
        foreach($this->responses as $id => $msgs) {
627
            foreach($msgs as $response) {
628
                if(\is_array($response)) {
629
                    foreach($response as $resp) {
630
                        $resp->delete()->done();
631
                    }
632
                } else {
633
                    $response->delete()->done();
634
                }
635
            }
636
            
637
            $this->responses[$id] = array();
638
        }
639
    }
640
    
641
    /**
642
     * @return string|int
643
     */
644
    protected function getChannelIDOrDM(\CharlotteDunois\Yasmin\Interfaces\TextChannelInterface $channel) {
645
        if(!($channel instanceof \CharlotteDunois\Yasmin\Interfaces\DMChannelInterface)) {
646
            return $channel->id;
647
        }
648
        
649
        return 'dm';
650
    }
651
    
652
    /**
653
     * Parses an argument string into an array of arguments.
654
     * @param string          $argString
655
     * @param int|float|null  $argCount           float = \INF
656
     * @param bool            $allowSingleQuotes
657
     * @return string[]
658
     */
659
    static function parseArgs(string $argString, $argCount = null, bool $allowSingleQuotes = true) {
660
        if(\mb_strlen($argString) === 0) {
661
            return array();
662
        }
663
        
664
        if($argCount === 1) {
665
            return array($argString);
666
        }
667
        
668
        $regex = '/(?:(['.($allowSingleQuotes ? "'" : '').'"])(.*?)(?<!\\\\)(?>\\\\\\\)*\1|([^\\s]+))/Su';
669
        $results = array();
670
        
671
        $argString = \trim($argString);
672
        
673
        if($argCount === null) {
674
            $argCount = \mb_strlen($argString); // Large enough to get all items
675
        }
676
        
677
        $content = $argString;
678
        \preg_match_all($regex, $argString, $matches);
679
        
680
        foreach($matches[0] as $key => $val) {
681
            $argCount--;
682
            if($argCount === 0) {
683
                break;
684
            }
685
            
686
            $val = \trim(($matches[3][$key] !== '' ? $matches[3][$key] : ($matches[2][$key] !== '' ? $matches[2][$key] : $matches[1][$key])));
687
            $results[] = $val;
688
            
689
            $content = \trim(\preg_replace('/'.\preg_quote($val, '/').'/u', '', $content, 1));
690
        }
691
        
692
        // If text remains, push it to the array as-is (except for wrapping quotes, which are removed)
693
        if(\mb_strlen($content) > 0) {
694
            $results[] = \preg_replace(($allowSingleQuotes ? '/^("|\')(.*)\1$/u' : '/^(")(.*)\1$/u'), '$2', $content);
695
        }
696
        
697
        if(\count($results) > 0) {
698
            $results = \array_filter($results, function ($val) {
699
                return (\mb_strlen(\trim($val)) > 0);
700
            });
701
        }
702
        
703
        return $results;
704
    }
705
    
706
    /**
707
     * @return void
708
     * @internal
709
     */
710
    function setResponses($responses) {
711
        $this->responses = $responses;
712
    }
713
}
714