create-chapter 0.2.1 → 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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +4 -1
  3. package/package.json +1 -1
  4. package/templates/community/.env.example +4 -0
  5. package/templates/community/_gitignore +5 -0
  6. package/templates/community/package.json +23 -0
  7. package/templates/community/src/messages.ts +49 -0
  8. package/templates/community/src/presence.ts +6 -0
  9. package/templates/community/src/roles/roles.ts +60 -0
  10. package/templates/community/src/stats/weekly.ts +50 -0
  11. package/templates/community/src/welcome/channel.ts +19 -0
  12. package/templates/community/src/welcome/welcome.ts +37 -0
  13. package/templates/community/tsconfig.json +20 -0
  14. package/templates/default/_gitignore +1 -0
  15. package/templates/default/package.json +1 -1
  16. package/templates/default/src/events/ready.ts +14 -20
  17. package/templates/moderation/.env.example +4 -0
  18. package/templates/moderation/_gitignore +5 -0
  19. package/templates/moderation/package.json +23 -0
  20. package/templates/moderation/src/moderation/channel.ts +67 -0
  21. package/templates/moderation/src/moderation/log.ts +37 -0
  22. package/templates/moderation/src/moderation/messages.ts +158 -0
  23. package/templates/moderation/src/moderation/purge.ts +30 -0
  24. package/templates/moderation/src/moderation/timeout.ts +82 -0
  25. package/templates/moderation/src/moderation/warn.ts +61 -0
  26. package/templates/moderation/src/presence.ts +6 -0
  27. package/templates/moderation/tsconfig.json +20 -0
  28. package/templates/music/.env.example +4 -0
  29. package/templates/music/_gitignore +5 -0
  30. package/templates/music/package.json +23 -0
  31. package/templates/music/src/music/leave.ts +18 -0
  32. package/templates/music/src/music/messages.ts +83 -0
  33. package/templates/music/src/music/play.ts +120 -0
  34. package/templates/music/src/music/queue-command.ts +28 -0
  35. package/templates/music/src/music/queue.ts +61 -0
  36. package/templates/music/src/presence.ts +6 -0
  37. package/templates/music/tsconfig.json +20 -0
  38. package/templates/tickets/.env.example +4 -0
  39. package/templates/tickets/_gitignore +5 -0
  40. package/templates/tickets/package.json +23 -0
  41. package/templates/tickets/src/presence.ts +6 -0
  42. package/templates/tickets/src/tickets/close.ts +39 -0
  43. package/templates/tickets/src/tickets/messages.ts +68 -0
  44. package/templates/tickets/src/tickets/open.ts +81 -0
  45. package/templates/tickets/src/tickets/panel.ts +35 -0
  46. package/templates/tickets/tsconfig.json +20 -0
  47. package/templates/default/public/hello.ogg +0 -0
  48. package/templates/default/src/(moderation)/ban.ts +0 -49
  49. package/templates/default/src/(moderation)/checks.ts +0 -19
  50. package/templates/default/src/(moderation)/kick.ts +0 -27
  51. package/templates/default/src/(moderation)/messages.ts +0 -81
  52. package/templates/default/src/(moderation)/warn.ts +0 -42
  53. package/templates/default/src/commands/hello.ts +0 -23
  54. package/templates/default/src/commands/ping.ts +0 -55
  55. package/templates/default/src/commands/play.ts +0 -41
  56. package/templates/default/src/events/wave.ts +0 -14
  57. package/templates/default/src/messages/en-US.ts +0 -36
  58. package/templates/default/src/messages/fr.ts +0 -37
  59. package/templates/default/src/presence.ts +0 -8
