Command::isUsable()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 5
nc 4
nop 1
dl 0
loc 10
rs 9.6111
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
 * A command that can be run in a client.
14
 *
15
 * @property \CharlotteDunois\Livia\Client                      $client             The client which initiated the instance.
16
 * @property string                                             $name               The name of the command.
17
 * @property string[]                                           $aliases            Aliases of the command.
18
 * @property \CharlotteDunois\Livia\Commands\CommandGroup|null  $group              The group the command belongs to, assigned upon registration.
19
 * @property string                                             $groupID            ID of the command group the command is part of.
20
 * @property string                                             $description        A short description of the command.
21
 * @property string|null                                        $details            A longer description of the command.
22
 * @property string                                             $format             Usage format string of the command.
23
 * @property string[]                                           $examples           Examples of and for the command.
24
 * @property bool                                               $guildOnly          Whether the command can only be triggered in a guild channel.
25
 * @property bool                                               $ownerOnly          Whether the command can only be triggered by the bot owner (requires default hasPermission method).
26
 * @property string[]|null                                      $clientPermissions  The required permissions for the client user to make the command work.
27
 * @property string[]|null                                      $userPermissions    The required permissions for the user to use the command.
28
 * @property bool                                               $nsfw               Whether the command can only be run in NSFW channels.
29
 * @property array                                              $throttling         Options for throttling command usages.
30
 * @property bool                                               $defaultHandling    Whether the command gets handled normally.
31
 * @property array                                              $args               An array containing the command arguments.
32
 * @property int|double                                         $argsPromptLimit    How many times the user gets prompted for an argument.
33
 * @property bool                                               $argsSingleQuotes   Whether single quotes are allowed to encapsulate an argument.
34
 * @property string                                             $argsType           How the arguments are split when passed to the command's run method.
35
 * @property int                                                $argsCount          Maximum number of arguments that will be split.
36
 * @property string[]                                           $patterns           Regular expression triggers.
37
 * @property bool                                               $guarded            Whether the command is protected from being disabled.
38
 * @property bool                                               $hidden             Whether the command is hidden in the `help` command overview.
39
 */
