create-chapter 0.2.3 → 0.4.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.
Files changed (53) hide show
  1. package/README.md +17 -0
  2. package/dist/index.js +4 -98
  3. package/package.json +18 -5
  4. package/dist/choose.js +0 -27
  5. package/dist/node-version.js +0 -13
  6. package/dist/package-manager.js +0 -59
  7. package/dist/project.js +0 -82
  8. package/dist/templates.js +0 -31
  9. package/templates/community/.env.example +0 -4
  10. package/templates/community/_gitignore +0 -5
  11. package/templates/community/package.json +0 -23
  12. package/templates/community/src/messages.ts +0 -49
  13. package/templates/community/src/presence.ts +0 -6
  14. package/templates/community/src/roles/roles.ts +0 -60
  15. package/templates/community/src/stats/weekly.ts +0 -50
  16. package/templates/community/src/welcome/channel.ts +0 -19
  17. package/templates/community/src/welcome/welcome.ts +0 -37
  18. package/templates/community/tsconfig.json +0 -20
  19. package/templates/default/.env.example +0 -4
  20. package/templates/default/_gitignore +0 -5
  21. package/templates/default/package.json +0 -23
  22. package/templates/default/src/events/ready.ts +0 -28
  23. package/templates/default/tsconfig.json +0 -20
  24. package/templates/moderation/.env.example +0 -4
  25. package/templates/moderation/_gitignore +0 -5
  26. package/templates/moderation/package.json +0 -23
  27. package/templates/moderation/src/moderation/channel.ts +0 -67
  28. package/templates/moderation/src/moderation/log.ts +0 -37
  29. package/templates/moderation/src/moderation/messages.ts +0 -158
  30. package/templates/moderation/src/moderation/purge.ts +0 -30
  31. package/templates/moderation/src/moderation/timeout.ts +0 -82
  32. package/templates/moderation/src/moderation/warn.ts +0 -61
  33. package/templates/moderation/src/presence.ts +0 -6
  34. package/templates/moderation/tsconfig.json +0 -20
  35. package/templates/music/.env.example +0 -4
  36. package/templates/music/_gitignore +0 -5
  37. package/templates/music/package.json +0 -23
  38. package/templates/music/src/music/leave.ts +0 -18
  39. package/templates/music/src/music/messages.ts +0 -83
  40. package/templates/music/src/music/play.ts +0 -120
  41. package/templates/music/src/music/queue-command.ts +0 -28
  42. package/templates/music/src/music/queue.ts +0 -61
  43. package/templates/music/src/presence.ts +0 -6
  44. package/templates/music/tsconfig.json +0 -20
  45. package/templates/tickets/.env.example +0 -4
  46. package/templates/tickets/_gitignore +0 -5
  47. package/templates/tickets/package.json +0 -23
  48. package/templates/tickets/src/presence.ts +0 -6
  49. package/templates/tickets/src/tickets/close.ts +0 -39
  50. package/templates/tickets/src/tickets/messages.ts +0 -68
  51. package/templates/tickets/src/tickets/open.ts +0 -81
  52. package/templates/tickets/src/tickets/panel.ts +0 -35
  53. package/templates/tickets/tsconfig.json +0 -20
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # create-chapter
2
+
3
+ An alias of [`create-chapterjs`](https://www.npmjs.com/package/create-chapterjs), so that both spellings create a [ChapterJS](https://www.chapterjs.org) Discord bot:
4
+
5
+ ```bash
6
+ pnpm create chapter my-bot
7
+ ```
8
+
9
+ ```bash
10
+ pnpm create chapterjs my-bot
11
+ ```
12
+
13
+ Same questions, same templates, same version: this package only runs `create-chapterjs` with the arguments it receives. See [its README](https://www.npmjs.com/package/create-chapterjs) and the [quickstart](https://www.chapterjs.org/quickstart).
14
+
15
+ ## License
16
+
17
+ [MIT](./LICENSE)
package/dist/index.js CHANGED
@@ -1,99 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import * as p from '@clack/prompts';
3
- import { readFileSync } from 'node:fs';
4
- import { relative, resolve } from 'node:path';
5
- import { detectPackageManager, install, installedVersions, packageManagers, runScript, } from './package-manager.js';
6
- import { choose } from './choose.js';
7
- import { createProject, validateTargetDir } from './project.js';
8
- import { listTemplates } from './templates.js';
9
- import { nodeTooOld } from './node-version.js';
10
- /** Stops cleanly when the user presses Ctrl+C or Esc on a prompt. */
11
- function orExit(value) {
12
- if (p.isCancel(value)) {
13
- p.cancel('Cancelled, nothing was created.');
14
- process.exit(0);
15
- }
16
- return value;
17
- }
18
- // create-chapter and chapterjs are always released with the same version.
19
- const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
20
- p.intro(`Create a ChapterJS bot (v${version})`);
21
- const tooOld = nodeTooOld();
22
- if (tooOld) {
23
- p.cancel(tooOld);
24
- process.exit(1);
25
- }
26
- // Started now so it runs while the user answers the first question.
27
- const versionsCheck = installedVersions();
28
- // `pnpm create chapter my-bot` skips the first question when the folder is valid.
29
- const argDir = process.argv[2];
30
- const argError = argDir === undefined ? undefined : validateTargetDir(argDir);
31
- if (argError)
32
- p.log.warn(argError);
33
- const dir = argDir !== undefined && argError === undefined
34
- ? argDir.trim()
35
- : orExit(await p.text({
36
- message: 'Where should the project be created? (. for the current folder)',
37
- placeholder: 'my-bot',
38
- defaultValue: 'my-bot',
39
- validate: value => validateTargetDir(value || 'my-bot'),
40
- })).trim();
41
- const versions = await versionsCheck;
42
- const pm = orExit(await choose({
43
- message: 'Which package manager do you want to use?',
44
- initialValue: detectPackageManager(),
45
- choices: packageManagers.map(value => {
46
- const version = versions[value];
47
- return version === undefined
48
- ? { value, label: `${value} (not installed)`, disabled: true }
49
- : { value, hint: `v${version}` };
50
- }),
51
- onlyChoice: value => `Using ${value}, the only package manager installed`,
52
- }));
53
- if (pm === undefined) {
54
- p.cancel(`No package manager found. Install one of ${packageManagers.join(', ')}, then run this command again.`);
55
- process.exit(1);
56
- }
57
- const templates = await listTemplates();
58
- const template = orExit(await choose({
59
- message: 'Which template do you want to start from?',
60
- choices: templates.map(({ name, description }) => ({
61
- value: name,
62
- hint: description,
63
- })),
64
- onlyChoice: value => `Using the ${value} template`,
65
- }));
66
- if (template === undefined) {
67
- p.cancel('No template found. Reinstall create-chapter and try again.');
68
- process.exit(1);
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.
73
- await createProject({
74
- dir,
75
- template,
76
- chapterjsVersion: version,
77
- packageManager: pm,
78
- });
79
- p.log.success(`Project created in ${resolve(dir)}`);
80
- const spinner = p.spinner();
81
- spinner.start(`Installing dependencies with ${pm}`);
82
- const result = await install(pm, dir);
83
- if (result.ok) {
84
- spinner.stop(`Dependencies installed with ${pm}`);
85
- }
86
- else {
87
- spinner.error(`The dependencies could not be installed with ${pm}`);
88
- if (result.output)
89
- p.log.message(result.output);
90
- }
91
- const cd = relative(process.cwd(), resolve(dir));
92
- const steps = [
93
- ...(cd ? [`cd ${cd.includes(' ') ? `"${cd}"` : cd}`] : []),
94
- ...(result.ok ? [] : [`${pm} install`]),
95
- 'Copy .env.example to .env and fill in BOT_TOKEN and DEV_GUILD_ID',
96
- runScript(pm, 'dev'),
97
- ];
98
- p.note(steps.join('\n'), 'Next steps');
99
- p.outro('Happy building!');
2
+ // Alias so `pnpm create chapter` works too: runs the create-chapterjs CLI.
3
+ // @ts-expect-error create-chapterjs only ships a bin, without type declarations.
4
+ await import('create-chapterjs/dist/index.js');
5
+ export {};
package/package.json CHANGED
@@ -1,21 +1,34 @@
1
1
  {
2
2
  "name": "create-chapter",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
+ "description": "Alias of create-chapterjs: creates a ChapterJS Discord bot with pnpm create chapter",
5
+ "keywords": [
6
+ "discord",
7
+ "discord-bot",
8
+ "bot",
9
+ "framework",
10
+ "typescript",
11
+ "slash-commands",
12
+ "esm",
13
+ "create",
14
+ "scaffold",
15
+ "template",
16
+ "chapterjs"
17
+ ],
4
18
  "license": "MIT",
5
19
  "repository": {
6
20
  "type": "git",
7
21
  "url": "git+https://github.com/chapterjs/chapterjs.git",
8
22
  "directory": "packages/create-chapter"
9
23
  },
10
- "homepage": "https://chapterjs.dev",
24
+ "homepage": "https://www.chapterjs.org",
11
25
  "bugs": "https://github.com/chapterjs/chapterjs/issues",
12
26
  "type": "module",
13
27
  "bin": {
14
28
  "create-chapter": "./dist/index.js"
15
29
  },
16
30
  "files": [
17
- "dist",
18
- "templates"
31
+ "dist"
19
32
  ],
20
33
  "devDependencies": {
21
34
  "@chapterjs/test-utils": "0.0.0",
@@ -25,7 +38,7 @@
25
38
  "vitest": "^5.0.3"
26
39
  },
27
40
  "dependencies": {
28
- "@clack/prompts": "^1.8.1"
41
+ "create-chapterjs": "^0.4.0"
29
42
  },
30
43
  "engines": {
31
44
  "node": ">=24"
package/dist/choose.js DELETED
@@ -1,27 +0,0 @@
1
- import * as p from '@clack/prompts';
2
- /**
3
- * A select menu that never asks a question with only one answer: when a single
4
- * choice can be picked, it is taken right away and the user is told why.
5
- * Returns `undefined` when no choice can be picked, and the cancel symbol when
6
- * the user presses Ctrl+C or Esc.
7
- */
8
- export async function choose({ message, choices, initialValue, onlyChoice, }) {
9
- const enabled = choices.filter(choice => !choice.disabled);
10
- const [first] = enabled;
11
- if (!first)
12
- return undefined;
13
- if (enabled.length === 1) {
14
- p.log.info(onlyChoice(first.value));
15
- return first.value;
16
- }
17
- // clack's option type can't be resolved for a generic `Value`, so the menu is
18
- // typed with `string`: it only ever returns one of `choices`.
19
- const picked = await p.select({
20
- message,
21
- initialValue: enabled.some(choice => choice.value === initialValue)
22
- ? initialValue
23
- : first.value,
24
- options: choices,
25
- });
26
- return picked;
27
- }
@@ -1,13 +0,0 @@
1
- /** The oldest Node.js a ChapterJS bot runs on, as `engines` says. */
2
- export const MINIMUM_NODE = 24;
3
- /**
4
- * What to tell the person when this Node.js is older than ChapterJS needs,
5
- * or `null` when it is recent enough: refused before creating anything,
6
- * since the bot could not run.
7
- */
8
- export function nodeTooOld(version = process.versions.node) {
9
- const major = Number(version.split('.')[0]);
10
- if (major >= MINIMUM_NODE)
11
- return null;
12
- return `ChapterJS needs Node.js ${MINIMUM_NODE} or newer, and this is Node.js ${version}. Install the LTS version from https://nodejs.org, then run the command again.`;
13
- }
@@ -1,59 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { tmpdir } from 'node:os';
3
- /** In menu order: pnpm, the one ChapterJS itself uses, comes first. */
4
- export const packageManagers = ['pnpm', 'npm', 'yarn', 'bun'];
5
- /**
6
- * The package manager that launched `create chapter`, read from the user agent
7
- * it sets (e.g. `pnpm/11.0.0 npm/? node/v24.0.0 darwin arm64`).
8
- */
9
- export function detectPackageManager() {
10
- const name = process.env.npm_config_user_agent?.split('/')[0];
11
- return packageManagers.find(pm => pm === name) ?? 'pnpm';
12
- }
13
- /**
14
- * The installed version of each package manager, or `undefined` when it isn't
15
- * installed. All are checked at once, so this takes as long as the slowest one.
16
- */
17
- export async function installedVersions() {
18
- const versions = await Promise.all(packageManagers.map(pm => new Promise(resolve => {
19
- let output = '';
20
- const child = spawn(pm, ['--version'], {
21
- // Outside the current folder: a `packageManager` field in a parent
22
- // package.json makes corepack refuse to run any other manager.
23
- cwd: tmpdir(),
24
- // Corepack must never stop to ask before downloading a manager.
25
- env: { ...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: '0' },
26
- shell: process.platform === 'win32',
27
- stdio: ['ignore', 'pipe', 'ignore'],
28
- });
29
- child.stdout.on('data', (chunk) => (output += chunk));
30
- child.on('error', () => resolve(undefined));
31
- child.on('close', code => resolve(code === 0 ? output.trim() || undefined : undefined));
32
- })));
33
- return Object.fromEntries(packageManagers.map((pm, i) => [pm, versions[i]]));
34
- }
35
- /** The command that runs a package.json script, e.g. `pnpm dev` or `npm run dev`. */
36
- export function runScript(pm, script) {
37
- return pm === 'npm' ? `npm run ${script}` : `${pm} ${script}`;
38
- }
39
- /** Runs `<pm> install` in `cwd`, capturing the output instead of printing it. */
40
- export function install(pm, cwd) {
41
- return new Promise(resolve => {
42
- let output = '';
43
- const child = spawn(pm, ['install'], {
44
- cwd,
45
- // Package managers are `.cmd` shims on Windows, which only a shell can run.
46
- shell: process.platform === 'win32',
47
- stdio: ['ignore', 'pipe', 'pipe'],
48
- });
49
- child.stdout.on('data', (chunk) => (output += chunk));
50
- child.stderr.on('data', (chunk) => (output += chunk));
51
- child.on('error', (error) => {
52
- const reason = error.code === 'ENOENT'
53
- ? `${pm} is not installed on this computer.`
54
- : error.message;
55
- resolve({ ok: false, output: reason });
56
- });
57
- child.on('close', code => resolve({ ok: code === 0, output: output.trim() }));
58
- });
59
- }
package/dist/project.js DELETED
@@ -1,82 +0,0 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
2
- import { cp, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
- import { basename, join, resolve } from 'node:path';
4
- import { templatesDir } from './templates.js';
5
- /** Files that don't make a folder "used": a project can still be created there. */
6
- const ignoredFiles = new Set([
7
- '.git',
8
- '.DS_Store',
9
- 'Thumbs.db',
10
- '.idea',
11
- '.vscode',
12
- ]);
13
- /** Returns why `input` can't hold a new project, or `undefined` when it can. */
14
- export function validateTargetDir(input) {
15
- const dir = input?.trim();
16
- if (!dir)
17
- return 'Type a folder name, or . to use the current folder.';
18
- const path = resolve(dir);
19
- if (!existsSync(path))
20
- return undefined;
21
- if (!statSync(path).isDirectory())
22
- return `${dir} is a file, not a folder. Pick another name.`;
23
- const used = readdirSync(path).some(file => !ignoredFiles.has(file));
24
- if (used) {
25
- return path === process.cwd()
26
- ? 'The current folder is not empty. Type a name to create a new folder instead.'
27
- : `The folder ${dir} is not empty. Pick another name.`;
28
- }
29
- return undefined;
30
- }
31
- /** A valid npm package name from a folder name, e.g. `Mon Bot Été` → `mon-bot-ete`. */
32
- export function toPackageName(dir) {
33
- const name = basename(resolve(dir))
34
- // `é` → `e`: split letters from their accents, then drop the accents.
35
- .normalize('NFD')
36
- .replace(/\p{Diacritic}/gu, '')
37
- .toLowerCase()
38
- .replace(/[^a-z0-9-~._]+/g, '-')
39
- .replace(/-{2,}/g, '-')
40
- // npm names can't start with `.` or `_`, and 214 characters is the maximum.
41
- .replace(/^[-._]+/, '')
42
- .slice(0, 214)
43
- .replace(/[-.]+$/, '');
44
- return name || 'chapter-bot';
45
- }
46
- /**
47
- * What pnpm needs to install a project whose framework comes with esbuild
48
- * (which `chapterjs build` compiles the bot with). esbuild has an install
49
- * script it does not need; pnpm refuses to go on until the project says whether that
50
- * script may run. It may not: no script of a dependency runs.
51
- * `allowBuilds` is read by pnpm 11, `ignoredBuiltDependencies` by pnpm 10.
52
- */
53
- export const PNPM_WORKSPACE = `# esbuild works without its install script: pnpm is told not to run it.
54
- allowBuilds:
55
- esbuild: false
56
- ignoredBuiltDependencies:
57
- - esbuild
58
- `;
59
- /** Copies the template into `dir` and names the project after its folder. */
60
- export async function createProject({ dir, template, chapterjsVersion, packageManager, templatesRoot = templatesDir, }) {
61
- const root = resolve(dir);
62
- await cp(join(templatesRoot, template), root, { recursive: true });
63
- // Where the files the bot sends go (`asset()`): git and npm keep no empty
64
- // folder, so no template can ship it; every project gets it here.
65
- await mkdir(join(root, 'public'), { recursive: true });
66
- // npm strips `.gitignore` from published packages, so templates ship it as `_gitignore`.
67
- const gitignore = join(root, '_gitignore');
68
- if (existsSync(gitignore))
69
- await rename(gitignore, join(root, '.gitignore'));
70
- const pkgPath = join(root, 'package.json');
71
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
72
- pkg.name = toPackageName(dir);
73
- // The template's description is for the menu, not for the user's bot.
74
- delete pkg.description;
75
- pkg.dependencies = { ...pkg.dependencies, chapterjs: chapterjsVersion };
76
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
77
- // Only for the package manager that reads it: nothing to explain to the
78
- // others.
79
- if (packageManager === 'pnpm') {
80
- await writeFile(join(root, 'pnpm-workspace.yaml'), PNPM_WORKSPACE);
81
- }
82
- }
package/dist/templates.js DELETED
@@ -1,31 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { readdir, readFile } from 'node:fs/promises';
3
- import { fileURLToPath } from 'node:url';
4
- import { join } from 'node:path';
5
- /** `templates/` ships next to `dist/` in the published package. */
6
- export const templatesDir = fileURLToPath(new URL('../templates/', import.meta.url));
7
- /**
8
- * Every folder of `templates/` with a package.json is a template: adding one
9
- * needs no code change.
10
- */
11
- export async function listTemplates(dir = templatesDir) {
12
- const entries = await readdir(dir, { withFileTypes: true });
13
- const templates = await Promise.all(entries
14
- .filter(entry => entry.isDirectory())
15
- .map(async (entry) => {
16
- const pkgPath = join(dir, entry.name, 'package.json');
17
- if (!existsSync(pkgPath))
18
- return undefined;
19
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
20
- const description = typeof pkg.description === 'string' ? pkg.description : undefined;
21
- return { name: entry.name, description };
22
- }));
23
- // `default` first, so it is the preselected choice.
24
- return templates
25
- .filter(template => template !== undefined)
26
- .sort((a, b) => a.name === 'default'
27
- ? -1
28
- : b.name === 'default'
29
- ? 1
30
- : a.name.localeCompare(b.name));
31
- }
@@ -1,4 +0,0 @@
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=
@@ -1,5 +0,0 @@
1
- node_modules
2
- dist
3
- .env
4
- .chapterjs
5
- data
@@ -1,23 +0,0 @@
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
- }
@@ -1,49 +0,0 @@
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
- });
@@ -1,6 +0,0 @@
1
- import { presence } from 'chapterjs';
2
-
3
- // What the bot shows under its name.
4
- export default presence({
5
- activity: { type: 'watching', name: 'over the server' },
6
- });
@@ -1,60 +0,0 @@
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
- });
@@ -1,50 +0,0 @@
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
- });
@@ -1,19 +0,0 @@
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
- }
@@ -1,37 +0,0 @@
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
- }));
@@ -1,20 +0,0 @@
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
- }