├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── CNAME ├── assets │ ├── css │ │ ├── main.css │ │ └── main.css.map │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.js ├── classes │ ├── _core_client_.handlesclient.html │ ├── _core_client_.handlesclient.internal.eventemitter.html │ ├── _core_commandhandler_.commandhandler.html │ ├── _core_commandregistry_.commandregistry.html │ ├── _errors_argumenterror_.argumenterror.html │ ├── _errors_baseerror_.baseerror.html │ ├── _errors_validationerror_.validationerror.html │ ├── _middleware_argument_.argument.html │ ├── _middleware_validator_.validator.html │ ├── _structures_command_.command.html │ ├── _structures_response_.response.html │ └── _util_queue_.queue.html ├── globals.html ├── index.html ├── interfaces │ ├── _interfaces_config_.iconfig.html │ ├── _middleware_argument_.ioptions.html │ ├── _structures_command_.icommandoptions.html │ └── _structures_response_.iresponseoptions.html └── modules │ ├── _core_client_.handlesclient.internal.html │ ├── _core_client_.html │ ├── _core_commandhandler_.html │ ├── _core_commandregistry_.html │ ├── _errors_argumenterror_.html │ ├── _errors_baseerror_.html │ ├── _errors_validationerror_.html │ ├── _index_.html │ ├── _interfaces_config_.html │ ├── _middleware_argument_.html │ ├── _middleware_validator_.html │ ├── _structures_command_.html │ ├── _structures_response_.html │ └── _util_queue_.html ├── examples ├── commands │ ├── ayy.js │ ├── ban.js │ └── ping.js └── index.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── core │ ├── Client.ts │ ├── CommandHandler.ts │ └── CommandRegistry.ts ├── errors │ ├── ArgumentError.ts │ ├── BaseError.ts │ └── ValidationError.ts ├── index.ts ├── interfaces │ └── Config.ts ├── middleware │ ├── Argument.ts │ └── Validator.ts ├── structures │ ├── Command.ts │ └── Response.ts └── util │ └── Queue.ts ├── test ├── commands │ ├── add.js │ ├── dm.js │ ├── error.js │ ├── eval.js │ ├── invalid.js │ ├── ping.js │ └── spam.js └── index.js ├── tsconfig.json └── tslint.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "ecmaVersion": 2017 10 | }, 11 | "rules": { 12 | "indent": ["error", 2, { "SwitchCase": 1 }], 13 | "linebreak-style": ["error", "unix"], 14 | "quotes": ["error", "single"], 15 | "semi": ["error", "always"], 16 | "brace-style": ["error", "1tbs"], 17 | "no-multi-spaces": "error", 18 | "no-unused-vars": "warn", 19 | "no-console": "warn", 20 | "eqeqeq": ["warn", "always"], 21 | "no-invalid-this": "warn", 22 | "require-await": "error", 23 | "block-spacing": ["error", "always"], 24 | "prefer-const": ["warn", { "destructuring": "any" }], 25 | "keyword-spacing": "error", 26 | "object-curly-spacing": ["error", "always"], 27 | "prefer-template": "warn" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | /test/.env 4 | /dist 5 | /typings 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /test 3 | /docs 4 | /examples 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | - "8" 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/test/index.js", 12 | "preLaunchTask": "compile", 13 | "sourceMaps": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "taskName": "compile", 8 | "type": "shell", 9 | "command": "gulp" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 William Nelson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Handles 2 | 3 | [![Handles support server](https://discordapp.com/api/guilds/251245211416657931/embed.png)](https://discord.gg/DPuaDvP) 4 | [![Build Status](https://travis-ci.org/appellation/handles.svg?branch=master)](https://travis-ci.org/appellation/handles) 5 | ![Downloads](https://img.shields.io/npm/dt/discord-handles.svg) 6 | 7 | For those of us who get frustrated with writing command handlers but don't quite want to use a full framework. Intended for use with [Discord.js](https://github.com/hydrabolt/discord.js). 8 | 9 | Documentation is available at [handles.topkek.pw](http://handles.topkek.pw). 10 | 11 | ## Getting started 12 | 13 | ### Installation 14 | 15 | ```xl 16 | npm install --save discord-handles 17 | ``` 18 | 19 | Or, if you want to risk cutting yourself, install the bleeding edge version: 20 | 21 | ```xl 22 | npm install --save appellation/handles#master 23 | ``` 24 | 25 | Usually I try to avoid pushing broken code, but sometimes I move a little too fast. 26 | 27 | ### The basics 28 | 29 | ```js 30 | const discord = require('discord.js'); 31 | const handles = require('discord-handles'); 32 | 33 | const client = new discord.Client(); 34 | const handler = new handles.Client(client); 35 | 36 | client.login('token'); 37 | ``` 38 | 39 | This will automatically load all commands in the `./commands` directory and handle incoming messages. See [`Command`](https://handles.topkek.pw/modules/_structures_command_.html) in the docs for information on how to format the exports of the files you place in `./commands`. Particularly of interest are the `pre`, `exec`, and `post` methods. The loader and handler can be configured according to [`Config`](https://handles.topkek.pw/modules/_interfaces_config_.html) options passed to the constructor. 40 | 41 | ```js 42 | const handler = new handles.Client(client, { 43 | directory: './some/other/awesome/directory', 44 | prefixes: new Set(['dank', 'memes']) 45 | }); 46 | ``` 47 | 48 | Here's an example of what you might place in the `./commands` directory. 49 | ```js 50 | const { MessageMentions, Permissions } = require('discord.js'); 51 | const { Command, Argument, Validator } = require('discord-handles'); 52 | 53 | module.exports = class extends Command { 54 | static get triggers() { 55 | return ['banne', 'ban']; 56 | } 57 | 58 | async pre() { 59 | await this.guild.fetchMembers(); 60 | 61 | await new Validator(this) 62 | .apply(this.guild.me.permissions.has(Permissions.FLAGS.BAN_MEMBERS), 'I don\'t have permission to ban people.') 63 | .apply(this.member.permissions.has(Permissions.FLAGS.BAN_MEMBERS), 'You don\'t have permission to ban people.'); 64 | 65 | const member = await new Argument(this, 'member') 66 | .setResolver(c => { 67 | const member = this.guild.members.get(c); 68 | 69 | // if they provided a raw user ID 70 | if (member) return member; 71 | // if they mentioned someone 72 | else if (MessageMentions.USERS_PATTERN.test(c)) return this.guild.members.get(c.match(MessageMentions.USERS_PATTERN)[1]); 73 | // if they provided a user tag 74 | else if (this.guild.members.exists(u => u.tag === c)) return this.guild.members.find(u => u.tag === c); 75 | else return null; 76 | }) 77 | .setPrompt('Who would you like to ban?') 78 | .setRePrompt('You provided an invalid user. Please try again.'); 79 | 80 | await new Validator(this) 81 | .apply(member.bannable, 'I cannot ban this person.'); 82 | .apply(member.highestRole.position < this.member.highestRole.position, 'You cannot ban this person.') 83 | 84 | await new Argument(this, 'days') 85 | .setResolver(c => parseInt(c) || null); 86 | .setOptional(); 87 | } 88 | 89 | async exec() { 90 | await this.args.member.ban(this.args.days); 91 | return this.response.success(`banned ${this.args.member.user.tag}`); 92 | } 93 | }; 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appellation/handles/807e3af14e6a6b1739e69ffa69432d8144671a13/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | handles.topkek.pw -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appellation/handles/807e3af14e6a6b1739e69ffa69432d8144671a13/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appellation/handles/807e3af14e6a6b1739e69ffa69432d8144671a13/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appellation/handles/807e3af14e6a6b1739e69ffa69432d8144671a13/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appellation/handles/807e3af14e6a6b1739e69ffa69432d8144671a13/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /docs/assets/js/search.js: -------------------------------------------------------------------------------- 1 | var typedoc = typedoc || {}; 2 | typedoc.search = typedoc.search || {}; 3 | typedoc.search.data = {"kinds":{"1":"External module","65536":"Type literal"},"rows":[{"id":0,"kind":65536,"name":"__type","url":"modules/_core_commandregistry_.html#readdir.__type","classes":"tsd-kind-type-literal tsd-parent-kind-variable tsd-is-not-exported","parent":"\"core/CommandRegistry\".readdir"},{"id":1,"kind":65536,"name":"__type","url":"modules/_core_commandregistry_.html#stat.__type-1","classes":"tsd-kind-type-literal tsd-parent-kind-variable tsd-is-not-exported","parent":"\"core/CommandRegistry\".stat"},{"id":2,"kind":65536,"name":"__type","url":"modules/_core_commandhandler_.html#messagevalidator.__type","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias tsd-is-not-exported","parent":"\"core/CommandHandler\".MessageValidator"},{"id":3,"kind":65536,"name":"__type","url":"modules/_util_queue_.html#queuefunction.__type","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias tsd-is-not-exported","parent":"\"util/Queue\".QueueFunction"},{"id":4,"kind":65536,"name":"__type","url":"modules/_structures_response_.html#send.__type","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias tsd-is-not-exported","parent":"\"structures/Response\".Send"},{"id":5,"kind":65536,"name":"__type","url":"modules/_middleware_validator_.html#validationfunction.__type","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias tsd-is-not-exported","parent":"\"middleware/Validator\".ValidationFunction"},{"id":6,"kind":65536,"name":"__type","url":"modules/_middleware_argument_.html#resolver.__type-1","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias tsd-is-not-exported","parent":"\"middleware/Argument\".Resolver"},{"id":7,"kind":65536,"name":"__type","url":"modules/_middleware_argument_.html#matcher.__type","classes":"tsd-kind-type-literal tsd-parent-kind-type-alias tsd-is-not-exported","parent":"\"middleware/Argument\".Matcher"},{"id":8,"kind":1,"name":"\"index\"","url":"modules/_index_.html","classes":"tsd-kind-external-module"}]}; -------------------------------------------------------------------------------- /docs/globals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 59 |

discord-handles

60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |

Index

68 |
69 | 89 |
90 |
91 |
92 | 150 |
151 |
152 | 211 |
212 |

Generated using TypeDoc

213 |
214 |
215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /docs/interfaces/_structures_command_.icommandoptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ICommandOptions | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 65 |

Interface ICommandOptions

66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |

Hierarchy

74 |
    75 |
  • 76 | ICommandOptions 77 |
  • 78 |
79 |
80 |
81 |

Index

82 |
83 |
84 |
85 |

Properties

86 | 91 |
92 |
93 |
94 |
95 |
96 |

Properties

97 |
98 | 99 |

body

100 |
body: string
101 | 106 |
107 |
108 | 109 |

message

110 |
message: Message
111 | 116 |
117 |
118 | 119 |

trigger

120 |
trigger: Trigger
121 | 126 |
127 |
128 |
129 | 211 |
212 |
213 | 272 |
273 |

Generated using TypeDoc

274 |
275 |
276 | 277 | 278 | 279 | -------------------------------------------------------------------------------- /docs/modules/_core_client_.handlesclient.internal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | internal | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 68 |

Module internal

69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |

Index

77 |
78 |
79 |
80 |

Classes

81 | 84 |
85 |
86 |
87 |
88 |
89 | 105 |
106 |
107 | 166 |
167 |

Generated using TypeDoc

168 |
169 |
170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/modules/_core_client_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "core/Client" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "core/Client"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |
81 |
82 |
83 | 144 |
145 |
146 | 205 |
206 |

Generated using TypeDoc

207 |
208 |
209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /docs/modules/_core_commandhandler_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "core/CommandHandler" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "core/CommandHandler"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Type aliases

81 | 84 |
85 |
86 |
87 |
88 |
89 |

Type aliases

90 |
91 | 92 |

MessageValidator

93 |
MessageValidator: function
94 | 99 |
100 |

Type declaration

101 |
    102 |
  • 103 |
      104 |
    • (m: Message): string | null | Promise<string | null>
    • 105 |
    106 |
      107 |
    • 108 |

      Parameters

      109 |
        110 |
      • 111 |
        m: Message
        112 |
      • 113 |
      114 |

      Returns string 115 | | 116 | null 117 | | 118 | Promise<string | null> 119 |

      120 |
    • 121 |
    122 |
  • 123 |
124 |
125 |
126 |
127 |
128 | 192 |
193 |
194 | 253 |
254 |

Generated using TypeDoc

255 |
256 |
257 | 258 | 259 | 260 | -------------------------------------------------------------------------------- /docs/modules/_errors_argumenterror_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "errors/ArgumentError" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "errors/ArgumentError"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |
81 |
82 |
83 | 144 |
145 |
146 | 205 |
206 |

Generated using TypeDoc

207 |
208 |
209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /docs/modules/_errors_baseerror_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "errors/BaseError" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "errors/BaseError"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |
81 |
82 |
83 | 144 |
145 |
146 | 205 |
206 |

Generated using TypeDoc

207 |
208 |
209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /docs/modules/_errors_validationerror_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "errors/ValidationError" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "errors/ValidationError"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |
81 |
82 |
83 | 144 |
145 |
146 | 205 |
206 |

Generated using TypeDoc

207 |
208 |
209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /docs/modules/_index_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "index" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "index"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 128 |
129 |
130 | 189 |
190 |

Generated using TypeDoc

191 |
192 |
193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /docs/modules/_interfaces_config_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "interfaces/Config" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "interfaces/Config"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 144 |
145 |
146 | 205 |
206 |

Generated using TypeDoc

207 |
208 |
209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /docs/modules/_middleware_validator_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "middleware/Validator" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "middleware/Validator"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Type aliases

81 | 84 |
85 |
86 |
87 |
88 |
89 |

Type aliases

90 |
91 | 92 |

ValidationFunction

93 |
ValidationFunction: function
94 | 99 |
100 |
101 |
validator.apply(cmd => cmd.message.author.id === 'some id', 'uh oh'); // executed at runtime
102 | // or
103 | validator.apply(cmd.message.author.id === 'some id', 'uh oh'); // executed immediately
104 | 
105 |
106 |
107 |
108 |

Type declaration

109 |
    110 |
  • 111 | 114 |
      115 |
    • 116 |

      Parameters

      117 | 122 |

      Returns boolean

      123 |
    • 124 |
    125 |
  • 126 |
127 |
128 |
129 |
130 |
131 | 195 |
196 |
197 | 256 |
257 |

Generated using TypeDoc

258 |
259 |
260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /docs/modules/_structures_command_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "structures/Command" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "structures/Command"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Interfaces

81 | 84 |
85 |
86 |

Type aliases

87 | 90 |
91 |
92 |
93 |
94 |
95 |

Type aliases

96 |
97 | 98 |

Trigger

99 |
Trigger: string | RegExp
100 | 105 |
106 |
107 |
108 | 175 |
176 |
177 | 236 |
237 |

Generated using TypeDoc

238 |
239 |
240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /docs/modules/_util_queue_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "util/Queue" | discord-handles 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "util/Queue"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Type aliases

81 | 84 |
85 |
86 |
87 |
88 |
89 |

Type aliases

90 |
91 | 92 |

QueueFunction

93 |
QueueFunction: function
94 | 99 |
100 |

Type declaration

101 |
    102 |
  • 103 |
      104 |
    • (...args: any[]): Promise<void>
    • 105 |
    106 |
      107 |
    • 108 |

      Parameters

      109 |
        110 |
      • 111 |
        Rest ...args: any[]
        112 |
      • 113 |
      114 |

      Returns Promise<void>

      115 |
    • 116 |
    117 |
  • 118 |
119 |
120 |
121 |
122 |
123 | 187 |
188 |
189 | 248 |
249 |

Generated using TypeDoc

250 |
251 |
252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /examples/commands/ayy.js: -------------------------------------------------------------------------------- 1 | exports.exec = cmd => cmd.response.send('lmao'); 2 | exports.triggers = /^ay+$/i; 3 | -------------------------------------------------------------------------------- /examples/commands/ban.js: -------------------------------------------------------------------------------- 1 | exports.exec = (command) => { 2 | return command.args.member.ban(); 3 | }; 4 | 5 | exports.validator = (validator, command) => { 6 | // const mention = /<@!?([0-9]+)>/; 7 | return validator.apply( 8 | command.message.channel.type === 'text', 9 | 'This command cannot be run outside of a guild.' 10 | ) && 11 | validator.apply( 12 | command.message.member.hasPermission('BAN_MEMBERS'), 13 | 'You do not have permission to run this command.' 14 | ) && 15 | validator.apply( 16 | command.message.guild.me.permissions.has('BAN_MEMBERS'), 17 | 'I cannot ban members in this guild.' 18 | ); 19 | }; 20 | 21 | exports.arguments = function* (Argument) { 22 | const member = new Argument('member') 23 | .setPrompt('Who would you like to ban?') 24 | .setRePrompt('Please provide a valid member.') 25 | .setResolver((c, msg) => { 26 | if (!msg.mentions.users.size) return null; 27 | 28 | let toBan = msg.mentions.users.filter(u => u.id !== u.client.user.id); 29 | if (toBan.size > 1) { 30 | member.rePrompt = `Found multiple users: \`${toBan.map(u => `${u.username}#${u.discriminator}`).join(', ')}\``; 31 | return null; 32 | } 33 | if (toBan.size < 1) return null; 34 | 35 | toBan = toBan.first(); 36 | if (!toBan.bannable) return null; 37 | return msg.guild.member(toBan) || null; 38 | }); 39 | 40 | yield member; 41 | }; 42 | 43 | exports.triggers = [ 44 | 'ban', 45 | 'banne', 46 | 'b&', 47 | '🔨' 48 | ]; 49 | -------------------------------------------------------------------------------- /examples/commands/ping.js: -------------------------------------------------------------------------------- 1 | exports.exec = command => command.response.success('pong'); 2 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | const discord = require('discord.js'); 2 | const handles = require('../src/index'); 3 | 4 | const client = new discord.Client(); 5 | client.once('ready', () => { 6 | const handler = new handles.Client({ 7 | // config options go here 8 | }); 9 | client.on('message', handler.handle); 10 | }); 11 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const del = require('del'); 3 | const ts = require('gulp-typescript'); 4 | const sourcemaps = require('gulp-sourcemaps'); 5 | const typedoc = require('gulp-typedoc'); 6 | const project = ts.createProject('tsconfig.json'); 7 | 8 | gulp.task('default', ['build']); 9 | 10 | gulp.task('build', () => { 11 | del.sync(['dist/**', '!dist']); 12 | del.sync(['typings/**', '!typings']); 13 | 14 | const result = project.src() 15 | .pipe(sourcemaps.init()) 16 | .pipe(project()); 17 | 18 | result.dts.pipe(gulp.dest('typings')); 19 | result.js.pipe(sourcemaps.write('.', { sourceRoot: '../src' })).pipe(gulp.dest('dist')); 20 | }); 21 | 22 | gulp.task('docs', () => { 23 | del.sync(['docs/**']); 24 | return gulp.src(['src/*.ts']) 25 | .pipe(typedoc({ 26 | module: 'commonjs', 27 | target: 'es2017', 28 | out: './docs', 29 | })); 30 | }); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-handles", 3 | "version": "7.3.5", 4 | "description": "A simple Discord command handler.", 5 | "main": "dist/index.js", 6 | "typings": "typings/index.d.ts", 7 | "scripts": { 8 | "prepare": "npm run build", 9 | "build": "gulp build", 10 | "test": "tslint --project tsconfig.json src/**", 11 | "docs": "gulp docs" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/appellation/handles.git" 16 | }, 17 | "keywords": [ 18 | "discord", 19 | "discord.js", 20 | "command handler" 21 | ], 22 | "author": "Will Nelson ", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/appellation/handles/issues" 26 | }, 27 | "homepage": "https://github.com/appellation/handles#readme", 28 | "dependencies": { 29 | "tsubaki": "^1.2.0" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^8.0.19", 33 | "del": "^3.0.0", 34 | "discord.js": "^11.2.1", 35 | "dotenv": "^4.0.0", 36 | "gulp": "^3.9.1", 37 | "gulp-sourcemaps": "^2.6.1", 38 | "gulp-typedoc": "^2.0.3", 39 | "gulp-typescript": "^3.2.1", 40 | "raven": "^2.1.0", 41 | "tslint": "^5.5.0", 42 | "typedoc": "^0.8.0", 43 | "typescript": "^2.4.1" 44 | }, 45 | "peerDependencies": { 46 | "discord.js": "^11.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/Client.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter = require('events'); 2 | import BaseError from '../errors/BaseError'; 3 | import Validator from '../middleware/Validator'; 4 | 5 | import Command from '../structures/Command'; 6 | import Response from '../structures/Response'; 7 | 8 | import CommandHandler from './CommandHandler'; 9 | import CommandRegistry from './CommandRegistry'; 10 | 11 | import { IConfig } from '../interfaces/Config'; 12 | 13 | import { Client, Message } from 'discord.js'; 14 | 15 | /** 16 | * The starting point for using handles. 17 | * 18 | * ```js 19 | * const discord = require('discord.js'); 20 | * const handles = require('discord-handles'); 21 | * 22 | * const client = new discord.Client(); 23 | * const handler = new handles.Client(); 24 | * 25 | * client.on('message', handler.handle); 26 | * client.login('token'); 27 | * ``` 28 | */ 29 | export default class HandlesClient extends EventEmitter { 30 | public readonly registry: CommandRegistry; 31 | public readonly handler: CommandHandler; 32 | 33 | public Response: typeof Response; 34 | public argsSuffix?: string; 35 | public readonly prefixes: Set; 36 | 37 | constructor(client: Client, config: IConfig = {}) { 38 | super(); 39 | 40 | this.Response = Response; 41 | this.argsSuffix = config.argsSuffix; 42 | this.prefixes = config.prefixes || new Set(); 43 | 44 | this.registry = new CommandRegistry(this, config); 45 | this.handler = new CommandHandler(this, config); 46 | 47 | this.handle = this.handle.bind(this); 48 | 49 | client.once('ready', () => this.prefixes.add(`<@${client.user.id}>`).add(`<@!${client.user.id}>`)); 50 | if (!('autoListen' in config) || !config.autoListen) client.on('message', this.handle); 51 | } 52 | 53 | /** 54 | * Handle a message as a command. 55 | * 56 | * ```js 57 | * const client = new discord.Client(); 58 | * const handler = new handles.Client(); 59 | * 60 | * client.on('message', handler.handle); 61 | * 62 | * // or 63 | * 64 | * const client = new discord.Client(); 65 | * const handler = new handles.Client(); 66 | * 67 | * client.on('message', message => { 68 | * // do other stuff 69 | * handler.handle(message); 70 | * }); 71 | * ``` 72 | */ 73 | public async handle(msg: Message) { 74 | if ( 75 | msg.webhookID || 76 | msg.system || 77 | msg.author.bot || 78 | (!msg.client.user.bot && msg.author.id !== msg.client.user.id) 79 | ) return null; 80 | 81 | const cmd = await this.handler.resolve(msg); 82 | if (!cmd) { 83 | this.emit('commandUnknown', msg); 84 | return null; 85 | } 86 | 87 | return this.handler.exec(cmd); 88 | } 89 | 90 | public on(event: 'commandStarted' | 'commandUnknown', listener: (cmd: Command) => void): this; 91 | 92 | public on(event: 'commandError', listener: 93 | ({ command, error }: { command: Command, error: Error | BaseError }) => void): this; 94 | 95 | public on(event: 'commandFinished', listener: 96 | ({ command, result }: { command: Command, result: any }) => void): this; 97 | 98 | public on(event: 'commandFailed', listener: 99 | ({ command, error }: { command: Command, error: BaseError }) => void): this; 100 | 101 | public on(event: 'commandsLoaded', listener: 102 | ({ commands, failed, time }: { commands: Map, failed: string[], time: number }) => void): this; 103 | 104 | public on(event: string, listener: (...args: any[]) => void): this { 105 | return super.on(event, listener); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/core/CommandHandler.ts: -------------------------------------------------------------------------------- 1 | import BaseError from '../errors/BaseError'; 2 | 3 | import Command from '../structures/Command'; 4 | 5 | import HandlesClient from './Client'; 6 | import CommandRegistry from './CommandRegistry'; 7 | 8 | import { IConfig } from '../interfaces/Config'; 9 | 10 | import { Message } from 'discord.js'; 11 | 12 | export type MessageValidator = (m: Message) => string | null | Promise; 13 | 14 | /** 15 | * Class for handling a command. 16 | */ 17 | export default class CommandHandler { 18 | 19 | /** 20 | * The client. 21 | */ 22 | public readonly handles: HandlesClient; 23 | 24 | /** 25 | * Sessions to ignore. 26 | */ 27 | public readonly ignore: string[] = []; 28 | 29 | /** 30 | * Whether the [[handle]] method should always resolve with void (use when relying on events to catch errors). 31 | */ 32 | public silent: boolean; 33 | 34 | /** 35 | * The message validator from config. 36 | */ 37 | public validator: MessageValidator; 38 | 39 | /** 40 | * Methods to run before each command. Executed in sequence before the command's `pre` method. 41 | * **Deprecated:** the command parameter will be removed. 42 | */ 43 | public pre: Array<(this: Command, cmd: Command) => any> = []; 44 | 45 | /** 46 | * Methods to run after each command. Executed in sequence after the command's `post` method. 47 | * **Deprecated:** the command parameter will be removed. 48 | */ 49 | public post: Array<(this: Command, cmd: Command) => any> = []; 50 | 51 | /** 52 | * Recently executed commands. Stored regardless of success or failure. 53 | */ 54 | public executed: Command[] = []; 55 | 56 | constructor(handles: HandlesClient, config: IConfig) { 57 | this.handles = handles; 58 | this.silent = typeof config.silent === 'undefined' ? true : config.silent; 59 | 60 | if ( 61 | typeof config.validator !== 'function' && 62 | (!this.handles.prefixes || !this.handles.prefixes.size) 63 | ) throw new Error('Unable to validate commands: no validator or prefixes were provided.'); 64 | 65 | this.validator = config.validator || ((message) => { 66 | for (const p of this.handles.prefixes) { 67 | if (message.content.startsWith(p)) { 68 | return message.content.substring(p.length).trim(); 69 | } 70 | } 71 | 72 | return null; 73 | }); 74 | } 75 | 76 | /** 77 | * Resolve a command from a message. 78 | */ 79 | public async resolve(message: Message): Promise { 80 | const content = await this.validator(message); 81 | if (typeof content !== 'string' || !content) return null; 82 | 83 | const match = content.match(/^([^\s]+)(.*)/); 84 | if (match) { 85 | const [, cmd, commandContent] = match; 86 | const mod = this.handles.registry.get(cmd); 87 | 88 | if (mod) { 89 | return new mod(this.handles, { 90 | body: commandContent.trim(), 91 | message, 92 | trigger: cmd, 93 | }); 94 | } 95 | } 96 | 97 | for (const [trigger, command] of this.handles.registry) { 98 | let body = null; 99 | if (trigger instanceof RegExp) { 100 | const match = content.match(trigger); 101 | if (match) body = match[0].trim(); 102 | } else if (typeof trigger === 'string') { 103 | // if the trigger is lowercase, make the command case-insensitive 104 | if ((trigger.toLowerCase() === trigger ? content.toLowerCase() : content).startsWith(trigger)) { 105 | body = content.substring(trigger.length).trim(); 106 | } 107 | } 108 | 109 | if (body !== null) { 110 | const cmd = new command(this.handles, { 111 | body, 112 | message, 113 | trigger, 114 | }); 115 | 116 | if (!this.ignore.includes(cmd.session)) return cmd; 117 | } 118 | } 119 | 120 | return null; 121 | } 122 | 123 | /** 124 | * Execute a command message. 125 | */ 126 | public async exec(cmd: Command): Promise { 127 | this._ignore(cmd.session); 128 | this.handles.emit('commandStarted', cmd); 129 | 130 | try { 131 | // TODO: Remove the param on global pre and post for v8 132 | for (const fn of this.pre) await fn.call(cmd, cmd); 133 | await cmd.pre.call(cmd); 134 | const result = await cmd.exec.call(cmd); 135 | await cmd.post.call(cmd); 136 | for (const fn of this.post) await fn.call(cmd, cmd); 137 | 138 | this.handles.emit('commandFinished', { command: cmd, result }); 139 | return this.silent ? result : undefined; 140 | } catch (e) { 141 | try { 142 | await cmd.error(); 143 | } catch (e) { 144 | // do nothing 145 | } 146 | 147 | if (e instanceof BaseError) { 148 | this.handles.emit('commandFailed', { command: cmd, error: e }); 149 | if (!this.silent) return e; 150 | } else { 151 | this.handles.emit('commandError', { command: cmd, error: e }); 152 | if (!this.silent) throw e; 153 | } 154 | } finally { 155 | this.executed.push(cmd); 156 | setTimeout(() => this.executed.splice(this.executed.indexOf(cmd), 1), 60 * 60 * 1000); 157 | 158 | this._unignore(cmd.session); 159 | } 160 | } 161 | 162 | /** 163 | * Ignore something (designed for [[CommandMessage#session]]). 164 | * @param session The data to ignore. 165 | */ 166 | private _ignore(session: string) { 167 | this.ignore.push(session); 168 | } 169 | 170 | /** 171 | * Stop ignoring something (designed for [[CommandMessage#session]]). 172 | * @param session The data to unignore. 173 | */ 174 | private _unignore(session: string) { 175 | const index = this.ignore.indexOf(session); 176 | if (index > -1) this.ignore.splice(index, 1); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/core/CommandRegistry.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'tsubaki'; 2 | 3 | import { IConfig } from '../interfaces/Config'; 4 | import Command, { Trigger } from '../structures/Command'; 5 | import HandlesClient from './Client'; 6 | 7 | import fs = require('fs'); 8 | import path = require('path'); 9 | 10 | const readdir: (dir: string) => Promise = promisify(fs.readdir); 11 | const stat: (path: string) => Promise = promisify(fs.stat); 12 | 13 | /** 14 | * Manage command loading. 15 | */ 16 | export default class CommandRegistry extends Map { 17 | 18 | /** 19 | * Get all the file paths recursively in a directory. 20 | * @param dir The directory to start at. 21 | */ 22 | private static async _loadDir(dir: string): Promise { 23 | const files = await readdir(dir); 24 | const list: string[] = []; 25 | 26 | await Promise.all(files.map(async (f) => { 27 | const currentPath = path.join(dir, f); 28 | const stats = await stat(currentPath); 29 | 30 | if (stats.isFile() && path.extname(currentPath) === '.js') { 31 | list.push(currentPath); 32 | } else if (stats.isDirectory()) { 33 | const files = await this._loadDir(currentPath); 34 | list.push(...files); 35 | } 36 | })); 37 | 38 | return list; 39 | } 40 | 41 | /** 42 | * Handles client. 43 | */ 44 | public readonly handles: HandlesClient; 45 | 46 | /** 47 | * The directory from which to load commands. 48 | */ 49 | public directory: string; 50 | 51 | constructor(handles: HandlesClient, config: IConfig) { 52 | super(); 53 | 54 | this.handles = handles; 55 | this.directory = config.directory || './commands'; 56 | 57 | this.load(); 58 | } 59 | 60 | /** 61 | * Load all commands into memory. Use when reloading commands. 62 | */ 63 | public async load(): Promise { 64 | const start = Date.now(); 65 | 66 | this.clear(); 67 | const files = await CommandRegistry._loadDir(this.directory); 68 | 69 | const failed = []; 70 | for (const file of files) { 71 | let mod; 72 | const location = path.resolve(process.cwd(), file); 73 | 74 | try { 75 | delete require.cache[require.resolve(location)]; 76 | mod = require(location); 77 | } catch (e) { 78 | failed.push(file); 79 | console.error(e); // tslint:disable-line no-console 80 | continue; 81 | } 82 | 83 | if (typeof mod.default !== 'undefined') mod = mod.default; 84 | 85 | // if triggers are iterable 86 | if (Array.isArray(mod.triggers)) { 87 | for (const trigger of mod.triggers) this.set(trigger, mod); 88 | } else if (typeof mod.triggers === 'undefined') { // if no triggers are provided 89 | this.set(path.basename(file, '.js'), mod); 90 | } else { 91 | this.set(mod.triggers, mod); 92 | } 93 | } 94 | 95 | this.handles.emit('commandsLoaded', { commands: this, failed, time: Date.now() - start }); 96 | return this; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/errors/ArgumentError.ts: -------------------------------------------------------------------------------- 1 | import Argument from '../middleware/Argument'; 2 | import BaseError from './BaseError'; 3 | 4 | /** 5 | * Used to represent a user error with collecting arguments. 6 | */ 7 | export default class ArgumentError extends BaseError { 8 | /** 9 | * The argument that has errored. 10 | */ 11 | public argument: Argument; 12 | 13 | constructor(arg: Argument, reason: string) { 14 | super(reason); 15 | this.argument = arg; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/errors/BaseError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The base error class. Used to represent user input errors as opposed to logic failures. 3 | */ 4 | export default class BaseError { 5 | /** 6 | * The error message. 7 | */ 8 | public message: string; 9 | 10 | constructor(message: string) { 11 | this.message = message; 12 | } 13 | 14 | public toString() { 15 | return this.message; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import Validator from '../middleware/Validator'; 2 | import BaseError from './BaseError'; 3 | 4 | /** 5 | * Used to represent a user error with validation (e.g. the command was invalid). 6 | */ 7 | export default class ValidationError extends BaseError { 8 | /** 9 | * The validator that errored. 10 | */ 11 | public validator: Validator; 12 | 13 | constructor(validator: Validator) { 14 | super(validator.reason || 'Validation failed.'); 15 | this.validator = validator; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Client from './core/Client'; 2 | import CommandHandler from './core/CommandHandler'; 3 | import CommandRegistry from './core/CommandRegistry'; 4 | 5 | import Command from './structures/Command'; 6 | import Response from './structures/Response'; 7 | 8 | import Argument from './middleware/Argument'; 9 | import Validator from './middleware/Validator'; 10 | 11 | export * from './interfaces/Config'; 12 | 13 | export { 14 | Argument, 15 | Client, 16 | Command, 17 | CommandHandler, 18 | CommandRegistry, 19 | Response, 20 | Validator, 21 | }; 22 | -------------------------------------------------------------------------------- /src/interfaces/Config.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from 'discord.js'; 2 | import { MessageValidator } from '../core/CommandHandler'; 3 | import Response from '../structures/Response'; 4 | 5 | export interface IConfig { 6 | /** 7 | * Command prefixes (will not be used if a [[validator]] is provided). 8 | */ 9 | prefixes?: Set; 10 | 11 | /** 12 | * A global setting for configuring the argument suffix. 13 | */ 14 | argsSuffix?: string; 15 | 16 | /** 17 | * Directory to load commands from. Default to `./commands` relative to the cwd. 18 | */ 19 | directory?: string; 20 | 21 | /** 22 | * This will get run on every message. Use to manually determine whether a message is a command. 23 | */ 24 | validator?: MessageValidator; 25 | 26 | /** 27 | * Set to false to resolve/reject the command handling process properly. When true, 28 | * the command handler promise will always resolve with void (you should use the events 29 | * [[HandlesClient#commandFailed]] and [[HandlesClient#commandFinished]] to check completion status in this 30 | * case). 31 | */ 32 | silent?: boolean; 33 | 34 | /** 35 | * Whether to automatically listen for commands. If you specify this as false, you'll have to 36 | * `handles.handle(message)` in your message listener. 37 | */ 38 | autoListen?: boolean; 39 | } 40 | -------------------------------------------------------------------------------- /src/middleware/Argument.ts: -------------------------------------------------------------------------------- 1 | import HandlesClient from '../core/Client'; 2 | import ArgumentError from '../errors/ArgumentError'; 3 | import Command from '../structures/Command'; 4 | import Response from '../structures/Response'; 5 | 6 | import { Message } from 'discord.js'; 7 | 8 | /** 9 | * This is called every time new potential argument data is received, either in the body of 10 | * the original command or in subsequent prompts. 11 | */ 12 | export type Resolver = (content: string, message: Message, arg: Argument) => T | null; 13 | 14 | export interface IOptions { 15 | prompt?: string; 16 | rePrompt?: string; 17 | optional?: boolean; 18 | resolver?: Resolver; 19 | timeout?: number; 20 | pattern?: RegExp; 21 | suffix?: string | null; 22 | } 23 | 24 | /** 25 | * This function takes a string which contains any number of arguments and returns the first of them. 26 | * The return should be a substring of the input, which will then be removed from the input string. The remaining 27 | * input will be fed back into this function for the next argument, etc. until no more arguments remain. Use this 28 | * to determine whether an argument *exists*; use [[Argument#resolver]] to determine if the argument is *valid*. 29 | */ 30 | export type Matcher = (content: string) => string; 31 | 32 | /** 33 | * Represents a command argument. 34 | */ 35 | export default class Argument implements IOptions, Promise { 36 | public readonly command: Command; 37 | 38 | /** 39 | * The key that this arg will be set to. 40 | * 41 | * ```js 42 | * // in args definition 43 | * new Argument('thing'); 44 | * 45 | * // in command execution 46 | * const thingData = command.args.thing; 47 | * ``` 48 | */ 49 | public key: string; 50 | 51 | /** 52 | * The initial prompt text of this argument. 53 | */ 54 | public prompt: string; 55 | 56 | /** 57 | * Text sent for re-prompting to provide correct input when provided input is not resolved 58 | * (ie. the resolver returns null). 59 | */ 60 | public rePrompt: string; 61 | 62 | /** 63 | * Whether this argument is optional. 64 | */ 65 | public optional: boolean = false; 66 | 67 | /** 68 | * The argument resolver for this argument. 69 | */ 70 | public resolver: Resolver; 71 | 72 | /** 73 | * How long to wait for a response to a prompt, in seconds. 74 | */ 75 | public timeout: number; 76 | 77 | /** 78 | * Text to append to each prompt. Defaults to global setting or built-in text. 79 | */ 80 | public suffix: string | null; 81 | 82 | /** 83 | * The matcher for this argument. 84 | */ 85 | public matcher: Matcher; 86 | 87 | /** 88 | * The raw content matching regex. 89 | */ 90 | private _pattern: RegExp; 91 | 92 | constructor(command: Command, key: string, { 93 | prompt = '', 94 | rePrompt = '', 95 | optional = false, 96 | timeout = 30, 97 | suffix = null, 98 | pattern = /^\S+/, 99 | }: IOptions = {}) { 100 | this.command = command; 101 | this.key = key; 102 | 103 | this.prompt = prompt; 104 | this.rePrompt = rePrompt; 105 | this.optional = optional; 106 | this.timeout = timeout; 107 | this.suffix = suffix; 108 | this.pattern = pattern; 109 | } 110 | 111 | get handles(): HandlesClient { 112 | return this.command.handles; 113 | } 114 | 115 | /** 116 | * A regex describing the pattern of arguments. Defaults to single words. If more advanced matching 117 | * is required, set a custom [[matcher]] instead. Can pull arguments from anywhere in the unresolved 118 | * content, so make sure to specify `^` if you want to pull from the front. 119 | */ 120 | get pattern() { 121 | return this._pattern; 122 | } 123 | 124 | set pattern(regex) { 125 | this._pattern = regex; 126 | 127 | this.matcher = (content) => { 128 | const m = content.match(regex); 129 | return m === null ? '' : m[0]; 130 | }; 131 | } 132 | 133 | /** 134 | * Make this argument take up the rest of the words in the command. Any remaining required arguments 135 | * will be prompted for. 136 | */ 137 | public setInfinite() { 138 | return this.setPattern(/.*/); 139 | } 140 | 141 | /** 142 | * Set the pattern for matching args strings. 143 | */ 144 | public setPattern(pattern: RegExp) { 145 | this.pattern = pattern; 146 | return this; 147 | } 148 | 149 | /** 150 | * Set the prompt for the argument. 151 | */ 152 | public setPrompt(prompt: string = '') { 153 | this.prompt = prompt; 154 | return this; 155 | } 156 | 157 | /** 158 | * Set the re-prompt for the argument. 159 | */ 160 | public setRePrompt(rePrompt: string = '') { 161 | this.rePrompt = rePrompt; 162 | return this; 163 | } 164 | 165 | /** 166 | * Set whether the argument is optional. 167 | */ 168 | public setOptional(optional: boolean = true) { 169 | this.optional = optional; 170 | return this; 171 | } 172 | 173 | /** 174 | * Set the argument resolver function for this argument. 175 | */ 176 | public setResolver(resolver: Resolver) { 177 | this.resolver = resolver; 178 | return this; 179 | } 180 | 181 | /** 182 | * Set the time to wait for a prompt response (in seconds). 183 | */ 184 | public setTimeout(time: number = 30) { 185 | this.timeout = time; 186 | return this; 187 | } 188 | 189 | /** 190 | * Set the suffix for all prompts. 191 | */ 192 | public setSuffix(text: string = '') { 193 | this.suffix = text; 194 | return this; 195 | } 196 | 197 | public then( 198 | resolver?: ((value: T | null) => TResult1 | PromiseLike), 199 | rejector?: ((value: Error) => TResult2 | PromiseLike), 200 | ): Promise { 201 | return new Promise(async (resolve, reject) => { 202 | const matched = this.matcher(this.command.body); 203 | this.command.body = this.command.body.replace(matched, '').trim(); 204 | 205 | const resolver = this.resolver || ((c: string) => c); 206 | let resolved = !matched ? null : await resolver(matched, this.command.message, this); 207 | 208 | // if there is no matched content and the argument is not optional, collect a prompt 209 | if (resolved === null && !this.optional) { 210 | try { 211 | resolved = await this.collectPrompt(matched.length === 0); 212 | } catch (e) { 213 | this.command.response.send('Command cancelled.'); 214 | if (e instanceof Map && !e.size) e = new ArgumentError(this, 'time'); 215 | return reject(e); 216 | } 217 | } 218 | 219 | if (!this.command.args) this.command.args = {}; 220 | this.command.args[this.key] = resolved; 221 | 222 | return resolve(resolved); 223 | }).then(resolver, rejector); 224 | } 225 | 226 | public catch(rejector?: ((value: Error) => TResult2 | PromiseLike)) { 227 | return this.then(undefined, rejector); 228 | } 229 | 230 | public get [Symbol.toStringTag](): 'Promise' { 231 | return 'Promise'; 232 | } 233 | 234 | private async collectPrompt(first = true): Promise { 235 | const text = first ? this.prompt : this.rePrompt; 236 | const suffix = this.suffix || this.handles.argsSuffix || 237 | `\nCommand will be cancelled in **${this.timeout} seconds**. Type \`cancel\` to cancel immediately.`; 238 | 239 | // get first response 240 | const prompt = new this.handles.Response(this.command.message); 241 | await prompt.send(text + suffix); 242 | const responses = await prompt.channel.awaitMessages( 243 | (m: Message) => m.author.id === this.command.author.id, 244 | { time: this.timeout * 1000, max: 1, errors: ['time'] }, 245 | ); 246 | const response = responses.first(); 247 | 248 | // cancel 249 | if (response.content === 'cancel') throw new ArgumentError(this, 'cancelled'); 250 | 251 | // resolve: if not, prompt again 252 | const resolved = await this.resolver(response.content, response, this); 253 | if (resolved === null) return this.collectPrompt(false); 254 | 255 | return resolved; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/middleware/Validator.ts: -------------------------------------------------------------------------------- 1 | import ValidationError from '../errors/ValidationError'; 2 | import Command from '../structures/Command'; 3 | 4 | /** 5 | * ```js 6 | * validator.apply(cmd => cmd.message.author.id === 'some id', 'uh oh'); // executed at runtime 7 | * // or 8 | * validator.apply(cmd.message.author.id === 'some id', 'uh oh'); // executed immediately 9 | * ``` 10 | */ 11 | export type ValidationFunction = (v: Validator) => boolean; 12 | 13 | /** 14 | * Passed as a parameter to command validators. Arguments will not be available in this class, 15 | * as this is run before arguments are resolved from the command. Use for permissions checks and 16 | * other pre-command validations. 17 | * 18 | * ```js 19 | * // Using a custom validator. 20 | * class CustomValidator extends Validator { 21 | * ensureGuild() { 22 | * return this.apply(this.command.message.channel.type === 'text', 'Command must be run in a guild channel.'); 23 | * } 24 | * } 25 | * 26 | * // Usage in command 27 | * exports.validator = processor => { 28 | * return processor.ensureGuild(); 29 | * } 30 | * ``` 31 | * 32 | * ```js 33 | * // Usage without a custom validator 34 | * exports.validator = (processor, command) => { 35 | * return processor.apply(command.message.channel.type === 'text', 'Command must be run in a guild channel.'); 36 | * } 37 | * ``` 38 | */ 39 | export default class Validator { 40 | public command: Command; 41 | 42 | /** 43 | * The reason this validator is invalid. 44 | */ 45 | public reason: string | null = null; 46 | 47 | /** 48 | * Whether to automatically respond with reason when invalid. 49 | */ 50 | public respond: boolean = true; 51 | 52 | /** 53 | * Whether this validator is valid. 54 | */ 55 | public valid = true; 56 | 57 | /** 58 | * Functions to execute when determining validity. Maps validation functions to reasons. 59 | */ 60 | private exec: Map = new Map(); 61 | 62 | constructor(cmd: Command) { 63 | this.command = cmd; 64 | } 65 | 66 | /** 67 | * Test a new boolean for validity. 68 | * 69 | * ```js 70 | * const validator = new Validator(); 71 | * validator.apply(aCondition, 'borke') || validator.apply(otherCondition, 'different borke'); 72 | * yield validator; 73 | * ``` 74 | */ 75 | public apply(test: ValidationFunction | boolean, reason: string | null = null) { 76 | this.exec.set(typeof test === 'function' ? test : () => test, reason); 77 | return this; 78 | } 79 | 80 | public then( 81 | resolver?: ((value: void) => TResult1 | PromiseLike), 82 | rejector?: ((value: Error) => TResult2 | PromiseLike), 83 | ): Promise { 84 | return new Promise((resolve, reject) => { 85 | for (const [test, reason] of this.exec) { 86 | try { 87 | if (!test(this)) { 88 | this.reason = reason; 89 | this.valid = false; 90 | throw new ValidationError(this); 91 | } 92 | } catch (e) { 93 | if (this.respond) this.command.response.error(e); 94 | return reject(e); 95 | } 96 | } 97 | 98 | return resolve(); 99 | }).then(resolver, rejector); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/structures/Command.ts: -------------------------------------------------------------------------------- 1 | import HandlesClient from '../core/Client'; 2 | 3 | import { IConfig } from '../interfaces/Config'; 4 | import Response, { TextBasedChannel } from './Response'; 5 | 6 | import { Client, Guild, GuildMember, Message, User } from 'discord.js'; 7 | 8 | export type Trigger = string | RegExp; 9 | 10 | export interface ICommandOptions { 11 | trigger: Trigger; 12 | message: Message; 13 | body: string; 14 | } 15 | 16 | /** 17 | * A command. 18 | * ```js 19 | * const { Command } = require('discord-handles'); 20 | * module.exports = class extends Command { 21 | * static get triggers() { 22 | * return ['ping', 'pung', 'poing', 'pong']; 23 | * } 24 | * 25 | * exec() { 26 | * return this.response.success(`${this.trigger} ${Date.now() - this.message.createdTimestamp}ms`); 27 | * } 28 | * }; 29 | */ 30 | export default class Command { 31 | public static triggers?: Trigger | Trigger[]; 32 | 33 | /** 34 | * The handles client. 35 | */ 36 | public readonly handles: HandlesClient; 37 | 38 | /** 39 | * The command trigger that caused the message to run this command. 40 | */ 41 | public readonly trigger: Trigger; 42 | 43 | /** 44 | * The message that triggered this command. 45 | */ 46 | public readonly message: Message; 47 | 48 | /** 49 | * The body of the command (without prefix or command), as provided in the original message. 50 | */ 51 | public body: string; 52 | 53 | /** 54 | * Client config. 55 | */ 56 | public config: IConfig; 57 | 58 | /** 59 | * The command arguments as set by arguments in executor. 60 | */ 61 | public args?: any; 62 | 63 | /** 64 | * The response object for this command. 65 | */ 66 | public response: Response; 67 | 68 | constructor(client: HandlesClient, { trigger, message, body }: ICommandOptions) { 69 | this.handles = client; 70 | this.trigger = trigger; 71 | this.message = message; 72 | this.body = body; 73 | this.args = null; 74 | this.response = new this.handles.Response(this.message); 75 | } 76 | 77 | /** 78 | * The Discord.js client. 79 | */ 80 | get client(): Client { 81 | return this.message.client; 82 | } 83 | 84 | /** 85 | * The guild this command is in. 86 | */ 87 | get guild(): Guild { 88 | return this.message.guild; 89 | } 90 | 91 | /** 92 | * The channel this command is in. 93 | */ 94 | get channel(): TextBasedChannel { 95 | return this.message.channel; 96 | } 97 | 98 | /** 99 | * The author of this command. 100 | */ 101 | get author(): User { 102 | return this.message.author; 103 | } 104 | 105 | get member(): GuildMember { 106 | return this.message.member; 107 | } 108 | 109 | /** 110 | * Ensure unique commands for an author in a channel. 111 | * Format: "authorID:channelID" 112 | */ 113 | get session() { 114 | return `${this.message.author.id}:${this.message.channel.id}`; 115 | } 116 | 117 | /** 118 | * Executed prior to {@link Command#exec}. Should be used for middleware/validation. 119 | * ```js 120 | * async pre() { 121 | * await new handles.Argument(this, 'someArgument') 122 | * .setResolver(c => c === 'dank memes' ? 'top kek' : null); 123 | * } 124 | */ 125 | public pre() { 126 | // implemented by command 127 | } 128 | 129 | /** 130 | * The command execution method 131 | */ 132 | public exec() { 133 | // implemented by command 134 | } 135 | 136 | /** 137 | * Executed after {@link Command#exec}. Can be used for responses. 138 | */ 139 | public post() { 140 | // implemented by command 141 | } 142 | 143 | /** 144 | * Executed when any of the command execution methods error. Any errors here will be discarded. 145 | */ 146 | public error() { 147 | // implemented by command 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/structures/Response.ts: -------------------------------------------------------------------------------- 1 | import Queue from '../util/Queue'; 2 | 3 | import { DMChannel, GroupDMChannel, Message, MessageOptions, TextBasedChannel, TextChannel } from 'discord.js'; 4 | 5 | export type TextBasedChannel = TextChannel | DMChannel | GroupDMChannel; 6 | 7 | export type SentResponse = Message | Message[]; 8 | export interface IResponseOptions extends MessageOptions { 9 | /** 10 | * Whether to catch all rejections when sending. The promise will always resolve when this option 11 | * is enabled; if there is an error, the resolution will be undefined. 12 | */ 13 | catchall?: boolean; 14 | 15 | /** 16 | * Whether to send a new message regardless of any prior responses. 17 | */ 18 | force?: boolean; 19 | } 20 | 21 | export type Send = ( 22 | data: string | IResponseOptions, 23 | options?: IResponseOptions, 24 | ) => Promise; 25 | 26 | /** 27 | * Send responses to a message. 28 | */ 29 | export default class Response { 30 | 31 | /** 32 | * The message to respond to. 33 | */ 34 | public message: Message; 35 | 36 | /** 37 | * Whether to edit previous responses. 38 | */ 39 | public edit: boolean = true; 40 | 41 | /** 42 | * The channel to send responses in. 43 | */ 44 | public channel: TextBasedChannel; 45 | 46 | /** 47 | * The message to edit, if enabled. 48 | */ 49 | public responseMessage?: Message | null; 50 | 51 | /** 52 | * The queue of response jobs. 53 | */ 54 | private readonly _q: Queue; 55 | 56 | /** 57 | * @param message The message to respond to. 58 | * @param edit Whether to edit previous responses. 59 | */ 60 | constructor(message: Message, edit: boolean = true) { 61 | this.message = message; 62 | this.channel = message.channel; 63 | this.edit = edit; 64 | this.responseMessage = null; 65 | this._q = new Queue(); 66 | } 67 | 68 | /** 69 | * Send a message using the Discord.js `Message.send` method. If a prior 70 | * response has been sent, it will edit that unless the `force` parameter 71 | * is set. Automatically attempts to fallback to DM responses. You can 72 | * send responses without waiting for prior responses to succeed. 73 | * @param data The data to send 74 | * @param options Message options. 75 | * @param messageOptions Discord.js message options. 76 | */ 77 | public send: Send = (data, options = {}, ...extra: IResponseOptions[]) => { 78 | options = Object.assign(options, ...extra); 79 | return new Promise((resolve, reject) => { 80 | this._q.push(async (): Promise => { 81 | function success(m?: SentResponse): void { 82 | resolve(m); 83 | } 84 | 85 | function error(e: Error): void { 86 | if (options.catchall) return success(); 87 | reject(e); 88 | } 89 | 90 | if (this.responseMessage && this.edit && !options.force) { 91 | await this.responseMessage.edit(data, options).then(success, error); 92 | } else { 93 | await this.channel.send(data, options).then((m) => { 94 | if (Array.isArray(m)) this.responseMessage = m[0]; 95 | else this.responseMessage = m; 96 | return success(m); 97 | }, () => { 98 | if (this.channel.type === 'text') { 99 | return this.message.author.send(data, options).then(success, error); 100 | } 101 | }); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | public error: Send = (data, ...options: IResponseOptions[]) => { 108 | return this.send(`\`❌\` | ${data}`, ...options); 109 | } 110 | 111 | public success: Send = (data, ...options: IResponseOptions[]) => { 112 | return this.send(`\`✅\` | ${data}`, ...options); 113 | } 114 | 115 | public dm: Send = async (data, ...options: IResponseOptions[]) => { 116 | this.channel = this.message.author.dmChannel || await this.message.author.createDM(); 117 | return this.send(data, ...options); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/util/Queue.ts: -------------------------------------------------------------------------------- 1 | export type QueueFunction = (...args: any[]) => Promise; 2 | 3 | export default class Queue extends Array { 4 | private _started: boolean; 5 | 6 | constructor() { 7 | super(); 8 | Object.defineProperty(this, '_started', { writable: true, value: false }); 9 | 10 | return new Proxy(this, { 11 | set(target, prop: any, value: QueueFunction) { 12 | target[prop] = value; 13 | if (!isNaN(prop)) target.start(); 14 | return true; 15 | }, 16 | }); 17 | } 18 | 19 | public async start() { 20 | if (this._started) return; 21 | this._started = true; 22 | 23 | while (this.length) { 24 | const func = this.shift(); 25 | if (func) await func(); 26 | } 27 | 28 | this._started = false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/commands/add.js: -------------------------------------------------------------------------------- 1 | const Handles = require('../../dist/index'); 2 | 3 | class Command extends Handles.Command { 4 | async pre() { 5 | // const val1 = new Handles.Validator(this) 6 | // .apply(false, 'kek'); 7 | const val2 = new Handles.Validator(this) 8 | .apply(true, 'lol'); 9 | 10 | // await val1; 11 | 12 | await new Handles.Argument(this, 'first') 13 | .setPrompt('Please provide the first digit.') 14 | .setRePrompt('xd1') 15 | .setResolver(c => isNaN(c) ? null : parseInt(c)); 16 | 17 | await new Handles.Argument(this, 'second') 18 | .setPrompt('Please provide the second digit.') 19 | .setRePrompt('xd2') 20 | .setResolver(c => isNaN(c) ? null : parseInt(c)); 21 | } 22 | 23 | exec() { 24 | return this.response.send(this.args.first + this.args.second); 25 | } 26 | } 27 | 28 | module.exports = Command; 29 | -------------------------------------------------------------------------------- /test/commands/dm.js: -------------------------------------------------------------------------------- 1 | exports.exec = (cmd) => cmd.response.dm('stuff'); 2 | -------------------------------------------------------------------------------- /test/commands/error.js: -------------------------------------------------------------------------------- 1 | exports.exec = () => { 2 | throw new Error('failure!'); 3 | }; 4 | -------------------------------------------------------------------------------- /test/commands/eval.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const { Command, Argument } = require('../../dist'); 3 | 4 | module.exports = class extends Command { 5 | async pre() { 6 | await new Argument(this, 'code') 7 | .setResolver(c => c || null) 8 | .setPrompt('What would you like to eval?') 9 | .setRePrompt('That\'s not valid.'); 10 | } 11 | 12 | async exec() { 13 | try { 14 | this.response.send(util.inspect(await eval(this.args.code)), undefined, { code: 'js' }); 15 | } catch (e) { 16 | this.response.error(e); 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /test/commands/invalid.js: -------------------------------------------------------------------------------- 1 | const handles = require('../../dist/index'); 2 | 3 | exports.middleware = function* () { 4 | yield new handles.Validator() 5 | .apply(false); 6 | }; 7 | -------------------------------------------------------------------------------- /test/commands/ping.js: -------------------------------------------------------------------------------- 1 | const Argument = require('../../dist/middleware/Argument').default; 2 | 3 | // const HTTPPing = require('node-http-ping') 4 | 5 | class PingCommand 6 | { 7 | exec(command) 8 | { 9 | if (!command.args.site) { 10 | return command.response.success(`${Math.round(command.message.client.ping)}ms 💓`) 11 | } 12 | 13 | HTTPPing(command.args[0]).then((time) => { 14 | return command.response.success(`Website **${command.args[0]}** responded in ${time}ms.`) 15 | }).catch(console.error) 16 | } 17 | 18 | * middleware() 19 | { 20 | yield new Argument('site') 21 | .setPrompt('plz provide websit') 22 | .setRePrompt('this is no link my fRIEND') 23 | .setOptional() 24 | .setResolver((content) => { 25 | if (!content.match(new RegExp(/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi))) { 26 | return null 27 | } 28 | 29 | return content 30 | }) 31 | } 32 | } 33 | 34 | module.exports = new PingCommand 35 | -------------------------------------------------------------------------------- /test/commands/spam.js: -------------------------------------------------------------------------------- 1 | exports.exec = cmd => { 2 | const s = cmd.response.send; 3 | s('d'); 4 | s('a'); 5 | s('n'); 6 | s('k'); 7 | }; 8 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // process.on('unhandledRejection', console.error); 2 | 3 | const path = require('path'); 4 | require('dotenv').config({ path: path.join(__dirname, '.env') }); 5 | // const raven = require('raven'); 6 | const discord = require('discord.js'); 7 | const handles = require('../dist/index'); 8 | 9 | // raven.config(process.env.raven, { 10 | // captureUnhandledRejections: true, 11 | // }).install(); 12 | 13 | const client = new discord.Client(); 14 | const handler = new handles.Client(client, { 15 | directory: path.join('test', 'commands'), 16 | prefixes: new Set(['x!']) 17 | }); 18 | 19 | handler.on('commandError', ({ command, error }) => { 20 | // const extra = { 21 | // message: { 22 | // content: command.message.content, 23 | // id: command.message.id, 24 | // type: command.message.type, 25 | // }, 26 | // channel: { 27 | // id: command.message.channel.id, 28 | // type: command.message.channel.type, 29 | // }, 30 | // guild: {}, 31 | // client: { 32 | // shard: command.client.shard ? command.client.shard.id : null, 33 | // ping: command.client.ping, 34 | // status: command.client.status, 35 | // }, 36 | // }; 37 | 38 | // if (command.message.channel.type === 'text') { 39 | // extra.guild = { 40 | // id: command.guild.id, 41 | // name: command.guild.name, 42 | // owner: command.guild.ownerID, 43 | // }; 44 | // } 45 | console.error(error); 46 | // console.error(extra); 47 | 48 | // console.log(raven.captureException(error, { 49 | // user: { 50 | // id: command.message.author.id, 51 | // username: command.message.author.tag, 52 | // }, 53 | // extra 54 | // })); 55 | }); 56 | 57 | client.once('ready', () => console.log('ready')); 58 | 59 | client.login(process.env.BOT_TOKEN); 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2015", 7 | "lib": [ 8 | "es2017" 9 | ], 10 | "declaration": true, 11 | "sourceMap": true, 12 | "removeComments": false 13 | }, 14 | "include": [ 15 | "./src/" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "indent": [true, "spaces", 2], 9 | "linebreak-style": [true, "LF"], 10 | "quotemark": [true, "single"], 11 | "semicolon": [true, "always"], 12 | "curly": [true, "ignore-same-line"], 13 | "whitespace": [true, "check-branch", "check-decl", "check-module", "check-separator", "check-type", "check-typecast", "check-preblock"], 14 | // "no-unused-variable": [true], broken for some reason 15 | "no-console": [true], 16 | "triple-equals": true, 17 | "no-invalid-this": true, 18 | "no-trailing-whitespace": true, 19 | "no-unused-variable": true, 20 | "prefer-const": [true], 21 | "prefer-template": true, 22 | "variable-name": [ 23 | "check-format", 24 | "allow-leading-underscore", 25 | "ban-keywords" 26 | ], 27 | "no-shadowed-variable": false 28 | }, 29 | "rulesDirectory": [] 30 | } 31 | --------------------------------------------------------------------------------