Source: botCommands.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// Copyright 2019-2022 Campbell Crowley. All rights reserved.
// Author: Campbell Crowley (dev@campbellcrowley.com)
const fs = require('fs');
const SubModule = require('./subModule.js');

/**
 * @description Provides interface to allow other bots to run commands.
 * @augments SubModule
 * @listens Discord~Client#message
 * @listens Command#togglebot
 */
class BotCommands extends SubModule {
  /**
   * @description SubModule providing external bot command interface.
   */
  constructor() {
    super();
    /** @inheritdoc */
    this.myName = 'BotCommands';

    /**
     * @description The name of the file to check for if bot commands are
     * enabled.
     * @private
     * @constant
     * @default
     * @type {string}
     */
    this._filename = '/enableBotCommands';

    /**
     * @description Maximum number of commands that can be triggered this way
     * per {@link BotCommands~_maxDelta}.
     * @private
     * @type {number}
     * @constant
     * @default
     */
    this._maxNum = 5;
    /**
     * @description Amount of time in milliseconds where a maximum of
     * {@link BotCommands~_maxNum} commands may be triggered before cooldown is
     * started.
     * @private
     * @type {number}
     * @constant
     * @default
     */
    this._maxDelta = 15 * 1000;
    /**
     * @description Amount of time in milliseconds to ignore all bot commands
     * after exceeding the rate limit.
     * @private
     * @type {number}
     * @constant
     * @default
     */
    this._cooldownLength = 10 * 1000;

    /**
     * @description History of last few commands triggered per-guild. Used to
     * put command triggering on cooldown if rate limits are exceeded.
     * @private
     * @type {object.<{
     *   history: Array.<{author: string, time: number}>,
     *   cooldownStart: number
     * }>}
     * @default
     */
    this._recentCommands = {};

    this._onMessage = this._onMessage.bind(this);
    this._commandToggleBotCmds = this._commandToggleBotCmds.bind(this);
  }
  /** @inheritdoc */
  initialize() {
    this.command.on(new this.command.SingleCommand(
        [
          'allowbot',
          'allowbots',
          'enablebot',
          'enablebots',
          'togglebot',
          'togglebots',
          'denybot',
          'denybots',
          'disablebot',
          'disablebots',
        ],
        this._commandToggleBotCmds, new this.command.CommandSetting({
          validOnlyInGuild: true,
          defaultDisabled: true,
          permissions: this.Discord.PermissionsBitField.Flags.ManageRoles |
              this.Discord.PermissionsBitField.Flags.ManageGuild,
        })));
    this.client.guilds.cache.forEach((g) => {
      this.common.readFile(
          `${this.common.guildSaveDir}${g.id}${this._filename}`, () => {});
    });
    this.client.on('messageCreate', this._onMessage);
  }
  /** @inheritdoc */
  shutdown() {
    this.command.deleteEvent('allowbot');
    this.client.removeListener('messageCreate', this._onMessage);
  }

