Completed
Push — master ( fa6d1d...7e7c14 )
by Simon
29s
created

fisherman.js ➔ ???   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 15
Bugs 0 Features 0
Metric Value
c 15
b 0
f 0
nc 2
nop 1
dl 0
loc 43
cc 2
rs 8.8571
1
/**
2
 * The main fisherman class, to create commands and interact with them
3
 * @author Maxerbox | Simon Sassi 2017
4
 * @extends {EventEmitter}
5
 * @class Fisherman
6
 */
7
const async = require('async')
8
const FisherRegister = require('./register.js')
9
const defaultFisherOpts = require('./util/FishermanOptions')
10
const escapeRegExp = require('./util/RegExpEscape')
11
const fisherRouter = require('./router/router')
12
const EventEmitter = require('events')
13
const fisherCodes = require('./util/FisherCodes')
14
const CommandNotFoundException = require('./exceptions/CommandNotFoundException')
15
const InvalidChannelException = require('./exceptions/InvalidChannelException')
16
const InvalidPatternException = require('./exceptions/InvalidPatternException')
17
const MissingPermissionsException = require('./exceptions/MissingPermissionsException')
18
19
class Fisherman extends EventEmitter {
20
    /**
21
     * Creates an instance of Fisherman.
22
     * @param {FishermanOptions} options The options for fisherman
23
     * @memberof Fisherman
24
     */
25
  constructor (options = {}) {
26
    super()
27
        /**
28
         * The middleware handling function stack
29
         * @private
30
         * @name Fisherman#handleListeners
31
         * @type {Array}
32
         */
33
    this.handleListeners = []
34
        /**
35
         * The middleware function stack on init
36
         * @private
37
         * @name Fisherman#messageListeners
38
         * @type {Array}
39
         */
40
    this.setUpListeners = []
41
        /**
42
         * All the commands handled by fisherman
43
         * @name Fisherman#commands
44
         * @type {Map.<string, Command>}
45
         */
46
    this.commands = new Map()
47
        /**
48
         * All the command aliases handled by fisherman
49
         * @name Fisherman#aliases
50
         * @type {Map.<string, Command>}
51
         */
52
    this.aliases = new Map()
53
        /**
54
         * All the command registers handled by fisherman
55
         * @name Fisherman#registers
56
         * @type {Map.<string, FisherRegister>}
57
         */
58
    this.registers = new Map()
59
        /**
60
         * A fastfall empty callback
61
         * @private
62
         * @name Fisherman#fallHandle
63
         */
64
    this.fallHandle = require('fastfall')(this.handleListeners)
65
    this.setOptions(options)
66
    if (!this.client) { this.client = new (require('discord.js')).Client(this.clientOptions) }
67
  }
68
    /**
69
     * Set the options to fisherman
70
     *
71
     * @param {FishermanOptions} options
72
     * @memberof Fisherman
73
     */
74
  setOptions (options) {
75
    var opts = Object.assign(defaultFisherOpts, options)
76
    if (opts.client) { this.client = opts.client }
77
    if (opts.prefixes) { this.setPrefixe(opts.prefixes) }
78
    this.commandMatchRegExp = new RegExp(opts.commandMatchRegExp)
79
    this.ownerID = opts.ownerID
80
    this.clientOptions = options.clientOptions
81
    this.sendAliasStatus = opts.sendAliasStatus
82
    this.sendNotFoundStatus = opts.sendNotFoundStatus
83
    this.selfMessageProcessing = opts.selfMessageProcessing
84
  }
85
86
    /**
87
     *
88
     * Set the prefixe
89
     * @param {(Array|string)} prefixes
90
     * @memberof Fisherman
91
     */
92
  setPrefixe (prefixes) {
93
    if (typeof prefixes === 'string') {
94
      this.regPref = new RegExp('^(' + escapeRegExp.escapeString(prefixes) + ').*')
95
    } else if (Array.isArray(prefixes)) {
96
      var escapedArray = escapeRegExp.escapeArray(prefixes)
97
      this.regPref = new RegExp('^(' + escapedArray.join('|') + ').*')
98
    }
99
  }
100
    /**
101
     * Create a Fisherman instance from an already logged in discord.js client
102
     *
103
     * @static
104
     * @param {Client} client The discord.js client
105
     * @param {FishermanOptions} fisherOptions The options for Fisherman
106
     * @memberof Fisherman
107
     */
108
  static createFromClient (client, fisherOptions = {}) {
109
    var opts = Object.assign({ client: client }, fisherOptions)
110
    return new this(opts)
111
  }
112
113
    /**
114
     *
115
     * Message event listener
116
     * @private
117
     * @param {Message} message A discord.js Message
118
     * @memberof Fisherman
119
     */
120
  handleMessage (message) {
121
    if (message.author.id === this.client.user.id && !this.selfMessageProcessing) { return }
122
    var router = fisherRouter.buildFromMessage(this, message)
123
    var that = this
124
    var prefixe = router.request.prefix = this.checkPrefixe(message.content)
125
    try {
126
      router.request.command = prefixe ? this.checkCommand(prefixe, message) : null
127
      if (router.request.command) router.request.isCommand = true
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
128
    } catch (err) {
129
      router.response.sendCode(err.code, err)
130
      return
131
    }
132
    this.fallHandle(router.request, router.response, function (err, request, response) {
133
      if (err) return err === true ? undefined : router.response.sendCode(fisherCodes.MIDDLEWARE_FAILED, err)
134
      if (router.request.command) {
135
        var cmd = router.request.command
136
        that.matchSuffixe(cmd, cmd.suffixe, function (result) {
137
          if (result) {
138
            if (cmd.isPromise) {
139
              (new Promise(function (resolve, reject) {
140
                cmd.execute(router.request, router.response, resolve, reject)
141
              })).then(res => router.response.sendCode(fisherCodes.COMMAND_SUCESS, res)).catch(err => router.response.sendCode(fisherCodes.COMMAND_FAILED, err))
142
            } else {
143
              cmd.execute(router.request, router.response)
144
            }
145
          } else {
146
            router.response.sendCode(fisherCodes.INVALID_PATNERN, new InvalidPatternException(cmd.suffixe))
147
          }
148
        })
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
149
      }
150
    })
151
  }
152
153
    /**
154
     * Check if there is a command, throw exceptions
155
     * @private
156
     * @param {string} prefixe The command prefixe
157
     * @param {Message} message A discord.js message
158
     * @returns {Array}
159
     * @memberof Fisherman
160
     */
161
  checkCommand (prefixe, message) {
162
        /*
163
        Benchmark split vs regex match : https://jsperf.com/regex-vs-split/2
164
        Benchmark inline RegExp vs Stored RegExp : https://jsperf.com/regexp-indexof-perf/24
165
        */
166
    var textCmd = message.content.substring(prefixe.length).match(this.commandMatchRegExp)[0]
167
    var cmd = this.commands.get(textCmd) || this.aliases.get(textCmd)
168
    cmd = this.validateCommand(message, cmd, textCmd) ? cmd : null
169
    if (cmd) cmd.suffixe = textCmd ? message.content.substring(prefixe.length + textCmd.length + 1) : ''
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
170
    return cmd
171
  }
172
    /**
173
     *
174
     * @private
175
     * @param {command} cmd
176
     * @param {string} suffixe
177
     * @param {function} validate
178
     * @memberof Fisherman
179
     */
180
  matchSuffixe (cmd, suffixe, validate) {
181
    if (cmd.regPattern) {
182
      validate(cmd.regPattern.test(suffixe))
183
    } else if (cmd.patternCallback) {
184
      cmd.patternCallback.test(suffixe, validate)
185
    } else {
186
      validate(true)
187
    }
188
  }
189
    /**
190
     * Validate a command, throw exceptions
191
     * @private
192
     * @param {Message} message A discord.js message
193
     * @param {command} cmd the command
194
     * @returns {boolean}
195
     * @memberof Fisherman
196
     */
197
  validateCommand (message, cmd, textCmd) {
198
    if (!cmd && this.sendNotFoundStatus) { throw new CommandNotFoundException(textCmd) } else if (!cmd) { return false }
199
    if (cmd.channelType.indexOf(message.channel.type) === -1) { throw new InvalidChannelException(message.channel.type) }
200
    let permissions = message.guild ? message.channel.permissionsFor(this.client.user) : null
201
    if (permissions && permissions.has(cmd.discordPermRequired)) {
202
      let missing = permissions.missing(cmd.discordSpecialPerms)
203
      if (missing.length > 0) { throw new MissingPermissionsException(missing) }
204
    }
205
    return true
206
  }
207
    /**
208
     * Check if there is a prefixe in content
209
     * @private
210
     * @param {string} content
211
     * @returns {string}
212
     * @memberof Fisherman
213
     */
214
  checkPrefixe (content) {
215
    var result = this.regPref.exec(content)
216
        // return (Array.isArray(result)) ? result[1] : null;
217
    return result ? result[1] : null
218
  }
219
    /**
220
     * Initialize Fisherman and the middlewares
221
     * @param {string} [token = null] The token to log in with, optional if the client is already connected
0 ignored issues
show
Documentation Bug introduced by
The parameter [token does not exist. Did you maybe mean token instead?
Loading history...
222
     * @param {function} callback An optional callback to trigger when Fisherman is initialized
223
     * @memberof Fisherman
224
     * @fires Fisherman#initialized
225
     */
226
  init (token = null, callback) {
227
    /**
228
     *  Emitted when Fisherman and middlewares are initialized
229
     * @event Fisherman#initialized
230
     */
231
    this.client.on('message', this.handleMessage.bind(this))
232
    var that = this
233
    if (!token) {
234
      this.initializeMiddleware(callback)
235
    } else {
236
      this.client.login(token).then(() => {
237
        that.initializeMiddleware(callback)
238
      })
239
    }
240
  }
241
    /**
242
     * Initialize the middlewares
243
     * @private
244
     *
245
     * @memberof Fisherman
246
     */
247
  initializeMiddleware (callback) {
248
    var that = this
249
    async.parallel(this.setUpListeners, function (err) {
250
      if (err) { throw err }
251
      if (typeof callback === 'function') { callback() }
252
      that.emit('initialized')
253
    })
254
  }
255
256
    /**
257
     * Create a new register to add commands
258
     * @fires Fisherman#registerAdded
259
     * @param {string} keyName The register key value, to set in the registers map
260
     * @param {string} [registerName = null] The register's name
0 ignored issues
show
Documentation Bug introduced by
The parameter [registerName does not exist. Did you maybe mean registerName instead?
Loading history...
261
     * @param {string} [registerDescription = null] The register's description
0 ignored issues
show
Documentation Bug introduced by
The parameter [registerDescription does not exist. Did you maybe mean registerDescription instead?
Loading history...
262
     * @return {FisherRegister} Return a FisherRegister instance
263
     * @memberof Fisherman
264
     */
265
  createRegister (keyName, registerName = null, registerDescription = null) {
266
    /**
267
     * Emitted when a new register is added
268
     * @event Fisherman#registerAdded
269
     */
270
    var register = new FisherRegister(this, registerName || keyName, registerDescription)
271
    this.registers.set(keyName, register)
272
    return register
273
  }
274
    /**
275
     *
276
     * Add a middleware to Fisherman
277
     * @param {(function|Object)} middleware The middleware function|class
278
     * @return {Fisherman}
279
     * @memberof Fisherman
280
     */
281
  use (middleware) {
282
    if (typeof middleware !== 'object' && typeof middleware !== 'function') throw new TypeError('A middleware must be a function or an object')
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
283
    this.appendMiddleware(middleware)
284
    return this
285
  }
286
287
    /**
288
     *
289
     * Add a middleware to Fisherman
290
     * @private
291
     * @param {function} middleware The middleware function|class
292
     * @memberof Fisherman
293
     */
294
  appendMiddleware (middleware) {
295
    if (typeof middleware.setUp === 'function') { this.setUpListeners.push(middleware.setUp.bind(middleware, this)) }
296
    if (typeof middleware.handle === 'function') {
297
      this.handleListeners.push(middleware.handle.bind(middleware))
298
      this.fallHandle = require('fastfall')(this.handleListeners)
299
    }
300
  }
301
}
302
module.exports = Fisherman
303