Failed Conditions
Push — master ( 06e79d...257567 )
by Charlotte
02:19
created

Client::__construct()   B

Complexity

Conditions 9
Paths 64

Size

Total Lines 47
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 27
nc 64
nop 2
dl 0
loc 47
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;
11
12
/**
13
 * The Livia Client, the heart of the framework.
14
 *
15
 * @property \CharlotteDunois\Livia\CommandDispatcher                 $dispatcher     The client's command dispatcher.
16
 * @property \CharlotteDunois\Livia\CommandRegistry                   $registry       The client's command registry.
17
 * @property \CharlotteDunois\Livia\Providers\SettingProvider|null    $provider       The client's setting provider.
18
 *
19
 * @property string|null                                              $commandPrefix  The global command prefix. ({@see Client::setCommandPrefix})
20
 * @property \CharlotteDunois\Yasmin\Models\User[]                    $owners         Owners of the bot, set by the client option owners. If you simply need to check if a user is an owner of the bot, please use Client::isOwner instead. ({@see \CharlotteDunois\Livia\Client:isOwner})
21
 */
22
class Client extends \CharlotteDunois\Yasmin\Client {
23
    /**
24
     * The client's command dispatcher.
25
     * @var \CharlotteDunois\Livia\CommandDispatcher
26
     */
27
    protected $dispatcher;
28
    
29
    /**
30
     * The client's command registry.
31
     * @var \CharlotteDunois\Livia\CommandRegistry
32
     */
33
    protected $registry;
34
    
35
    /**
36
     * The client's setting provider.
37
     * @var \CharlotteDunois\Livia\Providers\SettingProvider|null
38
     */
39
    protected $provider;
40
    
41
    /**
42
     * Constructs a new Command Client. Additional available Client Options are as following:
43
     *
44
     * ```
45
     * array(
46
     *   'commandPrefix' => string|null, (Default command prefix, null means only mentions will trigger the handling, defaults to l$)
47
     *   'commandBlockedMessagePattern' => bool, (Whether command pattern maatches will send command blocked messages, defaults to true)
48
     *   'commandEditableDuration' => int, (Time in seconds that command messages should be editable, defaults to 30)
49
     *   'commandThrottlingMessagePattern' => bool, (Whether command pattern matches will send command throttling messages, defaults to true)
50
     *   'negativeResponseThrottlingDuration' => int, (Time in seconds how long negative responses should be not repeated during that time (determined by authorID-channelID-command), defaults to 15)
51
     *   'nonCommandEditable' => bool, (Whether messages without commands can be edited to a command, defaults to true)
52
     *   'unknownCommandResponse' => bool, (Whether the bot should respond to an unknown command, defaults to true)
53
     *   'owners' => string[], (Array of user IDs)
54
     *   'invite' => string, (Invite URL to the bot's support server)
55
     * )
56
     * ```
57
     *
58
     * @param array                           $options  Any Client Options.
59
     * @param \React\EventLoop\LoopInterface  $loop
60
     * @throws \Exception
61
     * @throws \RuntimeException
62
     * @throws \InvalidArgumentException
63
     */
64
    function __construct(array $options = array(), ?\React\EventLoop\LoopInterface $loop = null) {
65
        if(!\array_key_exists('commandPrefix', $options)) {
66
            $options['commandPrefix'] = 'l$';
67
        }
68
        
69
        if(!isset($options['commandEditableDuration'])) {
70
            $options['commandEditableDuration'] = 30;
71
        }
72
        
73
        if(!isset($options['negativeResponseThrottlingDuration'])) {
74
            $options['negativeResponseThrottlingDuration'] = 15;
75
        }
76
        
77
        if(!isset($options['nonCommandEditable'])) {
78
            $options['nonCommandEditable'] = true;
79
        }
80
        
81
        if(!isset($options['unknownCommandResponse'])) {
82
            $options['unknownCommandResponse'] = true;
83
        }
84
        
85
        parent::__construct($options, $loop);
86
        
87
        $this->dispatcher = new \CharlotteDunois\Livia\CommandDispatcher($this);
88
        $this->registry = new \CharlotteDunois\Livia\CommandRegistry($this);
89
        
90
        $msgError = function ($error) {
91
            $this->emit('error', $error);
92
        };
93
        
94
        $this->on('message', function ($message) use ($msgError) {
95
            $this->dispatcher->handleMessage($message)->done(null, $msgError);
96
        });
97
        $this->on('messageUpdate', function ($message, $oldMessage) use ($msgError) {
98
            $this->dispatcher->handleMessage($message, $oldMessage)->done(null, $msgError);
99
        });
100
        
101
        if(!empty($options['owners'])) {
102
            $this->once('ready', function () use ($options) {
103
                if(!\is_array($options['owners'])) {
104
                    $options['owners'] = array($options['owners']);
105
                }
106
                
107
                foreach($options['owners'] as $owner) {
108
                    $this->fetchUser($owner)->done(null, function ($error) use ($owner) {
109
                        $this->emit('warn', 'Unable to fetch owner '.$owner);
110
                        $this->emit('error', $error);
111
                    });
112
                }
113
            });
114
        }
115
    }
116
    
117
    /**
118
     * @param string  $name
119
     * @return mixed
120
     * @throws \Exception
121
     * @internal
122
     */
123
    function __get($name) {
124
        switch($name) {
125
            case 'dispatcher':
126
            case 'registry':
127
            case 'provider':
128
                return $this->$name;
129
            break;
130
            case 'commandPrefix':
131
                return $this->options['commandPrefix'];
132
            break;
133
            case 'owners':
134
                $owners = array();
135
                foreach($this->options['owners'] as $owner) {
136
                    $owners[$owner] = $this->users->get($owner);
137
                }
138
                
139
                return $owners;
140
            break;
141
        }
142
        
143
        return parent::__get($name);
144
    }
145
    
146
    /**
147
     * @return string
148
     * @internal
149
     */
150
    function serialize() {
151
        $serializable = ($this->provider instanceof \Serializable);
152
        
153
        if(!$serializable) {
154
            $provider = $this->provider;
155
            $this->provider = null;
156
        }
157
        
158
        $str = parent::serialize();
159
        
160
        if(!$serializable) {
161
            $this->provider = $provider;
162
        }
163
        
164
        return $str;
165
    }
166
    
167
    /**
168
     * @return void
169
     * @internal
170
     */
171
    function unserialize($str) {
172
        parent::unserialize($str);
173
        
174
        foreach($this->registry->internalSerializedCommands as $command) {
175
            $this->registry->registerCommand($command);
176
        }
177
        
178
        $this->registry->unsetInternalSerializedCommands();
179
    }
180
    
181
    /**
182
     * Sets the global command prefix. Null indicates that there is no default prefix, and only mentions will be used. Emits a commandPrefixChange event.
183
     * @param string|null  $prefix
184
     * @param bool         $fromProvider
185
     * @return $this
186
     * @throws \InvalidArgumentException
187
     */
188
    function setCommandPrefix($prefix, bool $fromProvider = false) {
189
        if(\is_string($prefix) && empty($prefix)) {
190
            throw new \InvalidArgumentException('Can not set an empty string as command prefix');
191
        }
192
        
193
        $this->options['commandPrefix'] = $prefix;
194
        
195
        if(!$fromProvider) {
196
            $this->emit('commandPrefixChange', null, $prefix);
197
        }
198
        
199
        return $this;
200
    }
201
    
202
    /**
203
     * Checks whether an user is an owner of the bot.
204
     * @param string|\CharlotteDunois\Yasmin\Models\User|\CharlotteDunois\Yasmin\Models\GuildMember  $user
205
     * @return bool
206
     */
207
    function isOwner($user) {
208
        if($user instanceof \CharlotteDunois\Yasmin\Models\User || $user instanceof \CharlotteDunois\Yasmin\Models\GuildMember) {
209
            $user = $user->id;
210
        }
211
        
212
        return (\in_array($user, $this->options['owners']));
213
    }
214
    
215
    /**
216
     * Sets the setting provider to use, and initializes it once the client is ready.
217
     * @param \CharlotteDunois\Livia\Providers\SettingProvider  $provider
218
     * @return \React\Promise\ExtendedPromiseInterface
219
     */
220
    function setProvider(\CharlotteDunois\Livia\Providers\SettingProvider $provider) {
221
        $this->provider = $provider;
222
        
223
        return (new \React\Promise\Promise(function (callable $resolve, callable $reject) {
224
            $classname = \get_class($this->provider);
225
            
226
            if($this->readyTimestamp !== null) {
227
                $this->emit('debug', 'Provider set to '.$classname.' - initializing...');
228
                $this->provider->init($this)->done($resolve, $reject);
229
                return;
230
            }
231
            
232
            $this->emit('debug', 'Provider set to '.$classname.' - will initialize once ready');
233
            
234
            $this->once('ready', function () use ($resolve, $reject) {
235
                $this->emit('debug', 'Initializing provider...');
236
                $this->provider->init($this)->done(function () use ($resolve) {
237
                    $this->emit('debug', 'Provider finished initialization.');
238
                    $resolve();
239
                }, $reject);
240
            });
241
        }));
242
    }
243
    
244
    /**
245
     * Get the guild's prefix - or the default prefix. Null means only mentions.
246
     * @param \CharlotteDunois\Yasmin\Models\Guild|null  $guild
247
     * @return string|null
248
     */
249
    function getGuildPrefix(?\CharlotteDunois\Yasmin\Models\Guild $guild = null) {
250
        if($guild !== null && $this->provider !== null) {
251
            try {
252
                $prefix = $this->provider->get($guild, 'commandPrefix', 404);
253
                if($prefix !== 404 && $prefix !== '') {
254
                    return $prefix;
255
                }
256
            } catch (\BadMethodCallException $e) {
257
                return $this->commandPrefix;
258
            }
259
        }
260
        
261
        return $this->commandPrefix;
262
    }
263
    
264
    /**
265
     * Set the guild's prefix. An empty string means the command prefix will be used. Null means only mentions.
266
     * @param \CharlotteDunois\Yasmin\Models\Guild|null  $guild
267
     * @param string|null                                $prefix
268
     * @return bool
269
     */
270
    function setGuildPrefix(?\CharlotteDunois\Yasmin\Models\Guild $guild, string $prefix = null) {
271
        $this->emit('commandPrefixChange', $guild, $prefix);
272
        return true;
273
    }
274
    
275
    /**
276
     * Login into Discord. Opens a WebSocket Gateway connection. Resolves once a WebSocket connection has been successfully established (does not mean the client is ready).
277
     * @param string $token  Your token.
278
     * @param bool   $force  Forces the client to get the gateway address from Discord.
279
     * @return \React\Promise\ExtendedPromiseInterface
280
     * @throws \RuntimeException
281
     * @internal
282
     */
283
    function login(string $token, bool $force = false) {
284
        $this->addPeriodicTimer(300, array($this->dispatcher, 'cleanupNegativeResponseMessages'));
285
        
286
        return parent::login($token, $force);
287
    }
288
    
289
    /**
290
     * @return \React\Promise\ExtendedPromiseInterface
291
     * @internal
292
     */
293
    function destroy(bool $destroyUtils = true) {
294
        return parent::destroy($destroyUtils)->then(function () {
295
            if($this->provider !== null) {
296
                return $this->provider->destroy();
297
            }
298
        });
299
    }
300
    
301
    /**
302
     * Validates the passed client options.
303
     * @param array  $options
304
     * @return void
305
     * @throws \InvalidArgumentException
306
     */
307
    protected function validateClientOptions(array $options) {
308
        parent::validateClientOptions($options);
309
        
310
        \CharlotteDunois\Validation\Validator::make($options, array(
311
            'commandPrefix' => 'string|nullable',
312
            'commandBlockedMessagePattern' => 'boolean',
313
            'commandEditableDuration' => 'integer',
314
            'commandThrottlingMessagePattern' => 'boolean',
315
            'nonCommandEditable' => 'boolean',
316
            'unknownCommandResponse' => 'boolean',
317
            'owners' => 'array:string|array:integer',
318
            'invite' => 'string'
319
        ))->throw(\InvalidArgumentException::class);
320
    }
321
}
322