create-chapter 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/default/src/(moderation)/ban.ts +49 -0
- package/templates/default/src/(moderation)/checks.ts +19 -0
- package/templates/default/src/(moderation)/kick.ts +27 -0
- package/templates/default/src/(moderation)/messages.ts +81 -0
- package/templates/default/src/(moderation)/warn.ts +42 -0
- package/templates/default/src/commands/hello.ts +1 -0
- package/templates/default/src/commands/ping.ts +25 -7
- package/templates/default/src/commands/play.ts +1 -0
- package/templates/default/src/events/ready.ts +34 -0
- package/templates/default/src/events/wave.ts +14 -0
- package/templates/default/src/messages/en-US.ts +4 -1
- package/templates/default/src/messages/fr.ts +1 -0
- package/templates/default/src/components/buttons/again.ts +0 -19
- package/templates/default/src/events/reactionAdd/wave.ts +0 -11
- package/templates/default/src/events/ready/online.ts +0 -30
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { button, command } from 'chapterjs';
|
|
2
|
+
import { canPunish } from './checks';
|
|
3
|
+
|
|
4
|
+
// A folder is only a folder: `(moderation)` groups what moderation needs,
|
|
5
|
+
// and the framework reads what these files export, wherever they are.
|
|
6
|
+
// /ban asks for a confirmation, then bans. The command and its button live
|
|
7
|
+
// in the same file: the button is named by its export (`confirmBan`), and
|
|
8
|
+
// `who: 'author'` keeps it for the person who used the command.
|
|
9
|
+
export default command({
|
|
10
|
+
name: 'ban',
|
|
11
|
+
permissions: ['BanMembers'],
|
|
12
|
+
ephemeral: true,
|
|
13
|
+
options: {
|
|
14
|
+
target: { type: 'user', required: true },
|
|
15
|
+
reason: { type: 'string' },
|
|
16
|
+
days: { type: 'integer', min: 0, max: 7 },
|
|
17
|
+
},
|
|
18
|
+
async run({ interaction, options, member, guild, t }) {
|
|
19
|
+
const target = guild.members.get(options.target.id);
|
|
20
|
+
if (target && !canPunish(member, target)) {
|
|
21
|
+
await interaction.reply(t('cannotPunish', { user: target.displayName }));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await interaction.reply({
|
|
25
|
+
content: t('ban.confirm', { user: options.target.username }),
|
|
26
|
+
components: [
|
|
27
|
+
confirmBan({
|
|
28
|
+
userId: options.target.id,
|
|
29
|
+
days: options.days ?? 0,
|
|
30
|
+
reason: options.reason ?? '',
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const confirmBan = button({
|
|
38
|
+
label: ({ t }) => t('ban.yes'),
|
|
39
|
+
style: 'danger',
|
|
40
|
+
who: 'author',
|
|
41
|
+
data: { userId: 'string', days: 'number', reason: 'string' },
|
|
42
|
+
async run({ interaction, data, guild, user, t }) {
|
|
43
|
+
await guild.ban(data.userId, {
|
|
44
|
+
reason: data.reason || t('ban.defaultReason', { by: user.username }),
|
|
45
|
+
deleteMessageSeconds: data.days * 86_400,
|
|
46
|
+
});
|
|
47
|
+
await interaction.update({ content: t('ban.done'), components: [] });
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { GuildMember } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// Shared code: this file declares nothing, the commands import it. Nothing
|
|
4
|
+
// survives a restart: a real bot keeps its warnings in a database.
|
|
5
|
+
export function canPunish(by: GuildMember, target: GuildMember): boolean {
|
|
6
|
+
return by.highestRole.position > target.highestRole.position;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const warnings = new Map<string, number>();
|
|
10
|
+
|
|
11
|
+
export function addWarning(userId: string): number {
|
|
12
|
+
const count = (warnings.get(userId) ?? 0) + 1;
|
|
13
|
+
warnings.set(userId, count);
|
|
14
|
+
return count;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function warningsOf(userId: string): number {
|
|
18
|
+
return warnings.get(userId) ?? 0;
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { command } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// /kick: the texts come from the language files, like every command here.
|
|
4
|
+
import { canPunish } from './checks';
|
|
5
|
+
|
|
6
|
+
export default command({
|
|
7
|
+
name: 'kick',
|
|
8
|
+
permissions: ['KickMembers'],
|
|
9
|
+
ephemeral: true,
|
|
10
|
+
options: {
|
|
11
|
+
target: { type: 'user', required: true },
|
|
12
|
+
reason: { type: 'string' },
|
|
13
|
+
},
|
|
14
|
+
async run({ interaction, options, member, guild, t }) {
|
|
15
|
+
const target = guild.members.get(options.target.id);
|
|
16
|
+
if (!target) {
|
|
17
|
+
await interaction.reply(t('notHere', { user: options.target.username }));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!canPunish(member, target)) {
|
|
21
|
+
await interaction.reply(t('cannotPunish', { user: target.displayName }));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await target.kick(options.reason);
|
|
25
|
+
await interaction.reply(t('kick.done', { user: target.displayName }));
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { language } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// A language may be declared in several places: these texts are added to the
|
|
4
|
+
// en-US and fr of src/messages/, next to the commands that use them. What
|
|
5
|
+
// says `default: true` there decides for the whole language.
|
|
6
|
+
export const en = language({
|
|
7
|
+
locale: 'en-US',
|
|
8
|
+
texts: {
|
|
9
|
+
cannotPunish: 'You can not act on {user}: their role is above yours.',
|
|
10
|
+
notHere: '{user} is not in this server.',
|
|
11
|
+
'ban.confirm': 'Ban {user}?',
|
|
12
|
+
'ban.yes': 'Ban',
|
|
13
|
+
'ban.defaultReason': 'Banned by {by}',
|
|
14
|
+
'ban.done': 'Banned.',
|
|
15
|
+
'kick.done': '{user} was kicked.',
|
|
16
|
+
'warn.added': '{user} now has {count} warning(s).',
|
|
17
|
+
// A plural: one form per quantity, picked by `count`.
|
|
18
|
+
'warn.count': {
|
|
19
|
+
one: '{user} has {count} warning.',
|
|
20
|
+
other: '{user} has {count} warnings.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
// What Discord shows of these commands: their descriptions live here too.
|
|
24
|
+
commands: {
|
|
25
|
+
ban: {
|
|
26
|
+
description: 'Bans a member, after a confirmation',
|
|
27
|
+
options: {
|
|
28
|
+
target: { description: 'Who to ban' },
|
|
29
|
+
reason: { description: 'Why' },
|
|
30
|
+
days: { description: 'Days of messages to delete' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
kick: {
|
|
34
|
+
description: 'Kicks a member',
|
|
35
|
+
options: {
|
|
36
|
+
target: { description: 'Who to kick' },
|
|
37
|
+
reason: { description: 'Why' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
'warn add': {
|
|
41
|
+
description: 'Warns a member',
|
|
42
|
+
options: {
|
|
43
|
+
target: { description: 'Who to warn' },
|
|
44
|
+
reason: { description: 'Why' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
'warn list': {
|
|
48
|
+
description: 'Counts the warnings of a member',
|
|
49
|
+
options: { target: { description: 'Whose warnings' } },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const fr = language({
|
|
55
|
+
locale: 'fr',
|
|
56
|
+
texts: {
|
|
57
|
+
cannotPunish:
|
|
58
|
+
'Vous ne pouvez pas agir sur {user} : son rôle est au-dessus du vôtre.',
|
|
59
|
+
notHere: '{user} n’est pas sur ce serveur.',
|
|
60
|
+
'ban.confirm': 'Bannir {user} ?',
|
|
61
|
+
'ban.yes': 'Bannir',
|
|
62
|
+
'ban.defaultReason': 'Banni par {by}',
|
|
63
|
+
'ban.done': 'Banni.',
|
|
64
|
+
'kick.done': '{user} a été expulsé.',
|
|
65
|
+
'warn.added': '{user} a maintenant {count} avertissement(s).',
|
|
66
|
+
'warn.count': {
|
|
67
|
+
one: '{user} a {count} avertissement.',
|
|
68
|
+
other: '{user} a {count} avertissements.',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
commands: {
|
|
72
|
+
// A command can be renamed in a language too: /bannir for French users.
|
|
73
|
+
ban: {
|
|
74
|
+
name: 'bannir',
|
|
75
|
+
description: 'Bannit un membre, après confirmation',
|
|
76
|
+
},
|
|
77
|
+
kick: { name: 'expulser', description: 'Expulse un membre' },
|
|
78
|
+
'warn add': { description: 'Avertit un membre' },
|
|
79
|
+
'warn list': { description: 'Compte les avertissements d’un membre' },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { command, event } from 'chapterjs';
|
|
2
|
+
import { addWarning, warningsOf } from './checks';
|
|
3
|
+
|
|
4
|
+
// Two subcommands in one file: /warn add and /warn list. A subcommand is a
|
|
5
|
+
// matter of `name`, not of folder.
|
|
6
|
+
export const add = command({
|
|
7
|
+
name: 'warn add',
|
|
8
|
+
permissions: ['ModerateMembers'],
|
|
9
|
+
options: {
|
|
10
|
+
target: { type: 'user', required: true },
|
|
11
|
+
reason: { type: 'string', required: true },
|
|
12
|
+
},
|
|
13
|
+
async run({ interaction, options, t }) {
|
|
14
|
+
const count = addWarning(options.target.id);
|
|
15
|
+
await interaction.reply(
|
|
16
|
+
t('warn.added', { user: options.target.username, count })
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const list = command({
|
|
22
|
+
name: 'warn list',
|
|
23
|
+
permissions: ['ModerateMembers'],
|
|
24
|
+
ephemeral: true,
|
|
25
|
+
options: { target: { type: 'user', required: true } },
|
|
26
|
+
async run({ interaction, options, t }) {
|
|
27
|
+
await interaction.reply(
|
|
28
|
+
t('warn.count', {
|
|
29
|
+
user: options.target.username,
|
|
30
|
+
count: warningsOf(options.target.id),
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// The event that goes with moderation, next to the commands it follows.
|
|
37
|
+
export const logged = event({
|
|
38
|
+
name: 'banAdd',
|
|
39
|
+
run({ user, guild }) {
|
|
40
|
+
console.log(`${user.username} was banned from ${guild.name}`);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -3,6 +3,7 @@ import { asset, command } from 'chapterjs';
|
|
|
3
3
|
// /hello joins your voice channel and plays public/hello.ogg. An Opus file
|
|
4
4
|
// (.ogg, .webm) plays as it is; an .mp3 or a link needs ffmpeg installed.
|
|
5
5
|
export default command({
|
|
6
|
+
name: 'hello',
|
|
6
7
|
async run({ interaction, member, t }) {
|
|
7
8
|
const channel = member.voiceChannel;
|
|
8
9
|
if (!channel) {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { command } from 'chapterjs';
|
|
2
|
-
import again from '../components/buttons/again';
|
|
1
|
+
import { button, command } from 'chapterjs';
|
|
3
2
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// A command is declared with command() and exported: `name` is what people
|
|
4
|
+
// type after the /. Where the file is and what it is called are up to you:
|
|
5
|
+
// this one could live anywhere in src/.
|
|
6
6
|
export default command({
|
|
7
|
+
name: 'ping',
|
|
7
8
|
// Only the person sees the answer, so `t` speaks their language.
|
|
8
9
|
ephemeral: true,
|
|
9
10
|
async run({ interaction, t }) {
|
|
10
|
-
//
|
|
11
|
-
//
|
|
11
|
+
// The button below, with the data it carries, and a text of
|
|
12
|
+
// src/messages/ in the language of who will read the answer.
|
|
12
13
|
await interaction.reply({
|
|
13
14
|
content: t('pong'),
|
|
14
15
|
components: [again({ count: 1 })],
|
|
@@ -16,10 +17,27 @@ export default command({
|
|
|
16
17
|
},
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
// A button is declared with button() and exported: the name of the export
|
|
21
|
+
// (here `again`) is what tells which one was clicked, you never write an
|
|
22
|
+
// id. `data` is what the button carries from one click to the next.
|
|
23
|
+
export const again = button({
|
|
24
|
+
label: 'Again',
|
|
25
|
+
data: { count: 'number' },
|
|
26
|
+
async run({ interaction, data, t }) {
|
|
27
|
+
const count = data.count + 1;
|
|
28
|
+
// Changes the message the button is on, instead of sending a new one.
|
|
29
|
+
await interaction.update({
|
|
30
|
+
content: t('again', { count }),
|
|
31
|
+
components: [again({ count })],
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
19
36
|
// A command can ask for options, typed for you in `run`, suggest values
|
|
20
37
|
// while the person types, and make each person wait between two uses:
|
|
21
38
|
//
|
|
22
|
-
// export
|
|
39
|
+
// export const hello = command({
|
|
40
|
+
// name: 'hello',
|
|
23
41
|
// description: 'Says hello to someone',
|
|
24
42
|
// cooldown: '10s',
|
|
25
43
|
// options: {
|
|
@@ -3,6 +3,7 @@ import { command } from 'chapterjs';
|
|
|
3
3
|
// /play plays a song in your voice channel: a file you send with the
|
|
4
4
|
// command, or a link to an audio file or a stream. play() takes both.
|
|
5
5
|
export default command({
|
|
6
|
+
name: 'play',
|
|
6
7
|
options: {
|
|
7
8
|
file: { type: 'attachment' },
|
|
8
9
|
url: { type: 'string' },
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { event } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// An event is declared with event() and exported: `name` is the event, and
|
|
4
|
+
// it types what `run` receives. This one runs once, when your bot is
|
|
5
|
+
// connected. A file may declare several events, commands or buttons.
|
|
6
|
+
export default event({
|
|
7
|
+
name: 'ready',
|
|
8
|
+
run({ user, guilds }) {
|
|
9
|
+
console.log(`${user.username} is online in ${guilds.size} server(s)`);
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Something to do by itself, every so often or at given times, is a task:
|
|
14
|
+
// the name of its export is the name of the task.
|
|
15
|
+
//
|
|
16
|
+
// import { task } from 'chapterjs';
|
|
17
|
+
//
|
|
18
|
+
// export const report = task({
|
|
19
|
+
// cron: '0 9 * * 1', // every Monday at 9:00
|
|
20
|
+
// async run({ guilds }) {
|
|
21
|
+
// console.log(`A new week in ${guilds.size} server(s)`);
|
|
22
|
+
// },
|
|
23
|
+
// });
|
|
24
|
+
//
|
|
25
|
+
// To react to something else, declare another event. For example, one that
|
|
26
|
+
// answers "Pong!" to "!ping" (reading messages needs the Message Content
|
|
27
|
+
// intent: `chapterjs dev` tells you how to enable it):
|
|
28
|
+
//
|
|
29
|
+
// export const pong = event({
|
|
30
|
+
// name: 'messageCreate',
|
|
31
|
+
// async run({ message }) {
|
|
32
|
+
// if (message.content === '!ping') await message.reply('Pong!');
|
|
33
|
+
// },
|
|
34
|
+
// });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { event } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// Runs every time someone adds a reaction to a message of your server.
|
|
4
|
+
// Reactions of bots, yours included, are left out: when yours waves back
|
|
5
|
+
// below, this does not run again. Other events work the same way:
|
|
6
|
+
// voiceJoin, pollVoteAdd, banAdd... (see the list of events in the docs).
|
|
7
|
+
export default event({
|
|
8
|
+
name: 'reactionAdd',
|
|
9
|
+
async run({ emoji, channel, messageId }) {
|
|
10
|
+
if (emoji.name !== '👋') return;
|
|
11
|
+
const message = await channel.fetchMessage(messageId);
|
|
12
|
+
await message.react('👋');
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { language } from 'chapterjs';
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// A language is declared with language(): `locale` says which one. Each
|
|
4
4
|
// function of your project receives `t`, which picks the language of who
|
|
5
5
|
// will read the message: t('pong'), t('again', { count }).
|
|
6
6
|
export default language({
|
|
7
|
+
locale: 'en-US',
|
|
7
8
|
// The language used when the one of a person or of a server is not here.
|
|
8
9
|
default: true,
|
|
9
10
|
texts: {
|
|
@@ -15,6 +16,8 @@ export default language({
|
|
|
15
16
|
'Send an audio file (.mp3, .ogg...) or a link starting with https://.',
|
|
16
17
|
playing: 'Playing {song} in {channel}.',
|
|
17
18
|
},
|
|
19
|
+
// What Discord shows of each command, by its name: its description, and
|
|
20
|
+
// the ones of its options. A command described here has none in its file.
|
|
18
21
|
commands: {
|
|
19
22
|
ping: {
|
|
20
23
|
description: 'Replies with Pong!',
|
|
@@ -3,6 +3,7 @@ import { language } from 'chapterjs';
|
|
|
3
3
|
// Every language has the texts of the default one, with the same
|
|
4
4
|
// {placeholders}: a missing one is reported when the file loads.
|
|
5
5
|
export default language({
|
|
6
|
+
locale: 'fr',
|
|
6
7
|
texts: {
|
|
7
8
|
pong: 'Pong! en fr',
|
|
8
9
|
again: 'Pong en fr ! ×{count}',
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { button } from 'chapterjs';
|
|
2
|
-
|
|
3
|
-
// The folder is the kind of component (buttons, selects, modals, embeds)
|
|
4
|
-
// and the path of the file is what tells which one was used: you never
|
|
5
|
-
// write an id. `data` is what the button carries from one click to the next.
|
|
6
|
-
const again = button({
|
|
7
|
-
label: 'Again',
|
|
8
|
-
data: { count: 'number' },
|
|
9
|
-
async run({ interaction, data, t }) {
|
|
10
|
-
const count = data.count + 1;
|
|
11
|
-
// Changes the message the button is on, instead of sending a new one.
|
|
12
|
-
await interaction.update({
|
|
13
|
-
content: t('again', { count }),
|
|
14
|
-
components: [again({ count })],
|
|
15
|
-
});
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export default again;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { event } from 'chapterjs';
|
|
2
|
-
|
|
3
|
-
// Runs every time someone adds a reaction to a message of your server.
|
|
4
|
-
// Reactions of bots, yours included, are left out: when yours waves back
|
|
5
|
-
// below, this file does not run again. Other folders work the same way:
|
|
6
|
-
// voiceJoin, pollVoteAdd, banAdd... (see the list of events in the docs).
|
|
7
|
-
export default event(async ({ emoji, channel, messageId }) => {
|
|
8
|
-
if (emoji.name !== '👋') return;
|
|
9
|
-
const message = await channel.fetchMessage(messageId);
|
|
10
|
-
await message.react('👋');
|
|
11
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { event } from 'chapterjs';
|
|
2
|
-
|
|
3
|
-
// The folder is the event: every file of src/events/ready/ runs once, when
|
|
4
|
-
// your bot is connected. The name of the file is yours to choose.
|
|
5
|
-
export default event(({ user, guilds }) => {
|
|
6
|
-
console.log(`${user.username} is online in ${guilds.size} server(s)`);
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
// Something to do by itself, every so often or at given times, is a task:
|
|
10
|
-
// a file of src/tasks/, like src/tasks/report.ts:
|
|
11
|
-
//
|
|
12
|
-
// import { task } from 'chapterjs';
|
|
13
|
-
//
|
|
14
|
-
// export default task({
|
|
15
|
-
// cron: '0 9 * * 1', // every Monday at 9:00
|
|
16
|
-
// async run({ guilds }) {
|
|
17
|
-
// console.log(`A new week in ${guilds.size} server(s)`);
|
|
18
|
-
// },
|
|
19
|
-
// });
|
|
20
|
-
//
|
|
21
|
-
// To react to something else, create a folder named after the event. For
|
|
22
|
-
// example src/events/messageCreate/ping.ts, which answers "Pong!" to "!ping"
|
|
23
|
-
// (reading messages needs the Message Content intent: `chapterjs dev` tells
|
|
24
|
-
// you how to enable it):
|
|
25
|
-
//
|
|
26
|
-
// import { event } from 'chapterjs';
|
|
27
|
-
//
|
|
28
|
-
// export default event(async ({ message }) => {
|
|
29
|
-
// if (message.content === '!ping') await message.reply('Pong!');
|
|
30
|
-
// });
|