create-chapterjs 0.4.0 → 0.5.0
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/README.md +1 -1
- package/package.json +1 -1
- package/templates/games/.env.example +7 -0
- package/templates/games/_gitignore +5 -0
- package/templates/games/package.json +24 -0
- package/templates/games/src/duel/duel.ts +84 -0
- package/templates/games/src/games.test.ts +67 -0
- package/templates/games/src/messages.ts +105 -0
- package/templates/games/src/presence.ts +23 -0
- package/templates/games/src/quiz/questions.ts +9 -0
- package/templates/games/src/quiz/quiz.ts +47 -0
- package/templates/games/src/quiz/score-menu.ts +21 -0
- package/templates/games/src/quiz/scores.ts +7 -0
- package/templates/games/tsconfig.json +20 -0
- package/templates/moderation/src/moderation/messages.ts +2 -0
- package/templates/moderation/src/moderation/moderation.test.ts +7 -1
- package/templates/moderation/src/moderation/timeout.ts +17 -2
- package/templates/tickets/src/tickets/tickets.test.ts +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ bun create chapterjs my-bot
|
|
|
24
24
|
|
|
25
25
|
- **The folder**, when it is not given: `.` is the current folder, which must be new or empty.
|
|
26
26
|
- **The package manager**, among those installed on your machine: the one you ran the command with is preselected.
|
|
27
|
-
- **A template**: `default` is one file, the `ready` event, and the [tutorial](https://www.chapterjs.org/tutorial) starts from it. The others are complete bots to start from or to read: `tickets` (support tickets in private threads), `music` (a player with a queue and buttons), `community` (welcome cards, roles people pick, weekly stats)
|
|
27
|
+
- **A template**: `default` is one file, the `ready` event, and the [tutorial](https://www.chapterjs.org/tutorial) starts from it. The others are complete bots to start from or to read: `tickets` (support tickets in private threads), `music` (a player with a queue and buttons), `community` (welcome cards, roles people pick, weekly stats), `moderation` (timeouts, purge, slowmode, lock, and a log of every action) and `games` (a quiz against the clock, duels with buttons, menu commands, a presence that takes turns).
|
|
28
28
|
|
|
29
29
|
A question with only one possible answer is not asked: its answer is shown with `ℹ`. Node 24 or later is needed, and the command says so at once when it is older.
|
|
30
30
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Your bot token: https://discord.com/developers/applications → your app → Bot → Reset Token
|
|
2
|
+
BOT_TOKEN=
|
|
3
|
+
# The ID of the server used by `chapterjs dev` (right-click the server → Copy Server ID)
|
|
4
|
+
DEV_GUILD_ID=
|
|
5
|
+
# Only for stores declared with storage: 'redis' (https://www.chapterjs.org/storage#in-redis)
|
|
6
|
+
# REDIS_URL=redis://localhost:6379
|
|
7
|
+
# REDIS_PASSWORD=
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "games-bot",
|
|
3
|
+
"description": "A quiz against the clock, duels with buttons, menu commands and a presence that takes turns",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "chapterjs dev",
|
|
8
|
+
"start": "chapterjs start",
|
|
9
|
+
"build": "chapterjs build",
|
|
10
|
+
"test": "chapterjs test",
|
|
11
|
+
"typecheck": "tsc -b",
|
|
12
|
+
"postinstall": "chapterjs sync"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"chapterjs": "latest"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^24.0.0",
|
|
19
|
+
"typescript": "^7.0.2"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { button, command, userMention } from 'chapterjs';
|
|
2
|
+
import type { CommandInteraction, Translator, User } from 'chapterjs';
|
|
3
|
+
|
|
4
|
+
// Buttons with no `run`: nothing runs them but `waitForClick()` below. Once
|
|
5
|
+
// nothing waits for them, a click tells the person they are not available.
|
|
6
|
+
export const accept = button({
|
|
7
|
+
label: ({ t }) => t('duelAccept'),
|
|
8
|
+
style: 'success',
|
|
9
|
+
});
|
|
10
|
+
export const decline = button({
|
|
11
|
+
label: ({ t }) => t('duelDecline'),
|
|
12
|
+
style: 'secondary',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** A roll of a six-sided die. */
|
|
16
|
+
const roll = (): number => Math.floor(Math.random() * 6) + 1;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Challenges someone: they have a minute to accept with a button, then
|
|
20
|
+
* both roll a die. Shared by `/duel` and the menu of a message.
|
|
21
|
+
*/
|
|
22
|
+
async function challenge(
|
|
23
|
+
interaction: CommandInteraction,
|
|
24
|
+
t: Translator,
|
|
25
|
+
user: User,
|
|
26
|
+
opponent: User
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
if (opponent.id === user.id || opponent.bot) {
|
|
29
|
+
await interaction.reply({ content: t('duelNobody'), ephemeral: true });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const asked = await interaction.reply({
|
|
33
|
+
content: t('duelAsk', {
|
|
34
|
+
opponent: userMention(opponent.id),
|
|
35
|
+
user: user.username,
|
|
36
|
+
}),
|
|
37
|
+
components: [accept, decline],
|
|
38
|
+
});
|
|
39
|
+
const click = await asked.waitForClick({ from: opponent, within: '1m' });
|
|
40
|
+
if (!click) {
|
|
41
|
+
await interaction.edit({
|
|
42
|
+
content: t('duelExpired', { opponent: opponent.username }),
|
|
43
|
+
components: [],
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (click.is(decline)) {
|
|
48
|
+
await click.interaction.update({
|
|
49
|
+
content: t('duelDeclined', { opponent: opponent.username }),
|
|
50
|
+
components: [],
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const mine = roll();
|
|
55
|
+
const theirs = roll();
|
|
56
|
+
const result =
|
|
57
|
+
mine === theirs
|
|
58
|
+
? t('duelDraw')
|
|
59
|
+
: t('duelWinner', {
|
|
60
|
+
user: mine > theirs ? user.username : opponent.username,
|
|
61
|
+
});
|
|
62
|
+
await click.interaction.update({
|
|
63
|
+
content: `${t('duelRolls', { user: user.username, mine, opponent: opponent.username, theirs })}\n${result}`,
|
|
64
|
+
components: [],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// `/duel @someone`.
|
|
69
|
+
export const duel = command({
|
|
70
|
+
name: 'duel',
|
|
71
|
+
options: { opponent: { type: 'user', required: true } },
|
|
72
|
+
run: ({ interaction, options, user, t }) =>
|
|
73
|
+
challenge(interaction, t, user, options.opponent),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// The same from the menu of a message (right click, Apps): the author of
|
|
77
|
+
// the message is challenged. `type: 'message'` gives `run` the message.
|
|
78
|
+
export const challengeAuthor = command({
|
|
79
|
+
type: 'message',
|
|
80
|
+
name: 'Challenge to a duel',
|
|
81
|
+
locales: { fr: { name: 'Défier en duel' } },
|
|
82
|
+
run: ({ interaction, message, user, t }) =>
|
|
83
|
+
challenge(interaction, t, user, message.author),
|
|
84
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { scenario } from 'chapterjs/test';
|
|
2
|
+
import { challengeAuthor, duel } from './duel/duel';
|
|
3
|
+
import { quizScore } from './quiz/score-menu';
|
|
4
|
+
import { scores, start } from './quiz/quiz';
|
|
5
|
+
|
|
6
|
+
// `chapterjs test` runs these against a Discord in memory: no token, no
|
|
7
|
+
// server. While a function waits (`waitForMessage()`, `waitForClick()`),
|
|
8
|
+
// what the test does next is what it waits for; `bot.endWaits()` ends
|
|
9
|
+
// every wait as if its time was up.
|
|
10
|
+
export default [
|
|
11
|
+
scenario('the first right answer of the quiz wins a point', async bot => {
|
|
12
|
+
const alice = bot.member('alice');
|
|
13
|
+
(await bot.use(start)).says('First to answer in 30 seconds wins!');
|
|
14
|
+
await bot.say('no idea', { by: bot.member('bob') });
|
|
15
|
+
// The question is picked at random: every answer, until the bot speaks.
|
|
16
|
+
for (const answer of ['Paris', '8', 'Jupiter', '1969', 'H2O']) {
|
|
17
|
+
await bot.say(answer, { by: alice });
|
|
18
|
+
if (bot.channel('general').last!.fromBot) break;
|
|
19
|
+
}
|
|
20
|
+
bot.channel('general').last!.says('alice found it, 1 point now.');
|
|
21
|
+
(await bot.use(scores)).hasEmbed('Best players');
|
|
22
|
+
(await bot.use(quizScore, { on: alice }))
|
|
23
|
+
.isEphemeral()
|
|
24
|
+
.says('alice has 1 point, rank 1 on this server.');
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
scenario('nobody finds it in time', async bot => {
|
|
28
|
+
await bot.use(start);
|
|
29
|
+
await bot.endWaits();
|
|
30
|
+
bot.channel('general').last!.says('Time is up!');
|
|
31
|
+
(await bot.use(scores)).isEphemeral().says('Nobody has a point yet');
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
scenario('a duel is accepted by who was challenged only', async bot => {
|
|
35
|
+
const alice = bot.member('alice');
|
|
36
|
+
const bob = bot.member('bob');
|
|
37
|
+
const asked = await bot.use(duel, { opponent: bob }, { by: alice });
|
|
38
|
+
asked.says('challenges you to a duel').hasButton('Accept');
|
|
39
|
+
(await bot.click(asked.button('Accept'), { by: bot.member('carol') })).says(
|
|
40
|
+
'not available'
|
|
41
|
+
);
|
|
42
|
+
const fought = await bot.click(asked.button('Accept'), { by: bob });
|
|
43
|
+
fought.says('🎲 alice rolls').hasNoComponent();
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
scenario('a duel can be declined, or never answered', async bot => {
|
|
47
|
+
const bob = bot.member('bob');
|
|
48
|
+
const asked = await bot.use(duel, { opponent: bob });
|
|
49
|
+
(await bot.click(asked.button('Decline'), { by: bob })).says(
|
|
50
|
+
'bob declined the duel.'
|
|
51
|
+
);
|
|
52
|
+
const again = await bot.use(duel, { opponent: bob });
|
|
53
|
+
await bot.endWaits();
|
|
54
|
+
again.says('bob did not answer: no duel.');
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
scenario('the author of a message is challenged from its menu', async bot => {
|
|
58
|
+
const said = await bot.say('I am the best', { by: bot.member('dave') });
|
|
59
|
+
// The menu of a message passes its author to the duel.
|
|
60
|
+
const asked = await bot.use(challengeAuthor, { on: said });
|
|
61
|
+
const dave = bot.member('dave');
|
|
62
|
+
asked.says(`<@${dave.id}>`).says('challenges you to a duel');
|
|
63
|
+
(await bot.use(duel, { opponent: bot.someone }))
|
|
64
|
+
.isEphemeral()
|
|
65
|
+
.says('not yourself');
|
|
66
|
+
}),
|
|
67
|
+
];
|
|
@@ -0,0 +1,105 @@
|
|
|
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: the person for a private answer, the server
|
|
5
|
+
// otherwise. `commands` is what Discord shows of each slash command; menu
|
|
6
|
+
// commands translate their name in their own file.
|
|
7
|
+
export const en = language({
|
|
8
|
+
locale: 'en-US',
|
|
9
|
+
default: true,
|
|
10
|
+
texts: {
|
|
11
|
+
presenceAnswers: {
|
|
12
|
+
one: '{count} answer found',
|
|
13
|
+
other: '{count} answers found',
|
|
14
|
+
},
|
|
15
|
+
presenceServers: { one: '{count} server', other: '{count} servers' },
|
|
16
|
+
quizAsk: '❓ {question} First to answer in 30 seconds wins!',
|
|
17
|
+
quizNobody: 'Time is up! The answer was {answer}.',
|
|
18
|
+
quizWin: {
|
|
19
|
+
one: '🎉 {user} found it, {count} point now.',
|
|
20
|
+
other: '🎉 {user} found it, {count} points now.',
|
|
21
|
+
},
|
|
22
|
+
questionCapital: 'What is the capital of France?',
|
|
23
|
+
questionPlanets: 'How many planets are there in the solar system?',
|
|
24
|
+
questionLargest: 'What is the largest planet of the solar system?',
|
|
25
|
+
questionMoon: 'In what year did people first walk on the Moon?',
|
|
26
|
+
questionWater: 'What is the chemical formula of water?',
|
|
27
|
+
scoresTitle: 'Best players',
|
|
28
|
+
scoresEmpty: 'Nobody has a point yet: try /quiz start.',
|
|
29
|
+
scoreOf: {
|
|
30
|
+
one: '{user} has {count} point, rank {rank} on this server.',
|
|
31
|
+
other: '{user} has {count} points, rank {rank} on this server.',
|
|
32
|
+
},
|
|
33
|
+
scoreNone: '{user} has no point yet.',
|
|
34
|
+
duelAccept: 'Accept',
|
|
35
|
+
duelDecline: 'Decline',
|
|
36
|
+
duelNobody: 'Challenge someone else: not yourself, and not a bot.',
|
|
37
|
+
duelAsk:
|
|
38
|
+
'⚔️ {opponent}, {user} challenges you to a duel! You have a minute.',
|
|
39
|
+
duelExpired: '{opponent} did not answer: no duel.',
|
|
40
|
+
duelDeclined: '{opponent} declined the duel.',
|
|
41
|
+
duelRolls: '🎲 {user} rolls {mine}, {opponent} rolls {theirs}.',
|
|
42
|
+
duelWinner: '{user} wins!',
|
|
43
|
+
duelDraw: 'A draw!',
|
|
44
|
+
},
|
|
45
|
+
commands: {
|
|
46
|
+
'quiz start': {
|
|
47
|
+
description: 'Asks a question: the first right answer wins',
|
|
48
|
+
},
|
|
49
|
+
'quiz scores': { description: 'The best players of the server' },
|
|
50
|
+
duel: {
|
|
51
|
+
description: 'Challenges someone to a dice duel',
|
|
52
|
+
options: { opponent: { description: 'Who to challenge' } },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const fr = language({
|
|
58
|
+
locale: 'fr',
|
|
59
|
+
texts: {
|
|
60
|
+
presenceAnswers: {
|
|
61
|
+
one: '{count} réponse trouvée',
|
|
62
|
+
other: '{count} réponses trouvées',
|
|
63
|
+
},
|
|
64
|
+
presenceServers: { one: '{count} serveur', other: '{count} serveurs' },
|
|
65
|
+
quizAsk:
|
|
66
|
+
'❓ {question} La première bonne réponse dans les 30 secondes gagne !',
|
|
67
|
+
quizNobody: 'Temps écoulé ! La réponse était {answer}.',
|
|
68
|
+
quizWin: {
|
|
69
|
+
one: '🎉 {user} a trouvé, {count} point maintenant.',
|
|
70
|
+
other: '🎉 {user} a trouvé, {count} points maintenant.',
|
|
71
|
+
},
|
|
72
|
+
questionCapital: 'Quelle est la capitale de la France ?',
|
|
73
|
+
questionPlanets: 'Combien de planètes compte le système solaire ?',
|
|
74
|
+
questionLargest: 'Quelle est la plus grande planète du système solaire ?',
|
|
75
|
+
questionMoon:
|
|
76
|
+
'En quelle année a-t-on marché sur la Lune pour la première fois ?',
|
|
77
|
+
questionWater: 'Quelle est la formule chimique de l’eau ?',
|
|
78
|
+
scoresTitle: 'Meilleurs joueurs',
|
|
79
|
+
scoresEmpty: 'Personne n’a encore de point : essayez /quiz start.',
|
|
80
|
+
scoreOf: {
|
|
81
|
+
one: '{user} a {count} point, {rank}e sur ce serveur.',
|
|
82
|
+
other: '{user} a {count} points, {rank}e sur ce serveur.',
|
|
83
|
+
},
|
|
84
|
+
scoreNone: '{user} n’a encore aucun point.',
|
|
85
|
+
duelAccept: 'Accepter',
|
|
86
|
+
duelDecline: 'Refuser',
|
|
87
|
+
duelNobody: 'Défiez quelqu’un d’autre : pas vous-même, ni un bot.',
|
|
88
|
+
duelAsk: '⚔️ {opponent}, {user} vous défie en duel ! Vous avez une minute.',
|
|
89
|
+
duelExpired: '{opponent} n’a pas répondu : pas de duel.',
|
|
90
|
+
duelDeclined: '{opponent} a refusé le duel.',
|
|
91
|
+
duelRolls: '🎲 {user} fait {mine}, {opponent} fait {theirs}.',
|
|
92
|
+
duelWinner: '{user} gagne !',
|
|
93
|
+
duelDraw: 'Égalité !',
|
|
94
|
+
},
|
|
95
|
+
commands: {
|
|
96
|
+
'quiz start': {
|
|
97
|
+
description: 'Pose une question : la première bonne réponse gagne',
|
|
98
|
+
},
|
|
99
|
+
'quiz scores': { description: 'Les meilleurs joueurs du serveur' },
|
|
100
|
+
duel: {
|
|
101
|
+
description: 'Défie quelqu’un en duel de dés',
|
|
102
|
+
options: { opponent: { description: 'Qui défier' } },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { presence } from 'chapterjs';
|
|
2
|
+
import { answered } from './quiz/scores';
|
|
3
|
+
|
|
4
|
+
// What the bot shows under its name: three activities in turn, each for
|
|
5
|
+
// its `for`. A `name` can be a function: it runs each time its activity is
|
|
6
|
+
// shown, with `guilds` (the servers of the bot) and `t` (the texts in the
|
|
7
|
+
// default language).
|
|
8
|
+
export default presence({
|
|
9
|
+
activities: [
|
|
10
|
+
{ type: 'playing', name: '/quiz start', for: '30s' },
|
|
11
|
+
{
|
|
12
|
+
type: 'watching',
|
|
13
|
+
name: async ({ t }) =>
|
|
14
|
+
t('presenceAnswers', { count: (await answered.get('total')) ?? 0 }),
|
|
15
|
+
for: '30s',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'competing',
|
|
19
|
+
name: ({ t, guilds }) => t('presenceServers', { count: guilds }),
|
|
20
|
+
for: '30s',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The questions of the quiz: what is asked (a key of the language files),
|
|
2
|
+
// what counts as the answer, and the answer shown when nobody found it.
|
|
3
|
+
export const QUESTIONS = [
|
|
4
|
+
{ text: 'questionCapital', answer: /^paris$/i, shown: 'Paris' },
|
|
5
|
+
{ text: 'questionPlanets', answer: /^(8|eight|huit)$/i, shown: '8' },
|
|
6
|
+
{ text: 'questionLargest', answer: /^jupiter$/i, shown: 'Jupiter' },
|
|
7
|
+
{ text: 'questionMoon', answer: /^1969$/, shown: '1969' },
|
|
8
|
+
{ text: 'questionWater', answer: /^h2o$/i, shown: 'H2O' },
|
|
9
|
+
] as const;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { command, userMention } from 'chapterjs';
|
|
2
|
+
import { QUESTIONS } from './questions';
|
|
3
|
+
import { answered, quizScores } from './scores';
|
|
4
|
+
|
|
5
|
+
// `/quiz start`: a question to the channel, and the first right answer
|
|
6
|
+
// wins a point. `waitForMessage()` resolves with that message, or `null`
|
|
7
|
+
// after 30 seconds. One quiz at a time per channel: the cooldown.
|
|
8
|
+
export const start = command({
|
|
9
|
+
name: 'quiz start',
|
|
10
|
+
cooldown: { channel: '30s' },
|
|
11
|
+
async run({ interaction, channel, t }) {
|
|
12
|
+
const question = QUESTIONS[Math.floor(Math.random() * QUESTIONS.length)]!;
|
|
13
|
+
await interaction.reply(t('quizAsk', { question: t(question.text) }));
|
|
14
|
+
const winner = await channel.waitForMessage({
|
|
15
|
+
matches: message => question.answer.test(message.content.trim()),
|
|
16
|
+
within: '30s',
|
|
17
|
+
});
|
|
18
|
+
if (!winner) {
|
|
19
|
+
await channel.send(t('quizNobody', { answer: question.shown }));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const points = await quizScores.add(winner.author.id, 1);
|
|
23
|
+
await answered.increment('total');
|
|
24
|
+
await winner.reply(
|
|
25
|
+
t('quizWin', { user: winner.author.username, count: points })
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// `/quiz scores`: the best players of the server.
|
|
31
|
+
export const scores = command({
|
|
32
|
+
name: 'quiz scores',
|
|
33
|
+
async run({ interaction, t }) {
|
|
34
|
+
const best = await quizScores.top(10);
|
|
35
|
+
if (best.length === 0) {
|
|
36
|
+
await interaction.reply({ content: t('scoresEmpty'), ephemeral: true });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// In an embed, a mention shows the name and notifies nobody.
|
|
40
|
+
const lines = best.map(
|
|
41
|
+
(entry, index) => `${index + 1}. ${userMention(entry.id)}: ${entry.score}`
|
|
42
|
+
);
|
|
43
|
+
await interaction.reply({
|
|
44
|
+
embeds: [{ title: t('scoresTitle'), description: lines.join('\n') }],
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { command } from 'chapterjs';
|
|
2
|
+
import { quizScores } from './scores';
|
|
3
|
+
|
|
4
|
+
// A menu command: right click someone, Apps, "Quiz score". `type: 'user'`
|
|
5
|
+
// puts it in the menu of a person, and `run` receives them as `target`.
|
|
6
|
+
// Its name is translated here: language files only describe slash commands.
|
|
7
|
+
export const quizScore = command({
|
|
8
|
+
type: 'user',
|
|
9
|
+
name: 'Quiz score',
|
|
10
|
+
locales: { fr: { name: 'Score au quiz' } },
|
|
11
|
+
ephemeral: true,
|
|
12
|
+
async run({ interaction, target, t }) {
|
|
13
|
+
const points = await quizScores.score(target.id);
|
|
14
|
+
const rank = await quizScores.rankOf(target.id);
|
|
15
|
+
await interaction.reply(
|
|
16
|
+
points === undefined || rank === undefined
|
|
17
|
+
? t('scoreNone', { user: target.username })
|
|
18
|
+
: t('scoreOf', { user: target.username, count: points, rank })
|
|
19
|
+
);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ranking, store } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// One point per right answer, per server, always in order.
|
|
4
|
+
export const quizScores = ranking();
|
|
5
|
+
|
|
6
|
+
// How many answers were found, for the whole bot: what the presence shows.
|
|
7
|
+
export const answered = store<number>({ scope: 'global' });
|
|
@@ -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
|
+
}
|
|
@@ -9,6 +9,7 @@ export const en = language({
|
|
|
9
9
|
texts: {
|
|
10
10
|
cantModerate:
|
|
11
11
|
'You can not moderate {user}: their highest role is above yours.',
|
|
12
|
+
notMember: '{user} is not in this server.',
|
|
12
13
|
timedOut: '{user} is timed out until {until}. Reason: {reason}',
|
|
13
14
|
noReason: 'none given',
|
|
14
15
|
notTimedOut: '{user} is not timed out.',
|
|
@@ -82,6 +83,7 @@ export const fr = language({
|
|
|
82
83
|
texts: {
|
|
83
84
|
cantModerate:
|
|
84
85
|
'Vous ne pouvez pas modérer {user} : son rôle le plus haut est au-dessus du vôtre.',
|
|
86
|
+
notMember: '{user} n’est pas sur ce serveur.',
|
|
85
87
|
timedOut:
|
|
86
88
|
'{user} est exclu temporairement jusqu’à {until}. Raison : {reason}',
|
|
87
89
|
noReason: 'aucune',
|
|
@@ -31,7 +31,7 @@ export default [
|
|
|
31
31
|
async bot => {
|
|
32
32
|
const alice = bot.member('alice');
|
|
33
33
|
const refused = await bot.use(timeout, { user: alice, duration: 5 });
|
|
34
|
-
refused.isEphemeral().says('
|
|
34
|
+
refused.isEphemeral().says('Moderate Members');
|
|
35
35
|
|
|
36
36
|
const by = { by: bot.member('mod', mod) };
|
|
37
37
|
const owner = await bot.use(
|
|
@@ -41,6 +41,12 @@ export default [
|
|
|
41
41
|
);
|
|
42
42
|
owner.isEphemeral().says('You can not moderate');
|
|
43
43
|
|
|
44
|
+
// Someone who left is no member any more.
|
|
45
|
+
const gone = await bot.join('gone');
|
|
46
|
+
await bot.leave(gone);
|
|
47
|
+
const left = await bot.use(timeout, { user: gone, duration: 5 }, by);
|
|
48
|
+
left.isEphemeral().says('is not in this server');
|
|
49
|
+
|
|
44
50
|
const done = await bot.use(timeout, { user: alice, duration: 60 }, by);
|
|
45
51
|
done.says('is timed out');
|
|
46
52
|
assert.equal(alice.timedOut, true);
|
|
@@ -30,7 +30,15 @@ export default command({
|
|
|
30
30
|
reason: { type: 'string', maxLength: 200 },
|
|
31
31
|
},
|
|
32
32
|
async run({ interaction, options, guild, member, t }) {
|
|
33
|
-
|
|
33
|
+
// Someone who left, or never joined, is no member: Discord says so.
|
|
34
|
+
const target = await guild.fetchMember(options.user).catch(() => null);
|
|
35
|
+
if (!target) {
|
|
36
|
+
await interaction.reply({
|
|
37
|
+
content: t('notMember', { user: options.user.toString() }),
|
|
38
|
+
ephemeral: true,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
34
42
|
if (!canModerate(target, member)) {
|
|
35
43
|
await interaction.reply({
|
|
36
44
|
content: t('cantModerate', { user: target.toString() }),
|
|
@@ -60,7 +68,14 @@ export const untimeout = command({
|
|
|
60
68
|
user: { type: 'user', required: true },
|
|
61
69
|
},
|
|
62
70
|
async run({ interaction, options, guild, member, t }) {
|
|
63
|
-
const target = await guild.fetchMember(options.user);
|
|
71
|
+
const target = await guild.fetchMember(options.user).catch(() => null);
|
|
72
|
+
if (!target) {
|
|
73
|
+
await interaction.reply({
|
|
74
|
+
content: t('notMember', { user: options.user.toString() }),
|
|
75
|
+
ephemeral: true,
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
64
79
|
// The same rule as /timeout: nobody lifts what someone above them set.
|
|
65
80
|
if (!canModerate(target, member)) {
|
|
66
81
|
await interaction.reply({
|
|
@@ -8,7 +8,7 @@ import panel, { open } from './panel';
|
|
|
8
8
|
export default [
|
|
9
9
|
scenario('the panel is posted by the staff only', async bot => {
|
|
10
10
|
const refused = await bot.use(panel);
|
|
11
|
-
refused.isEphemeral().says('
|
|
11
|
+
refused.isEphemeral().says('Manage Threads');
|
|
12
12
|
|
|
13
13
|
const staff = bot.member('staff', { permissions: ['ManageThreads'] });
|
|
14
14
|
const reply = await bot.use(panel, {}, { by: staff });
|