@@ -0,0 +1,61 @@
1
+ import type { Attachment, Guild, VoiceConnection } from 'chapterjs';
2
+
3
+ // Plain code: this file declares nothing, so the framework leaves it alone.
4
+ // It keeps the queue of each server and plays it, song after song.
5
+
6
+ export interface Song {
7
+ /** What play() takes: a file sent on Discord, or a link. */
8
+ source: Attachment | string;
9
+ title: string;
10
+ /** Who asked for it: a mention. */
11
+ requestedBy: string;
12
+ }
13
+
14
+ interface Queue {
15
+ songs: Song[];
16
+ current: Song | null;
17
+ }
18
+
19
+ const queues = new Map<string, Queue>();
20
+
21
+ /** The queue of a server, made on first use. */
22
+ export function queueOf(guild: Guild): Queue {
23
+ let queue = queues.get(guild.id);
24
+ if (!queue) {
25
+ queue = { songs: [], current: null };
26
+ queues.set(guild.id, queue);
27
+ }
28
+ return queue;
29
+ }
30
+
31
+ /**
32
+ * Plays the queue of a server until it is empty, then leaves the channel.
33
+ * Called once when the first song is added; a song added meanwhile is
34
+ * picked up by the loop. A song that can't be played (a dead link, ffmpeg
35
+ * missing) is skipped and said in the channel.
36
+ */
37
+ export async function playQueue(
38
+ guild: Guild,
39
+ voice: VoiceConnection,
40
+ say: (text: string) => Promise<unknown>
41
+ ): Promise<void> {
42
+ const queue = queueOf(guild);
43
+ if (queue.current) return;
44
+ let song: Song | undefined;
45
+ while ((song = queue.songs.shift())) {
46
+ queue.current = song;
47
+ try {
48
+ await voice.play(song.source);
49
+ } catch (error) {
50
+ await say(`⚠ ${song.title}: ${(error as Error).message}`);
51
+ }
52
+ }
53
+ queue.current = null;
54
+ // /leave may have made the bot leave already.
55
+ if (voice.connected) await voice.leave();
56
+ }
57
+
58
+ /** Empties the queue of a server: when the bot stops or leaves. */
59
+ export function clearQueue(guild: Guild): void {
60
+ queueOf(guild).songs.length = 0;
61
+ }
@@ -0,0 +1,6 @@
1
+ import { presence } from 'chapterjs';
2
+
3
+ // What the bot shows under its name.
4
+ export default presence({
5
+ activity: { type: 'listening', name: '/play' },
6
+ });
@@ -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,4 @@
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=
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .chapterjs
5
+ data
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "ticket-bot",
3
+ "description": "Support tickets in private threads: a panel, a form, a close button",
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,6 @@
1
+ import { presence } from 'chapterjs';
2
+
3
+ // What the bot shows under its name.
4
+ export default presence({
5
+ activity: { type: 'watching', name: 'the tickets' },
6
+ });
@@ -0,0 +1,39 @@
1
+ import { button } from 'chapterjs';
2
+ import { tickets } from './open';
3
+
4
+ // The button at the top of every ticket. Everyone in the thread sees it;
5
+ // the code decides who may use it: the staff, or who opened the ticket.
6
+ // `data` is what the button carries from the message to the click: here
7
+ // who opened the ticket, kept by Discord in the button itself.
8
+ export const close = button({
9
+ label: ({ t }) => t('closeButton'),
10
+ style: 'danger',
11
+ data: { userId: 'string' },
12
+ async run({ interaction, data, channel, member, guild, t }) {
13
+ if (!channel.isThread()) {
14
+ await interaction.reply({ content: t('notATicket'), ephemeral: true });
15
+ return;
16
+ }
17
+ const staff = member.permissions.has('ManageThreads');
18
+ if (!staff && member.id !== data.userId) {
19
+ await interaction.reply({ content: t('notYours'), ephemeral: true });
20
+ return;
21
+ }
22
+ // Said in the thread first: archiving it closes it for everyone.
23
+ await interaction.reply(t('closing', { user: member.toString() }));
24
+ await channel.edit(
25
+ { locked: true, archived: true },
26
+ `Closed by ${member.user.username}`
27
+ );
28
+ // The person may open another one.
29
+ await tickets.delete(data.userId);
30
+ // Who opened it is told in private, unless they closed it themselves.
31
+ // A person who refuses private messages is not an error.
32
+ if (member.id !== data.userId) {
33
+ const opener = await guild.fetchMember(data.userId).catch(() => null);
34
+ await opener
35
+ ?.send(t('closedDm', { ticket: channel.name, server: guild.name }))
36
+ .catch(() => {});
37
+ }
38
+ },
39
+ });
@@ -0,0 +1,68 @@
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 command.
6
+ export const en = language({
7
+ locale: 'en-US',
8
+ default: true,
9
+ texts: {
10
+ panelTitle: 'Need help?',
11
+ panelText:
12
+ 'Open a ticket and the staff will answer you in a private thread.',
13
+ panelPosted: 'The panel is posted.',
14
+ openButton: 'Open a ticket',
15
+ formTitle: 'New ticket',
16
+ formSubject: 'Subject',
17
+ formCategory: 'What is it about?',
18
+ formDetails: 'Tell us more',
19
+ notHere: 'Tickets can only be opened from a text channel.',
20
+ opened: 'Thanks {user}, the staff will be with you shortly.',
21
+ created: 'Your ticket is open: {thread}',
22
+ alreadyOpen: 'You already have a ticket open: {thread}. Close it first.',
23
+ category: 'Category',
24
+ openedBy: 'Opened by',
25
+ closeButton: 'Close the ticket',
26
+ notATicket: 'This button only works inside a ticket.',
27
+ notYours: 'Only the staff, or who opened this ticket, can close it.',
28
+ closing: 'Ticket closed by {user}.',
29
+ closedDm: 'Your ticket "{ticket}" on {server} was closed.',
30
+ },
31
+ commands: {
32
+ 'ticket panel': {
33
+ description: 'Posts the message people open tickets from',
34
+ },
35
+ },
36
+ });
37
+
38
+ export const fr = language({
39
+ locale: 'fr',
40
+ texts: {
41
+ panelTitle: 'Besoin d’aide ?',
42
+ panelText: 'Ouvrez un ticket et le staff vous répondra dans un fil privé.',
43
+ panelPosted: 'Le panneau est posté.',
44
+ openButton: 'Ouvrir un ticket',
45
+ formTitle: 'Nouveau ticket',
46
+ formSubject: 'Sujet',
47
+ formCategory: 'De quoi s’agit-il ?',
48
+ formDetails: 'Dites-nous en plus',
49
+ notHere: 'Un ticket ne peut être ouvert que depuis un salon textuel.',
50
+ opened: 'Merci {user}, le staff arrive.',
51
+ created: 'Votre ticket est ouvert : {thread}',
52
+ alreadyOpen:
53
+ 'Vous avez déjà un ticket ouvert : {thread}. Fermez-le d’abord.',
54
+ category: 'Catégorie',
55
+ openedBy: 'Ouvert par',
56
+ closeButton: 'Fermer le ticket',
57
+ notATicket: 'Ce bouton ne fonctionne que dans un ticket.',
58
+ notYours:
59
+ 'Seul le staff, ou la personne qui a ouvert ce ticket, peut le fermer.',
60
+ closing: 'Ticket fermé par {user}.',
61
+ closedDm: 'Votre ticket « {ticket} » sur {server} a été fermé.',
62
+ },
63
+ commands: {
64
+ 'ticket panel': {
65
+ description: 'Poste le message depuis lequel on ouvre un ticket',
66
+ },
67
+ },
68
+ });
@@ -0,0 +1,81 @@
1
+ import { ChannelType, modal, store } from 'chapterjs';
2
+ import { close } from './close';
3
+
4
+ // The ticket each person has open: the id of its thread, by person. A
5
+ // store keeps data from one restart to the next, per server: in this
6
+ // server, `tickets.get(member.id)` is the ticket of that member here.
7
+ export const tickets = store<string>();
8
+
9
+ // The form a ticket starts with. `run` receives the fields typed from
10
+ // their kind: `subject` and `details` are strings, `category` is one of
11
+ // the three values below.
12
+ export const openTicket = modal({
13
+ title: ({ t }) => t('formTitle'),
14
+ fields: {
15
+ subject: {
16
+ type: 'text',
17
+ label: ({ t }) => t('formSubject'),
18
+ style: 'short',
19
+ maxLength: 80,
20
+ },
21
+ category: {
22
+ type: 'radio',
23
+ label: ({ t }) => t('formCategory'),
24
+ options: { Bug: 'bug', Question: 'question', Other: 'other' },
25
+ },
26
+ details: {
27
+ type: 'text',
28
+ label: ({ t }) => t('formDetails'),
29
+ style: 'paragraph',
30
+ minLength: 20,
31
+ maxLength: 1000,
32
+ },
33
+ },
34
+ async run({ interaction, fields, channel, member, t }) {
35
+ if (!channel.isText()) {
36
+ await interaction.reply({ content: t('notHere'), ephemeral: true });
37
+ return;
38
+ }
39
+ // One ticket at a time per person.
40
+ const open = await tickets.get(member.id);
41
+ if (open) {
42
+ await interaction.reply({
43
+ content: t('alreadyOpen', { thread: `<#${open}>` }),
44
+ ephemeral: true,
45
+ });
46
+ return;
47
+ }
48
+ // A private thread: only the people added to it, and the staff, see it.
49
+ const thread = await channel.startThread(
50
+ {
51
+ name: `${fields.category}-${member.displayName}`.slice(0, 100),
52
+ type: ChannelType.PrivateThread,
53
+ invitable: false,
54
+ },
55
+ `Ticket opened by ${member.user.username}`
56
+ );
57
+ await thread.addMember(member);
58
+ await tickets.set(member.id, thread.id);
59
+ await thread.send({
60
+ content: t('opened', { user: member.toString() }),
61
+ embeds: [
62
+ {
63
+ title: fields.subject,
64
+ description: fields.details,
65
+ color: 0xc026d3,
66
+ fields: [
67
+ { name: t('category'), value: fields.category, inline: true },
68
+ { name: t('openedBy'), value: member.toString(), inline: true },
69
+ ],
70
+ },
71
+ ],
72
+ // The close button carries who opened the ticket, so it knows who may
73
+ // close it besides the staff, and who to tell when it is closed.
74
+ components: [close({ userId: member.id })],
75
+ });
76
+ await interaction.reply({
77
+ content: t('created', { thread: thread.toString() }),
78
+ ephemeral: true,
79
+ });
80
+ },
81
+ });
@@ -0,0 +1,35 @@
1
+ import { button, command } from 'chapterjs';
2
+ import { openTicket } from './open';
3
+
4
+ // /ticket panel posts the message people open a ticket from: an embed and
5
+ // a button. Only the staff can post it (`permissions`); Discord hides the
6
+ // command from everyone else. `t` speaks the language of the server.
7
+ export default command({
8
+ name: 'ticket panel',
9
+ permissions: ['ManageThreads'],
10
+ async run({ interaction, channel, t }) {
11
+ await channel.send({
12
+ embeds: [
13
+ {
14
+ title: t('panelTitle'),
15
+ description: t('panelText'),
16
+ color: 0xc026d3,
17
+ },
18
+ ],
19
+ components: [open],
20
+ });
21
+ await interaction.reply({ content: t('panelPosted'), ephemeral: true });
22
+ },
23
+ });
24
+
25
+ // A button is named by its export (`open`): nothing to register, no id to
26
+ // write. Its label is a function of `t`, run when the message is sent.
27
+ export const open = button({
28
+ label: ({ t }) => t('openButton'),
29
+ style: 'primary',
30
+ emoji: '🎫',
31
+ async run({ interaction }) {
32
+ // A form must be the first answer to a click.
33
+ await interaction.showModal(openTicket);
34
+ },
35
+ });
@@ -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
+ }
Binary file
@@ -1,49 +0,0 @@
1
- import { button, command } from 'chapterjs';
2
- import { canPunish } from './checks';
3
-
4
- // A folder is only a folder: `(moderation)` groups what moderation needs,
5
- // and the framework reads what these files export, wherever they are.
6
- // /ban asks for a confirmation, then bans. The command and its button live
7
- // in the same file: the button is named by its export (`confirmBan`), and
8
- // `who: 'author'` keeps it for the person who used the command.
9
- export default command({
10
- name: 'ban',
11
- permissions: ['BanMembers'],
12
- ephemeral: true,
13
- options: {
14
- target: { type: 'user', required: true },
15
- reason: { type: 'string' },
16
- days: { type: 'integer', min: 0, max: 7 },
17
- },
18
- async run({ interaction, options, member, guild, t }) {
19
- const target = guild.members.get(options.target.id);
20
- if (target && !canPunish(member, target)) {
21
- await interaction.reply(t('cannotPunish', { user: target.displayName }));
22
- return;
23
- }
24
- await interaction.reply({
25
- content: t('ban.confirm', { user: options.target.username }),
26
- components: [
27
- confirmBan({
28
- userId: options.target.id,
29
- days: options.days ?? 0,
30
- reason: options.reason ?? '',
31
- }),
32
- ],
33
- });
34
- },
35
- });
36
-
37
- export const confirmBan = button({
38
- label: ({ t }) => t('ban.yes'),
39
- style: 'danger',
40
- who: 'author',
41
- data: { userId: 'string', days: 'number', reason: 'string' },
42
- async run({ interaction, data, guild, user, t }) {
43
- await guild.ban(data.userId, {
44
- reason: data.reason || t('ban.defaultReason', { by: user.username }),
45
- deleteMessageSeconds: data.days * 86_400,
46
- });
47
- await interaction.update({ content: t('ban.done'), components: [] });
48
- },
49
- });
@@ -1,19 +0,0 @@
1
- import type { GuildMember } from 'chapterjs';
2
-
3
- // Shared code: this file declares nothing, the commands import it. Nothing
4
- // survives a restart: a real bot keeps its warnings in a database.
5
- export function canPunish(by: GuildMember, target: GuildMember): boolean {
6
- return by.highestRole.position > target.highestRole.position;
7
- }
8
-
9
- const warnings = new Map<string, number>();
10
-
11
- export function addWarning(userId: string): number {
12
- const count = (warnings.get(userId) ?? 0) + 1;
13
- warnings.set(userId, count);
14
- return count;
15
- }
16
-
17
- export function warningsOf(userId: string): number {
18
- return warnings.get(userId) ?? 0;
19
- }
@@ -1,27 +0,0 @@
1
- import { command } from 'chapterjs';
2
-
3
- // /kick: the texts come from the language files, like every command here.
4
- import { canPunish } from './checks';
5
-
6
- export default command({
7
- name: 'kick',
8
- permissions: ['KickMembers'],
9
- ephemeral: true,
10
- options: {
11
- target: { type: 'user', required: true },
12
- reason: { type: 'string' },
13
- },
14
- async run({ interaction, options, member, guild, t }) {
15
- const target = guild.members.get(options.target.id);
16
- if (!target) {
17
- await interaction.reply(t('notHere', { user: options.target.username }));
18
- return;
19
- }
20
- if (!canPunish(member, target)) {
21
- await interaction.reply(t('cannotPunish', { user: target.displayName }));
22
- return;
23
- }
24
- await target.kick(options.reason);
25
- await interaction.reply(t('kick.done', { user: target.displayName }));
26
- },
27
- });
@@ -1,81 +0,0 @@
1
- import { language } from 'chapterjs';
2
-
3
- // A language may be declared in several places: these texts are added to the
4
- // en-US and fr of src/messages/, next to the commands that use them. What
5
- // says `default: true` there decides for the whole language.
6
- export const en = language({
7
- locale: 'en-US',
8
- texts: {
9
- cannotPunish: 'You can not act on {user}: their role is above yours.',
10
- notHere: '{user} is not in this server.',
11
- 'ban.confirm': 'Ban {user}?',
12
- 'ban.yes': 'Ban',
13
- 'ban.defaultReason': 'Banned by {by}',
14
- 'ban.done': 'Banned.',
15
- 'kick.done': '{user} was kicked.',
16
- 'warn.added': '{user} now has {count} warning(s).',
17
- // A plural: one form per quantity, picked by `count`.
18
- 'warn.count': {
19
- one: '{user} has {count} warning.',
20
- other: '{user} has {count} warnings.',
21
- },
22
- },
23
- // What Discord shows of these commands: their descriptions live here too.
24
- commands: {
25
- ban: {
26
- description: 'Bans a member, after a confirmation',
27
- options: {
28
- target: { description: 'Who to ban' },
29
- reason: { description: 'Why' },
30
- days: { description: 'Days of messages to delete' },
31
- },
32
- },
33
- kick: {
34
- description: 'Kicks a member',
35
- options: {
36
- target: { description: 'Who to kick' },
37
- reason: { description: 'Why' },
38
- },
39
- },
40
- 'warn add': {
41
- description: 'Warns a member',
42
- options: {
43
- target: { description: 'Who to warn' },
44
- reason: { description: 'Why' },
45
- },
46
- },
47
- 'warn list': {
48
- description: 'Counts the warnings of a member',
49
- options: { target: { description: 'Whose warnings' } },
50
- },
51
- },
52
- });
53
-
54
- export const fr = language({
55
- locale: 'fr',
56
- texts: {
57
- cannotPunish:
58
- 'Vous ne pouvez pas agir sur {user} : son rôle est au-dessus du vôtre.',
59
- notHere: '{user} n’est pas sur ce serveur.',
60
- 'ban.confirm': 'Bannir {user} ?',
61
- 'ban.yes': 'Bannir',
62
- 'ban.defaultReason': 'Banni par {by}',
63
- 'ban.done': 'Banni.',
64
- 'kick.done': '{user} a été expulsé.',
65
- 'warn.added': '{user} a maintenant {count} avertissement(s).',
66
- 'warn.count': {
67
- one: '{user} a {count} avertissement.',
68
- other: '{user} a {count} avertissements.',
69
- },
70
- },
71
- commands: {
72
- // A command can be renamed in a language too: /bannir for French users.
73
- ban: {
74
- name: 'bannir',
75
- description: 'Bannit un membre, après confirmation',
76
- },
77
- kick: { name: 'expulser', description: 'Expulse un membre' },
78
- 'warn add': { description: 'Avertit un membre' },
79
- 'warn list': { description: 'Compte les avertissements d’un membre' },
80
- },
81
- });
@@ -1,42 +0,0 @@
1
- import { command, event } from 'chapterjs';
2
- import { addWarning, warningsOf } from './checks';
3
-
4
- // Two subcommands in one file: /warn add and /warn list. A subcommand is a
5
- // matter of `name`, not of folder.
6
- export const add = command({
7
- name: 'warn add',
8
- permissions: ['ModerateMembers'],
9
- options: {
10
- target: { type: 'user', required: true },
11
- reason: { type: 'string', required: true },
12
- },
13
- async run({ interaction, options, t }) {
14
- const count = addWarning(options.target.id);
15
- await interaction.reply(
16
- t('warn.added', { user: options.target.username, count })
17
- );
18
- },
19
- });
20
-
21
- export const list = command({
22
- name: 'warn list',
23
- permissions: ['ModerateMembers'],
24
- ephemeral: true,
25
- options: { target: { type: 'user', required: true } },
26
- async run({ interaction, options, t }) {
27
- await interaction.reply(
28
- t('warn.count', {
29
- user: options.target.username,
30
- count: warningsOf(options.target.id),
31
- })
32
- );
33
- },
34
- });
35
-
36
- // The event that goes with moderation, next to the commands it follows.
37
- export const logged = event({
38
- name: 'banAdd',
39
- run({ user, guild }) {
40
- console.log(`${user.username} was banned from ${guild.name}`);
41
- },
42
- });
@@ -1,23 +0,0 @@
1
- import { asset, command } from 'chapterjs';
2
-
3
- // /hello joins your voice channel and plays public/hello.ogg. An Opus file
4
- // (.ogg, .webm) plays as it is; an .mp3 or a link needs ffmpeg installed.
5
- export default command({
6
- name: 'hello',
7
- async run({ interaction, member, t }) {
8
- const channel = member.voiceChannel;
9
- if (!channel) {
10
- await interaction.reply({ content: t('joinVoice'), ephemeral: true });
11
- return;
12
- }
13
- // Answer first: a command has 3 seconds, joining takes a moment.
14
- await interaction.reply(t('hello', { channel: channel.name }));
15
- const voice = await channel.join();
16
- // Whatever happens while it plays, the bot leaves after.
17
- try {
18
- await voice.play(asset('hello.ogg'));
19
- } finally {
20
- await voice.leave();
21
- }
22
- },
23
- });