  /**
   * Handle messages sent by bots.
   *
   * @private
   * @param {Discord~Message} msg Message was sent.
   * @listens Discord#message
   */
  _onMessage(msg) {
    if (!msg.guild || !msg.author.bot ||
        msg.author.id === this.client.user.id) {
      return;
    }

    const prefixRegex = new RegExp(`^<@!?${this.client.user.id}>`);

    if (!msg.content.match(prefixRegex)) return;

    let recent = this._recentCommands[msg.guild.id];
    const now = Date.now();
    if (recent && now - recent.cooldownStart < this._cooldownLength) return;

    if (!fs.existsSync(
        `${this.common.guildSaveDir}${msg.guild.id}${this._filename}`)) {
      return;
    }

    msg.content = msg.content.replace(prefixRegex, '').trim();
    msg.prefix = this.bot.getPrefix(msg.guild);
    msg.botCmd = true;

    const commandSuccess =
        this.command.validate(msg.content.split(/ |\n/)[0], msg);
    if (commandSuccess !== 'No Handler') {
      if (!recent) {
        recent = this._recentCommands[msg.guild.id] = {
          history: [],
          cooldownStart: 0,
        };
      }
      const hist = recent.history;

      hist.push({author: msg.author.id, time: now});

      let num = 0;
      const oldest = now - this._maxDelta;
      while (num < hist.length && hist[num].time < oldest) num++;
      if (num > 0) hist.splice(0, num);

      if (hist.length > this._maxNum) {
        this.common.reply(
            msg, 'Bot Command Rate Limit',
            'Rate limit exceeded. All bot commands will be ignored for ' +
                '10 seconds.');
        this._recentCommands[msg.guild.id].cooldownStart = now;
        return;
      }
    }
    const content = msg.content.replace(/\n/g, '\\n');
    let author;
    let logged;
    if (msg.guild !== null) {
      author = `Bot:${msg.guild.id}#${msg.channel.id}@${msg.author.id}`;
    } else {
      author = `Bot:PM:${msg.author.id}@${msg.author.tag}`;
    }
    if (!commandSuccess) {
      logged = `${author} ${content}`;
      this.log(logged);
    } else {
      logged = `${author} ${commandSuccess} ${content}`;
      this.debug(logged);
    }
    const start = Date.now();
    this.command.trigger(msg);
    const delta = Date.now() - start;
    if (delta > 20) {
      this.debug(`${logged} took an excessive ${delta}ms`);
    }
  }

  /**
   * @description Toggle whether bots are able to run commands on this guild.
   * @private
   * @type {commandHandler}
   * @param {Discord~Message} msg Message that triggered command.
   * @listens Command#togglebot
   */
  _commandToggleBotCmds(msg) {
    const enableList = ['enable', 'on', 'allow', 'true'];
    const disableList = ['disable', 'off', 'deny', 'false', 'disallow'];
    const file = `${this.common.guildSaveDir}${msg.guild.id}${this._filename}`;
    const exists = fs.existsSync(file);
    let setTo = true;
    const content = msg.content.toLocaleLowerCase();
    if (enableList.find((el) => content.indexOf(el) > -1)) {
      setTo = true;
    } else if (disableList.find((el) => content.indexOf(el) > -1)) {
      setTo = false;
    } else {
      setTo = !exists;
    }

    if (!exists && setTo) {
      const emoji = '✅';
      this.common
          .reply(
              msg, 'Are you sure?',
              'Allowing bots to run commands could potentially cause a ' +
                  'feedback loop. Only enable this if you know what you are' +
                  ' doing. Use at your own risk.\nReact with ' + emoji +
                  ' to confirm')
          .then((msg_) => {
            msg_.react(emoji).catch(() => {});
            const filter = (reaction, user) =>
              reaction.emoji.name == emoji && user.id === msg.author.id;
            msg_.awaitReactions({filter, max: 1, time: 30 * 1000})
                .then((reactions) => {
                  msg_.reactions.removeAll().catch(() => {});
                  if (reactions.size == 0) {
                    msg_.edit({content: 'Timed Out'}).catch(() => {});
                    return;
                  }
                  try {
                    this.common.mkAndWriteSync(file, null, 'true');
                    this.common.reply(msg, 'Bot Commands', 'Now Allowed');
                  } catch (err) {
                    this.error(
                        'Failed to enable bot commands: ' + msg.guild.id);
                    console.error(err);
                    this.common.reply(
                        msg, 'Bot Commands',
                        'Failed to toggle due to internal error.');
                  }
                });
          });
    } else if (exists && !setTo) {
      this.common.unlink(file, (err) => {
        if (err) {
          this.error(`Failed to disable bot commands: ${msg.guild.id}`);
          console.error(err);
          this.common.reply(
              msg, 'Bot Commands', 'Failed to toggle due to internal error.');
        } else {
          this.common.reply(msg, 'Bot Commands', 'Now Disallowed');
        }
      });
    } else {
      this.common.reply(
          msg, 'Bot Commands',
          setTo ? 'Already allowed' : 'Already disallowed');
    }
  }
}

module.exports = new BotCommands();