create-chapter 0.2.2 â 0.2.3
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/LICENSE +21 -0
- package/dist/index.js +4 -1
- package/package.json +1 -1
- package/templates/community/.env.example +4 -0
- package/templates/community/_gitignore +5 -0
- package/templates/community/package.json +23 -0
- package/templates/community/src/messages.ts +49 -0
- package/templates/community/src/presence.ts +6 -0
- package/templates/community/src/roles/roles.ts +60 -0
- package/templates/community/src/stats/weekly.ts +50 -0
- package/templates/community/src/welcome/channel.ts +19 -0
- package/templates/community/src/welcome/welcome.ts +37 -0
- package/templates/community/tsconfig.json +20 -0
- package/templates/default/_gitignore +1 -0
- package/templates/default/package.json +1 -1
- package/templates/default/src/events/ready.ts +14 -20
- package/templates/moderation/.env.example +4 -0
- package/templates/moderation/_gitignore +5 -0
- package/templates/moderation/package.json +23 -0
- package/templates/moderation/src/moderation/channel.ts +67 -0
- package/templates/moderation/src/moderation/log.ts +37 -0
- package/templates/moderation/src/moderation/messages.ts +158 -0
- package/templates/moderation/src/moderation/purge.ts +30 -0
- package/templates/moderation/src/moderation/timeout.ts +82 -0
- package/templates/moderation/src/moderation/warn.ts +61 -0
- package/templates/moderation/src/presence.ts +6 -0
- package/templates/moderation/tsconfig.json +20 -0
- package/templates/music/.env.example +4 -0
- package/templates/music/_gitignore +5 -0
- package/templates/music/package.json +23 -0
- package/templates/music/src/music/leave.ts +18 -0
- package/templates/music/src/music/messages.ts +83 -0
- package/templates/music/src/music/play.ts +120 -0
- package/templates/music/src/music/queue-command.ts +28 -0
- package/templates/music/src/music/queue.ts +61 -0
- package/templates/music/src/presence.ts +6 -0
- package/templates/music/tsconfig.json +20 -0
- package/templates/tickets/.env.example +4 -0
- package/templates/tickets/_gitignore +5 -0
- package/templates/tickets/package.json +23 -0
- package/templates/tickets/src/presence.ts +6 -0
- package/templates/tickets/src/tickets/close.ts +39 -0
- package/templates/tickets/src/tickets/messages.ts +68 -0
- package/templates/tickets/src/tickets/open.ts +81 -0
- package/templates/tickets/src/tickets/panel.ts +35 -0
- package/templates/tickets/tsconfig.json +20 -0
- package/templates/default/public/hello.ogg +0 -0
- package/templates/default/src/commands/hello.ts +0 -23
- package/templates/default/src/commands/ping.ts +0 -55
- package/templates/default/src/commands/play.ts +0 -41
- package/templates/default/src/events/wave.ts +0 -14
- package/templates/default/src/messages/en-US.ts +0 -36
- package/templates/default/src/messages/fr.ts +0 -37
- package/templates/default/src/presence.ts +0 -8
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { language } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// The texts of the bot, one language() per language. `t` picks the one of
|
|
4
|
+
// who will read the message. A text with {count} can have one form per
|
|
5
|
+
// quantity: Discord's language rules pick the form.
|
|
6
|
+
export const en = language({
|
|
7
|
+
locale: 'en-US',
|
|
8
|
+
default: true,
|
|
9
|
+
texts: {
|
|
10
|
+
cantModerate:
|
|
11
|
+
'You can not moderate {user}: their highest role is above yours.',
|
|
12
|
+
timedOut: '{user} is timed out until {until}. Reason: {reason}',
|
|
13
|
+
noReason: 'none given',
|
|
14
|
+
notTimedOut: '{user} is not timed out.',
|
|
15
|
+
timeoutRemoved: '{user} can speak again.',
|
|
16
|
+
purged: {
|
|
17
|
+
one: '{count} message deleted.',
|
|
18
|
+
other: '{count} messages deleted.',
|
|
19
|
+
},
|
|
20
|
+
notHere: 'This only works in a text channel of the server.',
|
|
21
|
+
slowmodeOff: 'Slowmode is off.',
|
|
22
|
+
slowmodeOn: {
|
|
23
|
+
one: 'Slowmode: one message every {count} second.',
|
|
24
|
+
other: 'Slowmode: one message every {count} seconds.',
|
|
25
|
+
},
|
|
26
|
+
locked: 'đ This channel is locked. Reason: {reason}',
|
|
27
|
+
unlocked: 'đ This channel is open again.',
|
|
28
|
+
warned: {
|
|
29
|
+
one: '{user} was warned. They have {count} warning.',
|
|
30
|
+
other: '{user} was warned. They have {count} warnings.',
|
|
31
|
+
},
|
|
32
|
+
noWarnings: '{user} has no warning.',
|
|
33
|
+
warningsOf: {
|
|
34
|
+
one: '{user} has {count} warning:',
|
|
35
|
+
other: '{user} has {count} warnings:',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
commands: {
|
|
39
|
+
timeout: {
|
|
40
|
+
description: 'Keeps someone from writing and speaking for a while',
|
|
41
|
+
options: {
|
|
42
|
+
user: { description: 'Who' },
|
|
43
|
+
duration: { description: 'For how long' },
|
|
44
|
+
reason: { description: 'Why' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
untimeout: {
|
|
48
|
+
description: 'Ends the timeout of someone',
|
|
49
|
+
options: { user: { description: 'Who' } },
|
|
50
|
+
},
|
|
51
|
+
purge: {
|
|
52
|
+
description: 'Deletes the last messages of this channel',
|
|
53
|
+
options: {
|
|
54
|
+
count: { description: 'How many, 100 at most' },
|
|
55
|
+
user: { description: 'Only the messages of this person' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
slowmode: {
|
|
59
|
+
description: 'Sets how long people wait between two messages here',
|
|
60
|
+
options: {
|
|
61
|
+
seconds: { description: 'Seconds between two messages, 0 for none' },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
lock: {
|
|
65
|
+
description: 'Keeps everyone from writing in this channel',
|
|
66
|
+
options: { reason: { description: 'Why' } },
|
|
67
|
+
},
|
|
68
|
+
unlock: { description: 'Lets everyone write in this channel again' },
|
|
69
|
+
'warn add': {
|
|
70
|
+
description: 'Warns someone; warnings are forgotten after 30 days',
|
|
71
|
+
options: { user: { description: 'Who' }, reason: { description: 'Why' } },
|
|
72
|
+
},
|
|
73
|
+
'warn list': {
|
|
74
|
+
description: 'Lists the warnings of someone',
|
|
75
|
+
options: { user: { description: 'Who' } },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const fr = language({
|
|
81
|
+
locale: 'fr',
|
|
82
|
+
texts: {
|
|
83
|
+
cantModerate:
|
|
84
|
+
'Vous ne pouvez pas modÊrer {user} : son rôle le plus haut est au-dessus du vôtre.',
|
|
85
|
+
timedOut:
|
|
86
|
+
'{user} est exclu temporairement jusquâà {until}. Raison : {reason}',
|
|
87
|
+
noReason: 'aucune',
|
|
88
|
+
notTimedOut: '{user} nâest pas exclu.',
|
|
89
|
+
timeoutRemoved: '{user} peut de nouveau parler.',
|
|
90
|
+
purged: {
|
|
91
|
+
one: '{count} message supprimÊ.',
|
|
92
|
+
other: '{count} messages supprimÊs.',
|
|
93
|
+
},
|
|
94
|
+
notHere: 'Ceci ne fonctionne que dans un salon textuel du serveur.',
|
|
95
|
+
slowmodeOff: 'Le mode lent est dÊsactivÊ.',
|
|
96
|
+
slowmodeOn: {
|
|
97
|
+
one: 'Mode lent : un message toutes les {count} seconde.',
|
|
98
|
+
other: 'Mode lent : un message toutes les {count} secondes.',
|
|
99
|
+
},
|
|
100
|
+
locked: 'đ Ce salon est verrouillÊ. Raison : {reason}',
|
|
101
|
+
unlocked: 'đ Ce salon est de nouveau ouvert.',
|
|
102
|
+
warned: {
|
|
103
|
+
one: '{user} a ÊtÊ averti. Il a {count} avertissement.',
|
|
104
|
+
other: '{user} a ÊtÊ averti. Il a {count} avertissements.',
|
|
105
|
+
},
|
|
106
|
+
noWarnings: '{user} nâa aucun avertissement.',
|
|
107
|
+
warningsOf: {
|
|
108
|
+
one: '{user} a {count} avertissement :',
|
|
109
|
+
other: '{user} a {count} avertissements :',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
commands: {
|
|
113
|
+
timeout: {
|
|
114
|
+
description: 'EmpÃĒche quelquâun dâÊcrire et de parler pendant un moment',
|
|
115
|
+
options: {
|
|
116
|
+
user: { description: 'Qui' },
|
|
117
|
+
duration: { description: 'Pendant combien de temps' },
|
|
118
|
+
reason: { description: 'Pourquoi' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
untimeout: {
|
|
122
|
+
description: 'Met fin à lâexclusion temporaire de quelquâun',
|
|
123
|
+
options: { user: { description: 'Qui' } },
|
|
124
|
+
},
|
|
125
|
+
purge: {
|
|
126
|
+
description: 'Supprime les derniers messages de ce salon',
|
|
127
|
+
options: {
|
|
128
|
+
count: { description: 'Combien, 100 au plus' },
|
|
129
|
+
user: { description: 'Seulement les messages de cette personne' },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
slowmode: {
|
|
133
|
+
description: 'Règle lâattente entre deux messages ici',
|
|
134
|
+
options: {
|
|
135
|
+
seconds: { description: 'Secondes entre deux messages, 0 pour aucune' },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
lock: {
|
|
139
|
+
description: 'EmpÃĒche tout le monde dâÊcrire dans ce salon',
|
|
140
|
+
options: { reason: { description: 'Pourquoi' } },
|
|
141
|
+
},
|
|
142
|
+
unlock: {
|
|
143
|
+
description: 'Laisse de nouveau tout le monde Êcrire dans ce salon',
|
|
144
|
+
},
|
|
145
|
+
'warn add': {
|
|
146
|
+
description:
|
|
147
|
+
'Avertit quelquâun ; un avertissement est oubliÊ après 30 jours',
|
|
148
|
+
options: {
|
|
149
|
+
user: { description: 'Qui' },
|
|
150
|
+
reason: { description: 'Pourquoi' },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
'warn list': {
|
|
154
|
+
description: 'Liste les avertissements de quelquâun',
|
|
155
|
+
options: { user: { description: 'Qui' } },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { command } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// /purge deletes the last messages of the channel, all of them or only
|
|
4
|
+
// the ones of a person. Discord only deletes in bulk what is less than two
|
|
5
|
+
// weeks old: older messages are left out, and the answer says how many
|
|
6
|
+
// really went.
|
|
7
|
+
export default command({
|
|
8
|
+
name: 'purge',
|
|
9
|
+
permissions: ['ManageMessages'],
|
|
10
|
+
// The answer only the person sees, so the channel is not filled again.
|
|
11
|
+
ephemeral: true,
|
|
12
|
+
cooldown: { channel: '5s' },
|
|
13
|
+
options: {
|
|
14
|
+
count: { type: 'integer', min: 1, max: 100, required: true },
|
|
15
|
+
user: { type: 'user' },
|
|
16
|
+
},
|
|
17
|
+
async run({ interaction, options, channel, member, t }) {
|
|
18
|
+
// Deleting takes a moment: tell Discord the answer is coming.
|
|
19
|
+
await interaction.defer();
|
|
20
|
+
const messages = await channel.fetchMessages({ limit: options.count });
|
|
21
|
+
const chosen = options.user
|
|
22
|
+
? messages.filter(message => message.author.id === options.user!.id)
|
|
23
|
+
: messages;
|
|
24
|
+
const deleted = await channel.bulkDelete(
|
|
25
|
+
chosen,
|
|
26
|
+
`Purge by ${member.user.username}`
|
|
27
|
+
);
|
|
28
|
+
await interaction.reply(t('purged', { count: deleted }));
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { command, timestamp, type GuildMember } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// Someone can only act on people below them, and never on the owner.
|
|
4
|
+
function canModerate(target: GuildMember, by: GuildMember): boolean {
|
|
5
|
+
return (
|
|
6
|
+
target.id !== target.guild.ownerId &&
|
|
7
|
+
target.highestRole.position < by.highestRole.position
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// /timeout keeps someone from writing, reacting and speaking for a while.
|
|
12
|
+
// `permissions` is what a member needs to see and use the command; the bot
|
|
13
|
+
// itself needs Moderate Members. The choices of `duration` are what the
|
|
14
|
+
// person picks from, in minutes.
|
|
15
|
+
export default command({
|
|
16
|
+
name: 'timeout',
|
|
17
|
+
permissions: ['ModerateMembers'],
|
|
18
|
+
options: {
|
|
19
|
+
user: { type: 'user', required: true },
|
|
20
|
+
duration: {
|
|
21
|
+
type: 'integer',
|
|
22
|
+
required: true,
|
|
23
|
+
choices: {
|
|
24
|
+
'5 minutes': 5,
|
|
25
|
+
'1 hour': 60,
|
|
26
|
+
'1 day': 60 * 24,
|
|
27
|
+
'1 week': 60 * 24 * 7,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
reason: { type: 'string', maxLength: 200 },
|
|
31
|
+
},
|
|
32
|
+
async run({ interaction, options, guild, member, t }) {
|
|
33
|
+
const target = await guild.fetchMember(options.user);
|
|
34
|
+
if (!canModerate(target, member)) {
|
|
35
|
+
await interaction.reply({
|
|
36
|
+
content: t('cantModerate', { user: target.toString() }),
|
|
37
|
+
ephemeral: true,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const until = Date.now() + options.duration * 60 * 1000;
|
|
42
|
+
await target.timeout(
|
|
43
|
+
options.duration * 60 * 1000,
|
|
44
|
+
options.reason ?? `By ${member.user.username}`
|
|
45
|
+
);
|
|
46
|
+
await interaction.reply(
|
|
47
|
+
t('timedOut', {
|
|
48
|
+
user: target.toString(),
|
|
49
|
+
until: timestamp(until, 'R'),
|
|
50
|
+
reason: options.reason ?? t('noReason'),
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const untimeout = command({
|
|
57
|
+
name: 'untimeout',
|
|
58
|
+
permissions: ['ModerateMembers'],
|
|
59
|
+
options: {
|
|
60
|
+
user: { type: 'user', required: true },
|
|
61
|
+
},
|
|
62
|
+
async run({ interaction, options, guild, member, t }) {
|
|
63
|
+
const target = await guild.fetchMember(options.user);
|
|
64
|
+
// The same rule as /timeout: nobody lifts what someone above them set.
|
|
65
|
+
if (!canModerate(target, member)) {
|
|
66
|
+
await interaction.reply({
|
|
67
|
+
content: t('cantModerate', { user: target.toString() }),
|
|
68
|
+
ephemeral: true,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!target.isTimedOut) {
|
|
73
|
+
await interaction.reply({
|
|
74
|
+
content: t('notTimedOut', { user: target.toString() }),
|
|
75
|
+
ephemeral: true,
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await target.removeTimeout(`By ${member.user.username}`);
|
|
80
|
+
await interaction.reply(t('timeoutRemoved', { user: target.toString() }));
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { command, store, timestamp } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
interface Warning {
|
|
4
|
+
reason: string;
|
|
5
|
+
/** Who gave it: a user id. */
|
|
6
|
+
by: string;
|
|
7
|
+
/** When, in ms since the epoch: a Date would not survive the file. */
|
|
8
|
+
at: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The warnings of each member, kept from one restart to the next, per
|
|
12
|
+
// server: `warnings.get(member.id)` in a command is the warnings of that
|
|
13
|
+
// member in the server the command was used in. A warning is forgotten
|
|
14
|
+
// thirty days after the last one was added.
|
|
15
|
+
export const warnings = store<Warning[]>({ expires: '30d' });
|
|
16
|
+
|
|
17
|
+
export default command({
|
|
18
|
+
name: 'warn add',
|
|
19
|
+
permissions: ['ModerateMembers'],
|
|
20
|
+
options: {
|
|
21
|
+
user: { type: 'user', required: true },
|
|
22
|
+
reason: { type: 'string', required: true, maxLength: 200 },
|
|
23
|
+
},
|
|
24
|
+
async run({ interaction, options, member, t }) {
|
|
25
|
+
// Reads, adds and keeps in one go; `list` is undefined the first time.
|
|
26
|
+
const list = await warnings.update(options.user.id, (list = []) => [
|
|
27
|
+
...list,
|
|
28
|
+
{ reason: options.reason, by: member.id, at: Date.now() },
|
|
29
|
+
]);
|
|
30
|
+
await interaction.reply(
|
|
31
|
+
t('warned', { user: options.user.toString(), count: list.length })
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const list = command({
|
|
37
|
+
name: 'warn list',
|
|
38
|
+
permissions: ['ModerateMembers'],
|
|
39
|
+
ephemeral: true,
|
|
40
|
+
options: {
|
|
41
|
+
user: { type: 'user', required: true },
|
|
42
|
+
},
|
|
43
|
+
async run({ interaction, options, t }) {
|
|
44
|
+
const list = (await warnings.get(options.user.id)) ?? [];
|
|
45
|
+
if (list.length === 0) {
|
|
46
|
+
await interaction.reply(
|
|
47
|
+
t('noWarnings', { user: options.user.toString() })
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await interaction.reply(
|
|
52
|
+
[
|
|
53
|
+
t('warningsOf', { user: options.user.toString(), count: list.length }),
|
|
54
|
+
...list.map(
|
|
55
|
+
warning =>
|
|
56
|
+
`âĸ ${timestamp(warning.at, 'R')} <@${warning.by}>: ${warning.reason}`
|
|
57
|
+
),
|
|
58
|
+
].join('\n')
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "Preserve",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"erasableSyntaxOnly": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["node"]
|
|
13
|
+
},
|
|
14
|
+
"files": [],
|
|
15
|
+
"references": [
|
|
16
|
+
{
|
|
17
|
+
"path": "./.chapterjs"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "music-bot",
|
|
3
|
+
"description": "A music player in voice: a queue, buttons, pause, skip and stop",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "chapterjs dev",
|
|
8
|
+
"start": "chapterjs start",
|
|
9
|
+
"build": "chapterjs build",
|
|
10
|
+
"typecheck": "tsc -b",
|
|
11
|
+
"postinstall": "chapterjs sync"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"chapterjs": "latest"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^24.0.0",
|
|
18
|
+
"typescript": "^7.0.2"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { command } from 'chapterjs';
|
|
2
|
+
import { clearQueue } from './queue';
|
|
3
|
+
|
|
4
|
+
// /leave makes the bot leave the voice channel and forget the queue.
|
|
5
|
+
export default command({
|
|
6
|
+
name: 'leave',
|
|
7
|
+
async run({ interaction, guild, t }) {
|
|
8
|
+
const voice = guild.voice;
|
|
9
|
+
if (!voice) {
|
|
10
|
+
await interaction.reply({ content: t('notInVoice'), ephemeral: true });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
clearQueue(guild);
|
|
14
|
+
// leave() ends what plays: the loop of play.ts stops too.
|
|
15
|
+
await voice.leave();
|
|
16
|
+
await interaction.reply(t('left'));
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { language } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// The texts of the bot, one language() per language. `t` picks the one of
|
|
4
|
+
// who will read the message. A text with {count} can have one form per
|
|
5
|
+
// quantity (`one`, `other`...): Discord's language rules pick the form.
|
|
6
|
+
export const en = language({
|
|
7
|
+
locale: 'en-US',
|
|
8
|
+
default: true,
|
|
9
|
+
texts: {
|
|
10
|
+
nothingToPlay:
|
|
11
|
+
'Send an audio file (.mp3, .ogg...) or a link starting with https://.',
|
|
12
|
+
joinVoice: 'Join a voice channel first, then ask again.',
|
|
13
|
+
playing: 'Now playing {song} in {channel}.',
|
|
14
|
+
queued: 'Added {song} to the queue, at position {position}.',
|
|
15
|
+
nowPlaying: 'âļ {song}, asked by {by}',
|
|
16
|
+
upNext: {
|
|
17
|
+
one: 'Up next, {count} song:',
|
|
18
|
+
other: 'Up next, {count} songs:',
|
|
19
|
+
},
|
|
20
|
+
nothingPlaying: 'Nothing is playing.',
|
|
21
|
+
notWithMe: 'Join my voice channel to control what plays.',
|
|
22
|
+
notInVoice: 'I am not in a voice channel.',
|
|
23
|
+
pauseButton: 'Pause / resume',
|
|
24
|
+
skipButton: 'Skip',
|
|
25
|
+
stopButton: 'Stop',
|
|
26
|
+
paused: 'Paused.',
|
|
27
|
+
resumed: 'Playing again.',
|
|
28
|
+
skipped: 'Skipped.',
|
|
29
|
+
stopped: 'Stopped, the queue is empty.',
|
|
30
|
+
left: 'Bye!',
|
|
31
|
+
},
|
|
32
|
+
commands: {
|
|
33
|
+
play: {
|
|
34
|
+
description:
|
|
35
|
+
'Plays a song in your voice channel, or adds it to the queue',
|
|
36
|
+
options: {
|
|
37
|
+
file: { description: 'A song to play' },
|
|
38
|
+
url: { description: 'Or the link of a song or a stream' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
queue: { description: 'Shows what plays and what comes next' },
|
|
42
|
+
leave: { description: 'Makes the bot leave the voice channel' },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const fr = language({
|
|
47
|
+
locale: 'fr',
|
|
48
|
+
texts: {
|
|
49
|
+
nothingToPlay:
|
|
50
|
+
'Envoyez un fichier audio (.mp3, .ogg...) ou un lien qui commence par https://.',
|
|
51
|
+
joinVoice: 'Rejoignez dâabord un salon vocal, puis redemandez.',
|
|
52
|
+
playing: 'Lecture de {song} dans {channel}.',
|
|
53
|
+
queued: '{song} ajoutÊ à la file, en position {position}.',
|
|
54
|
+
nowPlaying: 'âļ {song}, demandÊ par {by}',
|
|
55
|
+
upNext: {
|
|
56
|
+
one: 'Ensuite, {count} morceau :',
|
|
57
|
+
other: 'Ensuite, {count} morceaux :',
|
|
58
|
+
},
|
|
59
|
+
nothingPlaying: 'Rien nâest en cours de lecture.',
|
|
60
|
+
notWithMe: 'Rejoignez mon salon vocal pour contrôler la lecture.',
|
|
61
|
+
notInVoice: 'Je ne suis pas dans un salon vocal.',
|
|
62
|
+
pauseButton: 'Pause / reprendre',
|
|
63
|
+
skipButton: 'Passer',
|
|
64
|
+
stopButton: 'Stop',
|
|
65
|
+
paused: 'En pause.',
|
|
66
|
+
resumed: 'Reprise.',
|
|
67
|
+
skipped: 'PassÊ.',
|
|
68
|
+
stopped: 'ArrÃĒtÊ, la file est vide.',
|
|
69
|
+
left: 'Ã plus !',
|
|
70
|
+
},
|
|
71
|
+
commands: {
|
|
72
|
+
play: {
|
|
73
|
+
description:
|
|
74
|
+
'Joue une musique dans votre salon vocal, ou lâajoute à la file',
|
|
75
|
+
options: {
|
|
76
|
+
file: { description: 'Une musique à jouer' },
|
|
77
|
+
url: { description: 'Ou le lien dâune musique ou dâun flux' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
queue: { description: 'Montre ce qui joue et ce qui suit' },
|
|
81
|
+
leave: { description: 'Fait quitter le salon vocal au bot' },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { button, command, row } from 'chapterjs';
|
|
2
|
+
import { clearQueue, playQueue, queueOf, type Song } from './queue';
|
|
3
|
+
|
|
4
|
+
// /play adds a song to the queue of the server and starts playing when
|
|
5
|
+
// nothing is. A song is a file sent with the command (an .mp3, an .ogg...)
|
|
6
|
+
// or a link to an audio file or a stream; anything that is not Opus needs
|
|
7
|
+
// ffmpeg installed on the machine that runs the bot.
|
|
8
|
+
export default command({
|
|
9
|
+
name: 'play',
|
|
10
|
+
options: {
|
|
11
|
+
file: { type: 'attachment' },
|
|
12
|
+
url: { type: 'string' },
|
|
13
|
+
},
|
|
14
|
+
async run({ interaction, member, guild, channel, options, t }) {
|
|
15
|
+
const { file, url } = options;
|
|
16
|
+
if (
|
|
17
|
+
(!file && !url) ||
|
|
18
|
+
(file && !file.contentType?.startsWith('audio/')) ||
|
|
19
|
+
(url && !/^https?:\/\//.test(url))
|
|
20
|
+
) {
|
|
21
|
+
await interaction.reply({ content: t('nothingToPlay'), ephemeral: true });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const voiceChannel = member.voiceChannel;
|
|
25
|
+
if (!voiceChannel) {
|
|
26
|
+
await interaction.reply({ content: t('joinVoice'), ephemeral: true });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const song: Song = {
|
|
30
|
+
source: file ?? url!,
|
|
31
|
+
title: file?.filename ?? url!,
|
|
32
|
+
requestedBy: member.toString(),
|
|
33
|
+
};
|
|
34
|
+
const queue = queueOf(guild);
|
|
35
|
+
queue.songs.push(song);
|
|
36
|
+
if (queue.current) {
|
|
37
|
+
await interaction.reply(
|
|
38
|
+
t('queued', { song: song.title, position: queue.songs.length })
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await interaction.reply({
|
|
43
|
+
content: t('playing', { song: song.title, channel: voiceChannel.name }),
|
|
44
|
+
components: [row([pause, skip, stop])],
|
|
45
|
+
});
|
|
46
|
+
// Joining the channel the bot is already in changes nothing. The loop
|
|
47
|
+
// runs until the queue is empty, and the bot leaves by itself.
|
|
48
|
+
const voice = await voiceChannel.join();
|
|
49
|
+
await playQueue(guild, voice, text => channel.send(text));
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// The three buttons under "Now playing". A button is named by its export,
|
|
54
|
+
// and `run` receives the same things as a command.
|
|
55
|
+
export const pause = button({
|
|
56
|
+
label: ({ t }) => t('pauseButton'),
|
|
57
|
+
emoji: 'â¯ī¸',
|
|
58
|
+
async run({ interaction, guild, member, t }) {
|
|
59
|
+
const voice = guild.voice;
|
|
60
|
+
if (!voice?.playing) {
|
|
61
|
+
await interaction.reply({
|
|
62
|
+
content: t('nothingPlaying'),
|
|
63
|
+
ephemeral: true,
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (member.voiceChannel?.id !== voice.channel.id) {
|
|
68
|
+
await interaction.reply({ content: t('notWithMe'), ephemeral: true });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (voice.paused) voice.resume();
|
|
72
|
+
else voice.pause();
|
|
73
|
+
await interaction.reply(voice.paused ? t('paused') : t('resumed'));
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const skip = button({
|
|
78
|
+
label: ({ t }) => t('skipButton'),
|
|
79
|
+
emoji: 'âī¸',
|
|
80
|
+
async run({ interaction, guild, member, t }) {
|
|
81
|
+
const voice = guild.voice;
|
|
82
|
+
if (!voice?.playing) {
|
|
83
|
+
await interaction.reply({
|
|
84
|
+
content: t('nothingPlaying'),
|
|
85
|
+
ephemeral: true,
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (member.voiceChannel?.id !== voice.channel.id) {
|
|
90
|
+
await interaction.reply({ content: t('notWithMe'), ephemeral: true });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// stop() ends the current play(): the loop goes on with the next song.
|
|
94
|
+
voice.stop();
|
|
95
|
+
await interaction.reply(t('skipped'));
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const stop = button({
|
|
100
|
+
label: ({ t }) => t('stopButton'),
|
|
101
|
+
emoji: 'âšī¸',
|
|
102
|
+
style: 'danger',
|
|
103
|
+
async run({ interaction, guild, member, t }) {
|
|
104
|
+
const voice = guild.voice;
|
|
105
|
+
if (!voice) {
|
|
106
|
+
await interaction.reply({
|
|
107
|
+
content: t('nothingPlaying'),
|
|
108
|
+
ephemeral: true,
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (member.voiceChannel?.id !== voice.channel.id) {
|
|
113
|
+
await interaction.reply({ content: t('notWithMe'), ephemeral: true });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
clearQueue(guild);
|
|
117
|
+
voice.stop();
|
|
118
|
+
await interaction.reply(t('stopped'));
|
|
119
|
+
},
|
|
120
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { command } from 'chapterjs';
|
|
2
|
+
import { queueOf } from './queue';
|
|
3
|
+
|
|
4
|
+
// /queue shows what plays and what comes next. A text with {count} may
|
|
5
|
+
// have one form per quantity in the language files: see messages.ts.
|
|
6
|
+
export default command({
|
|
7
|
+
name: 'queue',
|
|
8
|
+
ephemeral: true,
|
|
9
|
+
async run({ interaction, guild, t }) {
|
|
10
|
+
const queue = queueOf(guild);
|
|
11
|
+
if (!queue.current) {
|
|
12
|
+
await interaction.reply(t('nothingPlaying'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const next = queue.songs
|
|
16
|
+
.slice(0, 10)
|
|
17
|
+
.map((song, index) => `${index + 1}. ${song.title} (${song.requestedBy})`)
|
|
18
|
+
.join('\n');
|
|
19
|
+
const lines = [
|
|
20
|
+
t('nowPlaying', {
|
|
21
|
+
song: queue.current.title,
|
|
22
|
+
by: queue.current.requestedBy,
|
|
23
|
+
}),
|
|
24
|
+
];
|
|
25
|
+
if (next) lines.push(t('upNext', { count: queue.songs.length }), next);
|
|
26
|
+
await interaction.reply(lines.join('\n'));
|
|
27
|
+
},
|
|
28
|
+
});
|