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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quentin Truffy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.js
CHANGED
|
@@ -67,10 +67,13 @@ if (template === undefined) {
|
|
|
67
67
|
p.cancel('No template found. Reinstall create-chapter and try again.');
|
|
68
68
|
process.exit(1);
|
|
69
69
|
}
|
|
70
|
+
// The exact version, not a range: `create chapter@X` makes a project on
|
|
71
|
+
// `chapterjs@X`, and the project only changes version when its developer
|
|
72
|
+
// runs the command `chapterjs dev` gives them.
|
|
70
73
|
await createProject({
|
|
71
74
|
dir,
|
|
72
75
|
template,
|
|
73
|
-
chapterjsVersion:
|
|
76
|
+
chapterjsVersion: version,
|
|
74
77
|
packageManager: pm,
|
|
75
78
|
});
|
|
76
79
|
p.log.success(`Project created in ${resolve(dir)}`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "community-bot",
|
|
3
|
+
"description": "Welcome messages, roles people pick, and weekly server stats",
|
|
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,49 @@
|
|
|
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, and the default language for a task.
|
|
6
|
+
export const en = language({
|
|
7
|
+
locale: 'en-US',
|
|
8
|
+
default: true,
|
|
9
|
+
texts: {
|
|
10
|
+
welcome: 'Welcome to {server}, {user}!',
|
|
11
|
+
goodbye: '{name} left. See you!',
|
|
12
|
+
pickRoles: 'Pick the roles you want:',
|
|
13
|
+
rolesPlaceholder: 'Your roles',
|
|
14
|
+
noRoles: 'You have none of these roles now.',
|
|
15
|
+
rolesUpdated: 'Your roles: {roles}.',
|
|
16
|
+
rolesMissing:
|
|
17
|
+
'These roles do not exist in this server yet: {roles}. Ask an admin to create them.',
|
|
18
|
+
statsTitle: 'A new week on {server}',
|
|
19
|
+
members: 'Members',
|
|
20
|
+
textChannels: 'Text channels',
|
|
21
|
+
voiceChannels: 'Voice channels',
|
|
22
|
+
roles: 'Roles',
|
|
23
|
+
},
|
|
24
|
+
commands: {
|
|
25
|
+
roles: { description: 'Picks the roles you want' },
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const fr = language({
|
|
30
|
+
locale: 'fr',
|
|
31
|
+
texts: {
|
|
32
|
+
welcome: 'Bienvenue sur {server}, {user} !',
|
|
33
|
+
goodbye: '{name} est parti. À bientôt !',
|
|
34
|
+
pickRoles: 'Choisissez vos rôles :',
|
|
35
|
+
rolesPlaceholder: 'Vos rôles',
|
|
36
|
+
noRoles: 'Vous n’avez plus aucun de ces rôles.',
|
|
37
|
+
rolesUpdated: 'Vos rôles : {roles}.',
|
|
38
|
+
rolesMissing:
|
|
39
|
+
'Ces rôles n’existent pas encore sur ce serveur : {roles}. Demandez à un admin de les créer.',
|
|
40
|
+
statsTitle: 'Une nouvelle semaine sur {server}',
|
|
41
|
+
members: 'Membres',
|
|
42
|
+
textChannels: 'Salons textuels',
|
|
43
|
+
voiceChannels: 'Salons vocaux',
|
|
44
|
+
roles: 'Rôles',
|
|
45
|
+
},
|
|
46
|
+
commands: {
|
|
47
|
+
roles: { description: 'Choisit les rôles que vous voulez' },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { command, select } from 'chapterjs';
|
|
2
|
+
|
|
3
|
+
// The roles people may give themselves: change the list, and create them
|
|
4
|
+
// in your server with the same names.
|
|
5
|
+
const SELF_ROLES = ['Announcements', 'Events', 'Gaming'] as const;
|
|
6
|
+
|
|
7
|
+
// /roles shows the menu, with what the person already has picked.
|
|
8
|
+
export default command({
|
|
9
|
+
name: 'roles',
|
|
10
|
+
ephemeral: true,
|
|
11
|
+
async run({ interaction, member, t }) {
|
|
12
|
+
const mine = member.roles
|
|
13
|
+
.map(role => role.name)
|
|
14
|
+
.filter((name): name is (typeof SELF_ROLES)[number] =>
|
|
15
|
+
(SELF_ROLES as readonly string[]).includes(name)
|
|
16
|
+
);
|
|
17
|
+
await interaction.reply({
|
|
18
|
+
content: t('pickRoles'),
|
|
19
|
+
components: [pick({ defaults: mine })],
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// A select menu is named by its export. `values` is what was picked, typed
|
|
25
|
+
// from `options`; `min: 0` lets the person remove every role.
|
|
26
|
+
export const pick = select({
|
|
27
|
+
placeholder: ({ t }) => t('rolesPlaceholder'),
|
|
28
|
+
options: SELF_ROLES,
|
|
29
|
+
min: 0,
|
|
30
|
+
max: SELF_ROLES.length,
|
|
31
|
+
async run({ interaction, values, member, guild, t }) {
|
|
32
|
+
const roles = [...guild.roles.values()];
|
|
33
|
+
const missing: string[] = [];
|
|
34
|
+
for (const name of SELF_ROLES) {
|
|
35
|
+
const role = roles.find(role => role.name === name);
|
|
36
|
+
if (!role) {
|
|
37
|
+
missing.push(name);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const wanted = values.includes(name);
|
|
41
|
+
if (wanted && !member.hasRole(role))
|
|
42
|
+
await member.addRole(role, 'Picked with /roles');
|
|
43
|
+
if (!wanted && member.hasRole(role))
|
|
44
|
+
await member.removeRole(role, 'Removed with /roles');
|
|
45
|
+
}
|
|
46
|
+
await interaction.update({
|
|
47
|
+
content:
|
|
48
|
+
values.length === 0
|
|
49
|
+
? t('noRoles')
|
|
50
|
+
: t('rolesUpdated', { roles: values.join(', ') }),
|
|
51
|
+
components: [],
|
|
52
|
+
});
|
|
53
|
+
if (missing.length > 0) {
|
|
54
|
+
await interaction.followUp({
|
|
55
|
+
content: t('rolesMissing', { roles: missing.join(', ') }),
|
|
56
|
+
ephemeral: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { event } from 'chapterjs';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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.
|
|
6
7
|
export default event({
|
|
7
8
|
name: 'ready',
|
|
8
9
|
run({ user, guilds }) {
|
|
@@ -10,25 +11,18 @@ export default event({
|
|
|
10
11
|
},
|
|
11
12
|
});
|
|
12
13
|
|
|
13
|
-
//
|
|
14
|
-
// the name of its export is the name of the task.
|
|
14
|
+
// A command is a file like this one, anywhere in src/, with command():
|
|
15
15
|
//
|
|
16
|
-
// import {
|
|
16
|
+
// import { command } from 'chapterjs';
|
|
17
17
|
//
|
|
18
|
-
// export
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// export default command({
|
|
19
|
+
// name: 'ping',
|
|
20
|
+
// description: 'Replies with Pong!',
|
|
21
|
+
// async run({ interaction }) {
|
|
22
|
+
// await interaction.reply('Pong!');
|
|
22
23
|
// },
|
|
23
24
|
// });
|
|
24
25
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// export const pong = event({
|
|
30
|
-
// name: 'messageCreate',
|
|
31
|
-
// async run({ message }) {
|
|
32
|
-
// if (message.content === '!ping') await message.reply('Pong!');
|
|
33
|
-
// },
|
|
34
|
-
// });
|
|
26
|
+
// Save it, and /ping shows up in your test server. The other templates of
|
|
27
|
+
// `pnpm create chapter` show tickets, music, moderation and a community
|
|
28
|
+
// bot; the docs at https://www.chapterjs.org walk through everything.
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
"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,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
|
+
});
|