40
abstract class Command {
41
    /**
42
     * The client which initiated the instance.
43
     * @var \CharlotteDunois\Livia\Client
44
     */
45
    protected $client;
46
    
47
    /**
48
     * The name of the command.
49
     * @var string
50
     */
51
    protected $name;
52
    
53
    /**
54
     * Aliases of the command.
55
     * @var string[]
56
     */
57
    protected $aliases = array();
58
    
59
    /**
60
     * ID of the command group the command is part of.
61
     * @var string
62
     */
63
    protected $groupID;
64
    
65
    /**
66
     * A short description of the command.
67
     * @var string
68
     */
69
    protected $description;
70
    
71
    /**
72
     * A longer description of the command.
73
     * @var string|null
74
     */
75
    protected $details;
76
    
77
    /**
78
     * Usage format string of the command.
79
     * @var string
80
     */
81
    protected $format = '';
82
    
83
    /**
84
     * Examples of and for the command.
85
     * @var string[]
86
     */
87
    protected $examples = array();
88
    
89
    /**
90
     * Whether the command can only be triggered in a guild channel.
91
     * @var bool
92
     */
93
    protected $guildOnly = false;
94
    
95
    /**
96
     * Whether the command can only be triggered by the bot owner (requires default hasPermission method).
97
     * @var bool
98
     */
99
    protected $ownerOnly = false;
100
    
101
    /**
102
     * The required permissions for the client user to make the command work.
103
     * @var string[]|null
104
     */
105
    protected $clientPermissions;
106
    
107
    /**
108
     * The required permissions for the user to use the command.
109
     * @var string[]|null
110
     */
111
    protected $userPermissions;
112
    
113
    /**
114
     * Whether the command can only be run in NSFW channels.
115
     * @var bool
116
     */
117
    protected $nsfw = false;
118
    
119
    /**
120
     * Options for throttling command usages.
121
     * @var array
122
     */
123
    protected $throttling = array();
124
    
125
    /**
126
     * Whether the command gets handled normally.
127
     * @var bool
128
     */
129
    protected $defaultHandling = true;
130
    
131
    /**
132
     * An array containing the command arguments.
133
     * @var array
134
     */
135
    protected $args = array();
136
    
137
    /**
138
     * How many times the user gets prompted for an argument.
139
     * @var int|float
140
     */
141
    protected $argsPromptLimit = \INF;
142
    
143
    /**
144
     * Whether single quotes are allowed to encapsulate an argument.
145
     * @var bool
146
     */
147
    protected $argsSingleQuotes = true;
148
    
149
    /**
150
     * How the arguments are split when passed to the command's run method.
151
     * @var string
152
     */
153
    protected $argsType = 'single';
154
    
155
    /**
156
     * Maximum number of arguments that will be split.
157
     * @var int
158
     */
159
    protected $argsCount = 0;
160
    
161
    /**
162
     * Command patterns for pattern matches.
163
     * @var string[]
164
     */
165
    protected $patterns = array();
166
    
167
    /**
168
     * Whether the command is guarded (can not be disabled).
169
     * @var bool
170
     */
171
    protected $guarded = false;
172
    
173
    /**
174
     * Whether the command is hidden in the `help` command overview.
175
     * @var bool
176
     */
177
    protected $hidden = false;
178
    
179
    /**
180
     * Whether the command is globally enabled.
181
     * @var bool
182
     */
183
    protected $globalEnabled = true;
184
    
185
    /**
186
     * Array of guild ID to bool, which indicates whether the command is enabled in that guild.
187
     * @var bool[]
188
     */
189
    protected $guildEnabled = array();
190
    
191
    /**
192
     * The argument collector for the command.
193
     * @var \CharlotteDunois\Livia\Arguments\ArgumentCollector|null
194
     */
195
    protected $argsCollector;
196
    
197
    /**
198
     * A collection of throttle arrays.
199
     * @var \CharlotteDunois\Collect\Collection
200
     */
201
    protected $throttles;
202
    
203
    /**
204
     * Constructs a new Command. Info is an array as following:
205
     *
206
     * ```
207
     * array(
208
     *   'name' => string,
209
     *   'aliases' => string[], (optional)
210
     *   'group' => string, (the ID of the command group)
211
     *   'description => string,
212
     *   'details' => string, (optional)
213
     *   'format' => string, (optional)
214
     *   'examples' => string[], (optional)
215
     *   'guildOnly' => bool, (defaults to false)
216
     *   'ownerOnly' => bool, (defaults to false)
217
     *   'clientPermissions' => string[], (optional)
218
     *   'userPermissions' => string[], (optional)
219
     *   'nsfw' => bool, (defaults to false)
220
     *   'throttling' => array, (associative array of array('usages' => int, 'duration' => int) - duration in seconds, optional)
221
     *   'defaultHandling' => bool, (defaults to true)
222
     *   'args' => array, ({@see \CharlotteDunois\Livia\Arguments\Argument} - key can be the index instead, optional)
223
     *   'argsPromptLimit' => int|\INF, (optional)
224
     *   'argsType' => string, (one of 'single' or 'multiple', defaults to 'single')
225
     *   'argsCount' => int, (optional)
226
     *   'argsSingleQuotes' => bool, (optional)
227
     *   'patterns' => string[], (Regular Expression strings, pattern matches don't get parsed for arguments, optional)
228
     *   'guarded' => bool, (defaults to false)
229
     *   'hidden' => bool, (defaults to false)
230
     * )
231
     * ```
232
     *
233
     * @param \CharlotteDunois\Livia\Client  $client
234
     * @param array                          $info
235
     * @throws \InvalidArgumentException
236
     */
237
    function __construct(\CharlotteDunois\Livia\Client $client, array $info) {
238
        $this->client = $client;
239
        
240
        \CharlotteDunois\Validation\Validator::make($info, array(
241
            'name' => 'required|string|lowercase|nowhitespace',
242
            'group' => 'required|string',
243
            'description' => 'required|string',
244
            'aliases' => 'array:string',
245
            'autoAliases' => 'boolean',
246
            'details' => 'string',
247
            'format' => 'string',
248
            'examples' => 'array:string',
249
            'guildOnly' => 'boolean',
250
            'ownerOnly' => 'boolean',
251
            'clientPermissions' => 'array',
252
            'userPermissions' => 'array',
253
            'nsfw' => 'boolean',
254
            'throttling' => 'array',
255
            'defaultHandling' => 'boolean',
256
            'args' => 'array',
257
            'argsType' => 'string|in:single,multiple',
258
            'argsPromptLimit' => 'integer|float',
259
            'argsCount' => 'integer|min:2',
260
            'patterns' => 'array:string',
261
            'guarded' => 'boolean',
262
            'hidden' => 'boolean'
263
        ))->throw(\InvalidArgumentException::class);
264
        
265
        $this->name = $info['name'];
266
        $this->groupID = $info['group'];
267
        $this->description = $info['description'];
268
        
269
        if(!empty($info['aliases'])) {
270
            $this->aliases = $info['aliases'];
271
            
272
            foreach($this->aliases as $alias) {
273
                if(\mb_strtolower($alias) !== $alias) {
274
                    throw new \InvalidArgumentException('Command aliases must be lowercase');
275
                }
276
            }
277
        }
278
        
279
        if(!empty($info['autoAliases'])) {
280
            if(\mb_strpos($this->name, '-') !== false) {
281
                $this->aliases[] = \str_replace('-', '', $this->name);
282
            }
283
            
284
            foreach($this->aliases as $alias) {
285
                if(\mb_strpos($alias, '-') !== false) {
286
                    $this->aliases[] = \str_replace('-', '', $alias);
287
                }
288
            }
289
        }
290
        
291
        $this->details = $info['details'] ?? $this->details;
292
        $this->format = $info['format'] ?? $this->format;
293
        $this->examples = $info['examples'] ?? $this->examples;
294
        
295
        $this->guildOnly = $info['guildOnly'] ?? $this->guildOnly;
296
        $this->ownerOnly = $info['ownerOnly'] ?? $this->ownerOnly;
297
        
298
        $this->clientPermissions = $info['clientPermissions'] ?? $this->clientPermissions;
299
        $this->userPermissions = $info['userPermissions'] ?? $this->userPermissions;
300
        
301
        $this->nsfw = $info['nsfw'] ?? $this->nsfw;
302
        
303
        if(isset($info['throttling'])) {
304
            if(empty($info['throttling']['usages']) || empty($info['throttling']['duration'])) {
305
                throw new \InvalidArgumentException('Throttling array is missing elements or its elements are empty');
306
            }
307
            
308
            if(!\is_int($info['throttling']['usages'])) {
309
                throw new \InvalidArgumentException('Throttling usages must be an integer');
310
            }
311
            
312
            if(!\is_int($info['throttling']['duration'])) {
313
                throw new \InvalidArgumentException('Throttling duration must be an integer');
314
            }
315
            
316
            $this->throttling = $info['throttling'];
317
        }
318
        
319
        $this->defaultHandling = $info['defaultHandling'] ?? $this->defaultHandling;
320
        
321
        $this->args = $info['args'] ?? array();
322
        if(!empty($this->args)) {
323
            $this->argsCollector = new \CharlotteDunois\Livia\Arguments\ArgumentCollector($this->client, $this->args, $this->argsPromptLimit);
324
            
325
            if(empty($this->format)) {
326
                $this->format = \array_reduce($this->argsCollector->args, function ($prev, $arg) {
327
                    $wrapL = ($arg->default !== null ? '[' : '<');
328
                    $wrapR = ($arg->default !== null ? ']' : '>');
329
                    
330
                    return $prev.($prev ? ' ' : '').$wrapL.$arg->label.(!empty($arg->infinite) ? '...' : '').$wrapR;
331
                }, null);
332
            }
333
        }
334
        
335
        $this->argsSingleQuotes = (bool) ($info['argsSingleQuotes'] ?? $this->argsSingleQuotes);
336
        $this->argsType = $info['argsType'] ?? $this->argsType;
337
        $this->argsPromptLimit = $info['argsPromptLimit'] ?? $this->argsPromptLimit;
338
        $this->argsCount = $info['argsCount'] ?? $this->argsCount;
339
        
340
        $this->patterns = $info['patterns'] ?? $this->patterns;
341
        $this->guarded = $info['guarded'] ?? $this->guarded;
342
        $this->hidden = $info['hidden'] ?? $this->hidden;
343
        
344
        $this->throttles = new \CharlotteDunois\Collect\Collection();
345
    }
346
    
347
    /**
348
     * @param string  $name
349
     * @return bool
350
     * @throws \Exception
351
     * @internal
352
     */
353
    function __isset($name) {
354
        try {
355
            return $this->$name !== null;
356
        } catch (\RuntimeException $e) {
357
            if($e->getTrace()[0]['function'] === '__get') {
358
                return false;
359
            }
360
            
361
            throw $e;
362
        }
363
    }
364
    
365
    /**
366
     * @param string  $name
367
     * @return mixed
368
     * @throws \RuntimeException
369
     * @internal
370
     */
371
    function __get($name) {
372
        if(\property_exists($this, $name)) {
373
            return $this->$name;
374
        }
375
        
376
        switch($name) {
377
            case 'group':
378
                return $this->client->registry->resolveGroup($this->groupID);
379
            break;
380
        }
381
        
382
        throw new \RuntimeException('Unknown property '.\get_class($this).'::$'.$name);
383
    }
384
    
385
    /**
386
     * Checks if the user has permission to use the command.
387
     * @param \CharlotteDunois\Livia\Commands\Context  $context
388
     * @param bool                                     $ownerOverride  Whether the bot owner(s) will always have permission.
389
     * @return bool|string  Whether the user has permission, or an error message to respond with if they don't.
390
     */
391
    function hasPermission(\CharlotteDunois\Livia\Commands\Context $context, bool $ownerOverride = true) {
392
        if($this->ownerOnly === false && empty($this->userPermissions)) {
393
            return true;
394
        }
395
        
396
        if($ownerOverride && $this->client->isOwner($context->message->author)) {
397
            return true;
398
        }
399
        
400
        if($this->ownerOnly && ($ownerOverride || !$this->client->isOwner($context->message->author))) {
401
            return 'The command `'.$this->name.'` can only be used by the bot owner.';
402
        }
403
        
404
        // Ensure the user has the proper permissions
405
        if(
406
            $context->message->channel instanceof \CharlotteDunois\Yasmin\Interfaces\GuildChannelInterface &&
407
            $context->message->guild !== null &&
408
            !empty($this->userPermissions)
409
        ) {
410
            $perms = $context->message->channel->permissionsFor($context->message->member);
411
            
412
            $missing = array();
413
            foreach($this->userPermissions as $perm) {
414
                if($perms->missing($perm)) {
415
                    $missing[] = $perm;
416
                }
417
            }
418
            
419
            if(\count($missing) > 0) {
420
                $this->client->emit('commandBlocked', $context, 'userPermissions');
421
                
422
                if(\count($missing) === 1) {
423
                    $msg = 'The command `'.$this->name.'` requires you to have the `'.$missing[0].'` permission.';
424
                } else {
425
                    $missing = \implode(', ', \array_map(function ($perm) {
426
                        return '`'.\CharlotteDunois\Yasmin\Models\Permissions::resolveToName($perm).'`';
427
                    }, $missing));
428
                    $msg = 'The `'.$this->name.'` command requires you to have the following permissions:'.\PHP_EOL.$missing;
429
                }
430
                
431
                return $msg;
432
            }
433
        }
434
        
435
        return true;
436
    }
437
    
438
    /**
439
     * Runs the command. The method must return null, an array of Message instances or an instance of Message, a Promise that resolves to an instance of Message, or an array of Message instances. The array can contain Promises which each resolves to an instance of Message.
440
     * @param \CharlotteDunois\Livia\Commands\Context  $context      The context the command is being run for.
441
     * @param \ArrayObject                             $args         The arguments for the command, or the matches from a pattern. If args is specified on the command, this will be the argument values object. If argsType is single, then only one string will be passed. If multiple, an array of strings will be passed. When fromPattern is true, this is the matches array from the pattern match.
442
     * @param bool                                     $fromPattern  Whether or not the command is being run from a pattern match.
443
     * @return \React\Promise\ExtendedPromiseInterface|\React\Promise\ExtendedPromiseInterface[]|\CharlotteDunois\Yasmin\Models\Message|\CharlotteDunois\Yasmin\Models\Message[]|null|void
444
     */
445
    abstract function run(\CharlotteDunois\Livia\Commands\Context $context, \ArrayObject $args, bool $fromPattern);
446
    
447
    /**
448
     * Reloads the command.
449
     * @return void
450
     * @throws \RuntimeException
451
     */
452
    function reload() {
453
        $this->client->registry->reregisterCommand($this->groupID.':'.$this->name, $this);
454
    }
455
    
456
    /**
457
     * Unloads the command.
458
     * @return void
459
     * @throws \RuntimeException
460
     */
461
    function unload() {
462
        $this->client->registry->unregisterCommand($this);
463
    }
464
    
465
    /**
466
     * Creates/obtains the throttle object for a user, if necessary (owners are excluded).
467
     * @param string  $userID
468
     * @return array|null
469
     * @internal
470
     */
471
    final function throttle(string $userID) {
472
        if(empty($this->throttling) || $this->client->isOwner($userID)) {
473
            return null;
474
        }
475
        
476
        if(!$this->throttles->has($userID)) {
477
            $this->throttles->set($userID, array(
478
                'start' => \time(),
479
                'usages' => 0,
480
                'timeout' => $this->client->addTimer($this->throttling['duration'], function () use ($userID) {
481
                    $this->throttles->delete($userID);
482
                })
483
            ));
484
        }
485
        
486
        return $this->throttles->get($userID);
487
    }
488
    
489
    /**
490
     * Increments the usage of the throttle object for a user, if necessary (owners are excluded).
491
     * @param string  $userID
492
     * @return void
493
     * @internal
494
     */
495
    final function updateThrottle(string $userID) {
496
        if(empty($this->throttling) || $this->client->isOwner($userID)) {
497
            return;
498
        }
499
        
500
        if(!$this->throttles->has($userID)) {
501
            $this->throttles->set($userID, array(
502
                'start' => \time(),
503
                'usages' => 1,
504
                'timeout' => $this->client->addTimer($this->throttling['duration'], function () use ($userID) {
505
                    $this->throttles->delete($userID);
506
                })
507
            ));
508
            
509
            return;
510
        }
511
        
512
        $throttle = $this->throttles->get($userID);
513
        $throttle['usages']++;
514
        
515
        $this->throttles->set($userID, $throttle);
516
    }
517
    
518
    /**
519
     * Enables or disables the command in a guild (or globally).
520
     * @param string|int|\CharlotteDunois\Yasmin\Models\Guild|null  $guild    The guild instance or the guild ID.
521
     * @param bool                                                  $enabled
522
     * @return bool
523
     * @throws \BadMethodCallException
524
     * @throws \InvalidArgumentException
525
     */
526
    function setEnabledIn($guild, bool $enabled) {
527
        if($guild !== null) {
528
            $guild = $this->client->guilds->resolve($guild);
529
        }
530
        
531
        if($this->guarded) {
532
            throw new \BadMethodCallException('The group is guarded');
533
        }
534
        
535
        if($guild !== null) {
536
            $this->guildEnabled[$guild->id] = $enabled;
537
        } else {
538
            $this->globalEnabled = $enabled;
539
        }
540
        
541
        $this->client->emit('commandStatusChange', $guild, $this, $enabled);
542
        return ($guild !== null ? $this->guildEnabled[$guild->id] : $this->globalEnabled);
543
    }
544
    
545
    /**
546
     * Checks if the command is enabled in a guild or globally.
547
     * @param \CharlotteDunois\Yasmin\Models\Guild|string|int|null  $guild  The guild instance or the guild ID, null for global.
548
     * @return bool
549
     * @throws \InvalidArgumentException
550
     */
551
    function isEnabledIn($guild) {
552
        if($guild !== null) {
553
            $guild = $this->client->guilds->resolve($guild);
554
            return ($this->globalEnabled && (!\array_key_exists($guild->id, $this->guildEnabled) || $this->guildEnabled[$guild->id]));
555
        }
556
        
557
        return $this->globalEnabled;
558
    }
559
    
560
    /**
561
     * Checks if the command is usable for a message.
562
     * @param \CharlotteDunois\Livia\Commands\Context|null  $context
563
     * @return bool
564
     */
565
    function isUsable(?\CharlotteDunois\Livia\Commands\Context $context = null) {
566
        if($context === null) {
567
            return $this->globalEnabled;
568
        }
569
        
570
        if($this->guildOnly && $context->message->guild === null) {
571
            return false;
572
        }
573
        
574
        return ($this->isEnabledIn($context->message->guild) && $this->hasPermission($context) === true);
575
    }
576
    
577
    /**
578
     * Creates a usage string for the command.
579
     * @param string                               $argString  A string of arguments for the command.
580
     * @param string|null                          $prefix     Prefix to use for the prefixed command format.
581
     * @param \CharlotteDunois\Yasmin\Models\User  $user       User to use for the mention command format. Defaults to client user.
582
     * @return string
583
     */
584
    function usage(string $argString, string $prefix = null, \CharlotteDunois\Yasmin\Models\User $user = null) {
585
        if($prefix === null) {
586
            $prefix = $this->client->commandPrefix;
587
        }
588
        
589
        if($user === null) {
590
            $user = $this->client->user;
591
        }
592
        
593
        return self::anyUsage($this->name.' '.$argString, $prefix, $user);
594
    }
595
    
596
    /**
597
     * Creates a usage string for any command.
598
     * @param string                                    $command    A command + arguments string.
599
     * @param string|null                               $prefix     Prefix to use for the prefixed command format.
600
     * @param \CharlotteDunois\Yasmin\Models\User|null  $user       User to use for the mention command format.
601
     * @return string
602
     */
603
    static function anyUsage(string $command, string $prefix = null, \CharlotteDunois\Yasmin\Models\User $user = null) {
604
        $command = \str_replace(' ', "\u{00A0}", $command);
605
        
606
        if(empty($prefix) && $user === null) {
607
            return '`'.$command.'`';
608
        }
609
        
610
        $prStr = null;
611
        if(!empty($prefix)) {
612
            $prefix = \str_replace(' ', "\u{00A0}", $prefix);
613
            $prStr = '`'.\CharlotteDunois\Yasmin\Utils\MessageHelpers::escapeMarkdown($prefix.$command).'`';
614
        }
615
        
616
        $meStr = null;
617
        if($user !== null) {
618
            $meStr = '`@'.\CharlotteDunois\Yasmin\Utils\MessageHelpers::escapeMarkdown(\str_replace(' ', "\u{00A0}", $user->tag)."\u{00A0}".$command).'`';
619
        }
620
        
621
        return ($prStr ?? '').(!empty($prefix) && $user !== null ? ' or ' : '').($meStr ?? '');
622
    }
623
}
624