create-chapterjs 0.2.2 → 0.3.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/choose.js +27 -0
- package/dist/index.js +98 -4
- package/dist/node-version.js +13 -0
- package/dist/package-manager.js +59 -0
- package/dist/project.js +82 -0
- package/dist/templates.js +31 -0
- package/package.json +19 -5
- package/templates/community/.env.example +4 -0
- package/templates/community/_gitignore +5 -0
- package/templates/community/package.json +24 -0
- package/templates/community/src/community.test.ts +48 -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/.env.example +4 -0
- package/templates/default/_gitignore +5 -0
- package/templates/default/package.json +24 -0
- package/templates/default/src/events/ready.ts +28 -0
- package/templates/default/tsconfig.json +20 -0
- package/templates/moderation/.env.example +4 -0
- package/templates/moderation/_gitignore +5 -0
- package/templates/moderation/package.json +24 -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/moderation.test.ts +76 -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 +24 -0
- package/templates/music/src/music/leave.ts +18 -0
- package/templates/music/src/music/messages.ts +83 -0
- package/templates/music/src/music/music.test.ts +29 -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 +24 -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/src/tickets/tickets.test.ts +72 -0
- package/templates/tickets/tsconfig.json +20 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ChannelType, task } from 'chapterjs';
|
|
2
|
+
import { communityChannel } from '../welcome/channel';
|
|
3
|
+
|
|
4
|
+
// A task runs by itself: every Monday at 9:00, in the timezone given. The
|
|
5
|
+
// name of the export is the name of the task. `t` is the default language.
|
|
6
|
+
export const weekly = task({
|
|
7
|
+
cron: '0 9 * * 1',
|
|
8
|
+
timezone: 'Europe/Paris',
|
|
9
|
+
async run({ guilds, t }) {
|
|
10
|
+
for (const guild of guilds.values()) {
|
|
11
|
+
const channel = communityChannel(guild, 'general');
|
|
12
|
+
if (!channel) continue;
|
|
13
|
+
const channels = [...guild.channels.values()];
|
|
14
|
+
await channel.send({
|
|
15
|
+
embeds: [
|
|
16
|
+
{
|
|
17
|
+
title: t('statsTitle', { server: guild.name }),
|
|
18
|
+
color: 0xc026d3,
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
name: t('members'),
|
|
22
|
+
value: String(guild.memberCount ?? '?'),
|
|
23
|
+
inline: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: t('textChannels'),
|
|
27
|
+
value: String(
|
|
28
|
+
channels.filter(c => c.type === ChannelType.GuildText).length
|
|
29
|
+
),
|
|
30
|
+
inline: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: t('voiceChannels'),
|
|
34
|
+
value: String(
|
|
35
|
+
channels.filter(c => c.type === ChannelType.GuildVoice).length
|
|
36
|
+
),
|
|
37
|
+
inline: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: t('roles'),
|
|
41
|
+
value: String(guild.roles.size - 1),
|
|
42
|
+
inline: true,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Guild, TextChannel } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// Plain code, shared by the other files: the channel the bot writes in.
|
|
4
|
+
// The one named like `name` when there is one, else the system channel of
|
|
5
|
+
// the server (where Discord itself says who joined), else nothing.
|
|
6
|
+
export function communityChannel(
|
|
7
|
+
guild: Guild,
|
|
8
|
+
name: string
|
|
9
|
+
): TextChannel | null {
|
|
10
|
+
const channels = [...guild.channels.values()];
|
|
11
|
+
const named = channels.find(
|
|
12
|
+
channel => channel.isText() && channel.name === name
|
|
13
|
+
);
|
|
14
|
+
if (named?.isText()) return named;
|
|
15
|
+
const system = guild.systemChannelId
|
|
16
|
+
? guild.channels.get(guild.systemChannelId)
|
|
17
|
+
: undefined;
|
|
18
|
+
return system?.isText() ? system : null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { embed, event, type GuildMember } from 'chapterjs';
|
|
2
|
+
import { communityChannel } from './channel';
|
|
3
|
+
|
|
4
|
+
// Runs when someone joins the server. It needs the Server Members Intent:
|
|
5
|
+
// chapterjs dev tells you where to enable it the first time.
|
|
6
|
+
export default event({
|
|
7
|
+
name: 'memberJoin',
|
|
8
|
+
async run({ member, guild, t }) {
|
|
9
|
+
const channel = communityChannel(guild, 'welcome');
|
|
10
|
+
if (!channel) return;
|
|
11
|
+
await channel.send({
|
|
12
|
+
content: t('welcome', { user: member.toString(), server: guild.name }),
|
|
13
|
+
embeds: [card(member)],
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Someone left. `member` is the member as it was, when the bot knew them;
|
|
19
|
+
// `user` is always there.
|
|
20
|
+
export const goodbye = event({
|
|
21
|
+
name: 'memberLeave',
|
|
22
|
+
async run({ user, member, guild, t }) {
|
|
23
|
+
const channel = communityChannel(guild, 'welcome');
|
|
24
|
+
if (!channel) return;
|
|
25
|
+
await channel.send(
|
|
26
|
+
t('goodbye', { name: member?.displayName ?? user.displayName })
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// An embed is a function of what it shows, reusable from any file.
|
|
32
|
+
export const card = embed((member: GuildMember) => ({
|
|
33
|
+
title: member.displayName,
|
|
34
|
+
description: `You are member #${member.guild.memberCount ?? '?'}.`,
|
|
35
|
+
thumbnail: { url: member.avatarURL() },
|
|
36
|
+
color: 0xc026d3,
|
|
37
|
+
}));
|
|
@@ -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,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chapter-bot",
|
|
3
|
+
"description": "A bot that starts: one event, ready to grow",
|
|
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,28 @@
|
|
|
1
|
+
import { event } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// Your bot is made of the files of src/: each one exports what it declares
|
|
4
|
+
// with a function of 'chapterjs' (event, command, button, task...), and
|
|
5
|
+
// chapterjs dev runs it. This one is an event: `name` says which, and types
|
|
6
|
+
// what `run` receives. `ready` runs once, when the bot is connected.
|
|
7
|
+
export default event({
|
|
8
|
+
name: 'ready',
|
|
9
|
+
run({ user, guilds }) {
|
|
10
|
+
console.log(`${user.username} is online in ${guilds.size} server(s)`);
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// A command is a file like this one, anywhere in src/, with command():
|
|
15
|
+
//
|
|
16
|
+
// import { command } from 'chapterjs';
|
|
17
|
+
//
|
|
18
|
+
// export default command({
|
|
19
|
+
// name: 'ping',
|
|
20
|
+
// description: 'Replies with Pong!',
|
|
21
|
+
// async run({ interaction }) {
|
|
22
|
+
// await interaction.reply('Pong!');
|
|
23
|
+
// },
|
|
24
|
+
// });
|
|
25
|
+
//
|
|
26
|
+
// Save it, and /ping shows up in your test server. The other templates of
|
|
27
|
+
// `pnpm create chapterjs` show tickets, music, moderation and a community
|
|
28
|
+
// bot; the docs at https://www.chapterjs.org walk through everything.
|
|
@@ -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,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "moderation-bot",
|
|
3
|
+
"description": "Timeouts, purge, slowmode, lock, and a log of every moderation action",
|
|
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,67 @@
|
|
|
1
|
+
import { command } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// /slowmode sets how long people wait between two messages in the channel.
|
|
4
|
+
// 0 removes it.
|
|
5
|
+
export const slowmode = command({
|
|
6
|
+
name: 'slowmode',
|
|
7
|
+
permissions: ['ManageChannels'],
|
|
8
|
+
options: {
|
|
9
|
+
seconds: { type: 'integer', min: 0, max: 21600, required: true },
|
|
10
|
+
},
|
|
11
|
+
async run({ interaction, options, channel, member, t }) {
|
|
12
|
+
if (!channel.isText()) {
|
|
13
|
+
await interaction.reply({ content: t('notHere'), ephemeral: true });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await channel.edit(
|
|
17
|
+
{ rateLimitPerUser: options.seconds },
|
|
18
|
+
`By ${member.user.username}`
|
|
19
|
+
);
|
|
20
|
+
await interaction.reply(
|
|
21
|
+
options.seconds === 0
|
|
22
|
+
? t('slowmodeOff')
|
|
23
|
+
: t('slowmodeOn', { count: options.seconds })
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// /lock keeps everyone from writing in the channel, /unlock gives it back.
|
|
29
|
+
// Both change the permissions of @everyone on the channel only.
|
|
30
|
+
export const lock = command({
|
|
31
|
+
name: 'lock',
|
|
32
|
+
permissions: ['ManageChannels'],
|
|
33
|
+
options: {
|
|
34
|
+
reason: { type: 'string', maxLength: 200 },
|
|
35
|
+
},
|
|
36
|
+
async run({ interaction, options, channel, guild, member, t }) {
|
|
37
|
+
if (!channel.isText()) {
|
|
38
|
+
await interaction.reply({ content: t('notHere'), ephemeral: true });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
await channel.setPermissions(
|
|
42
|
+
guild.everyoneRole,
|
|
43
|
+
{ deny: ['SendMessages', 'CreatePublicThreads', 'CreatePrivateThreads'] },
|
|
44
|
+
options.reason ?? `Locked by ${member.user.username}`
|
|
45
|
+
);
|
|
46
|
+
await interaction.reply(
|
|
47
|
+
t('locked', { reason: options.reason ?? t('noReason') })
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const unlock = command({
|
|
53
|
+
name: 'unlock',
|
|
54
|
+
permissions: ['ManageChannels'],
|
|
55
|
+
async run({ interaction, channel, guild, member, t }) {
|
|
56
|
+
if (!channel.isText()) {
|
|
57
|
+
await interaction.reply({ content: t('notHere'), ephemeral: true });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Deleting the overwrite gives @everyone back what the server says.
|
|
61
|
+
await channel.deletePermissions(
|
|
62
|
+
guild.everyoneRole,
|
|
63
|
+
`Unlocked by ${member.user.username}`
|
|
64
|
+
);
|
|
65
|
+
await interaction.reply(t('unlocked'));
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AuditLogEvent, event } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// Every moderation action, whoever did it and however (a command of this
|
|
4
|
+
// bot, another bot, the Discord app), reaches the audit log of the server.
|
|
5
|
+
// This posts the ones below in the channel named mod-log. Discord only
|
|
6
|
+
// sends the audit log to a bot with the View Audit Log permission.
|
|
7
|
+
const ACTIONS: Partial<Record<number, string>> = {
|
|
8
|
+
[AuditLogEvent.MemberKick]: 'kicked',
|
|
9
|
+
[AuditLogEvent.MemberBanAdd]: 'banned',
|
|
10
|
+
[AuditLogEvent.MemberBanRemove]: 'unbanned',
|
|
11
|
+
[AuditLogEvent.MemberUpdate]: 'updated (timeout, nickname...)',
|
|
12
|
+
[AuditLogEvent.MessageBulkDelete]: 'purged messages in',
|
|
13
|
+
[AuditLogEvent.ChannelOverwriteUpdate]: 'changed the permissions of',
|
|
14
|
+
[AuditLogEvent.ChannelOverwriteCreate]: 'changed the permissions of',
|
|
15
|
+
[AuditLogEvent.ChannelOverwriteDelete]: 'changed the permissions of',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default event({
|
|
19
|
+
name: 'auditLogEntryCreate',
|
|
20
|
+
async run({ entry, guild }) {
|
|
21
|
+
const action = ACTIONS[entry.actionType];
|
|
22
|
+
if (!action || !entry.userId) return;
|
|
23
|
+
const log = [...guild.channels.values()].find(
|
|
24
|
+
channel => channel.isText() && channel.name === 'mod-log'
|
|
25
|
+
);
|
|
26
|
+
if (!log?.isText()) return;
|
|
27
|
+
const who = `<@${entry.userId}>`;
|
|
28
|
+
const target = entry.targetId
|
|
29
|
+
? guild.channels.has(entry.targetId)
|
|
30
|
+
? `<#${entry.targetId}>`
|
|
31
|
+
: `<@${entry.targetId}>`
|
|
32
|
+
: 'something';
|
|
33
|
+
await log.send(
|
|
34
|
+
`🛡️ ${who} ${action} ${target}${entry.reason ? `: ${entry.reason}` : ''}`
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -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,76 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { scenario } from 'chapterjs/test';
|
|
3
|
+
import { lock, slowmode } from './channel';
|
|
4
|
+
import purge from './purge';
|
|
5
|
+
import timeout from './timeout';
|
|
6
|
+
import warn, { list } from './warn';
|
|
7
|
+
|
|
8
|
+
// `chapterjs test` runs these against a Discord in memory: no token, no
|
|
9
|
+
// server. Each scenario starts on a fresh server, with empty stores.
|
|
10
|
+
const mod = {
|
|
11
|
+
permissions: ['ModerateMembers', 'ManageMessages', 'ManageChannels'],
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export default [
|
|
15
|
+
scenario('warnings add up, per member', async bot => {
|
|
16
|
+
const alice = bot.member('alice');
|
|
17
|
+
const by = { by: bot.member('mod', mod) };
|
|
18
|
+
const first = await bot.use(warn, { user: alice, reason: 'spam' }, by);
|
|
19
|
+
first.says('They have 1 warning.');
|
|
20
|
+
const second = await bot.use(warn, { user: alice, reason: 'again' }, by);
|
|
21
|
+
second.says('They have 2 warnings.');
|
|
22
|
+
|
|
23
|
+
const listed = await bot.use(list, { user: alice }, by);
|
|
24
|
+
listed.isEphemeral().says('has 2 warnings').says('spam').says('again');
|
|
25
|
+
const nobody = await bot.use(list, { user: bot.member('bob') }, by);
|
|
26
|
+
nobody.says('has no warning');
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
scenario(
|
|
30
|
+
'a timeout needs the permission, and spares the owner',
|
|
31
|
+
async bot => {
|
|
32
|
+
const alice = bot.member('alice');
|
|
33
|
+
const refused = await bot.use(timeout, { user: alice, duration: 5 });
|
|
34
|
+
refused.isEphemeral().says('ModerateMembers');
|
|
35
|
+
|
|
36
|
+
const by = { by: bot.member('mod', mod) };
|
|
37
|
+
const owner = await bot.use(
|
|
38
|
+
timeout,
|
|
39
|
+
{ user: bot.owner, duration: 5 },
|
|
40
|
+
by
|
|
41
|
+
);
|
|
42
|
+
owner.isEphemeral().says('You can not moderate');
|
|
43
|
+
|
|
44
|
+
const done = await bot.use(timeout, { user: alice, duration: 60 }, by);
|
|
45
|
+
done.says('is timed out');
|
|
46
|
+
assert.equal(alice.timedOut, true);
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
|
|
50
|
+
scenario('purge deletes the last messages', async bot => {
|
|
51
|
+
await bot.say('one');
|
|
52
|
+
await bot.say('two');
|
|
53
|
+
await bot.say('three');
|
|
54
|
+
const by = { by: bot.member('mod', mod) };
|
|
55
|
+
const reply = await bot.use(purge, { count: 2 }, by);
|
|
56
|
+
reply.isDeferred().isEphemeral().says('2 messages deleted.');
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
bot.channel('general').messages.map(message => message.content),
|
|
59
|
+
['one']
|
|
60
|
+
);
|
|
61
|
+
}),
|
|
62
|
+
|
|
63
|
+
scenario('slowmode and lock change the channel', async bot => {
|
|
64
|
+
const by = { by: bot.member('mod', mod) };
|
|
65
|
+
const slow = await bot.use(slowmode, { seconds: 5 }, by);
|
|
66
|
+
slow.says('one message every 5 seconds');
|
|
67
|
+
assert.equal(bot.channel('general').slowmode, 5);
|
|
68
|
+
|
|
69
|
+
const locked = await bot.use(lock, { reason: 'calm down' }, by);
|
|
70
|
+
locked.says('locked').says('calm down');
|
|
71
|
+
assert.equal(
|
|
72
|
+
bot.channel('general').allows(bot.someone, 'SendMessages'),
|
|
73
|
+
false
|
|
74
|
+
);
|
|
75
|
+
}),
|
|
76
|
+
];
|
|
@@ -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
|
+
});
|