diff --git a/.github/workflows/deploy-commands.yml b/.github/workflows/deploy-commands.yml
index 561d337..3ee914e 100644
--- a/.github/workflows/deploy-commands.yml
+++ b/.github/workflows/deploy-commands.yml
@@ -8,30 +8,30 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Read Node version
- run: |
- NODE_VERSION=$(cat .nvmrc | sed 's/v//')
- echo "NODE_VERSION=$NODE_VERSION" >> $GITHUB_ENV
-
- - name: Deploy Discord Commands to VPS
- uses: appleboy/ssh-action@v1.0.3
- with:
- host: ${{ secrets.VPS_HOST }}
- username: ${{ secrets.VPS_USER }}
- key: ${{ secrets.VPS_SSH_KEY }}
- script: |
- cd /home/${{ secrets.VPS_USER }}/webdev-bot-deploy
-
- # Read NODE_VERSION from .nvmrc
- export NODE_VERSION=$(cat .nvmrc | sed 's/v//')
- echo "Using Node version: $NODE_VERSION"
-
- # Run deploy script inside the already running Docker container
- # .env file should already exist from main deployment
- echo "Deploying Discord commands..."
- docker compose --profile prod exec bot-prod node dist/scripts/deploy-commands.js
-
- echo "Discord commands deployment completed!"
\ No newline at end of file
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Read Node version
+ run: |
+ NODE_VERSION=$(cat .nvmrc | sed 's/v//')
+ echo "NODE_VERSION=$NODE_VERSION" >> $GITHUB_ENV
+
+ - name: Deploy Discord Commands to VPS
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.VPS_HOST }}
+ username: ${{ secrets.VPS_USER }}
+ key: ${{ secrets.VPS_SSH_KEY }}
+ script: |
+ cd /home/${{ secrets.VPS_USER }}/webdev-bot-deploy
+
+ # Read NODE_VERSION from .nvmrc
+ export NODE_VERSION=$(cat .nvmrc | sed 's/v//')
+ echo "Using Node version: $NODE_VERSION"
+
+ # Run deploy script inside the already running Docker container
+ # .env file should already exist from main deployment
+ echo "Deploying Discord commands..."
+ docker compose --profile prod exec bot-prod node dist/scripts/deploy-commands.js
+
+ echo "Discord commands deployment completed!"
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index a92a697..469c9ae 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -2,7 +2,7 @@ name: Build, Test and Deploy Discord Bot to VPS
on:
push:
- branches: [ "main" ]
+ branches: ['main']
workflow_dispatch: # Allow manual deployment
jobs:
@@ -10,49 +10,49 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Read Node version
- run: |
- NODE_VERSION=$(cat .nvmrc | sed 's/v//')
- echo "NODE_VERSION=$NODE_VERSION" >> $GITHUB_ENV
-
- - name: Deploy to VPS
- uses: appleboy/ssh-action@v1.0.3
- with:
- host: ${{ secrets.VPS_HOST }}
- username: ${{ secrets.VPS_USER }}
- key: ${{ secrets.VPS_SSH_KEY }}
- script: |
- cd /home/${{ secrets.VPS_USER }}/webdev-bot-deploy
-
- # Stash any local changes
- git stash push -m "Auto-deploy $(date)" 2>/dev/null || true
-
- # Pull latest changes
- git checkout main
- git pull origin main
-
- # Read NODE_VERSION from .nvmrc
- export NODE_VERSION=$(cat .nvmrc | sed 's/v//')
- echo "Using Node version: $NODE_VERSION"
-
- # Create .env file with secrets
- # Public config comes from .env.production (committed to repo)
- # NODE_ENV=production is set in docker-compose.yml
- cat > .env << EOF
- DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }}
- CLIENT_ID=${{ secrets.CLIENT_ID }}
- EOF
-
- # Stop any existing containers
- docker compose down || true
-
- # Build and start production container with profile
- # NODE_ENV=production is explicitly set in docker-compose.yml bot-prod service
- docker compose --profile prod up -d --build
-
- # Check status
- echo "Deployment completed. Container status:"
- docker compose ps
\ No newline at end of file
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Read Node version
+ run: |
+ NODE_VERSION=$(cat .nvmrc | sed 's/v//')
+ echo "NODE_VERSION=$NODE_VERSION" >> $GITHUB_ENV
+
+ - name: Deploy to VPS
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.VPS_HOST }}
+ username: ${{ secrets.VPS_USER }}
+ key: ${{ secrets.VPS_SSH_KEY }}
+ script: |
+ cd /home/${{ secrets.VPS_USER }}/webdev-bot-deploy
+
+ # Stash any local changes
+ git stash push -m "Auto-deploy $(date)" 2>/dev/null || true
+
+ # Pull latest changes
+ git checkout main
+ git pull origin main
+
+ # Read NODE_VERSION from .nvmrc
+ export NODE_VERSION=$(cat .nvmrc | sed 's/v//')
+ echo "Using Node version: $NODE_VERSION"
+
+ # Create .env file with secrets
+ # Public config comes from .env.production (committed to repo)
+ # NODE_ENV=production is set in docker-compose.yml
+ cat > .env << EOF
+ DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }}
+ CLIENT_ID=${{ secrets.CLIENT_ID }}
+ EOF
+
+ # Stop any existing containers
+ docker compose down || true
+
+ # Build and start production container with profile
+ # NODE_ENV=production is explicitly set in docker-compose.yml bot-prod service
+ docker compose --profile prod up -d --build
+
+ # Check status
+ echo "Deployment completed. Container status:"
+ docker compose ps
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1b067fe..806e678 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -2,7 +2,7 @@ name: Test
on:
pull_request:
- branches: [ "**" ]
+ branches: ['**']
paths-ignore:
- 'docs/**'
- '.gitignore'
@@ -32,15 +32,14 @@ jobs:
npm install --no-audit --no-fund
fi
- - name: Lint and Format
- run: npm run check
+ - name: Lint
+ run: npm run lint
- - name: Typecheck
- run: npm run typecheck
+ - name: Format
+ run: npm run fmt:check
- name: Build
run: npm run build
- name: Test
run: npm run test:ci
-
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..99e2f7d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["oxc.oxc-vscode"]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9affadd..19fa416 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,16 +1,23 @@
{
- "editor.defaultFormatter": "biomejs.biome",
- "prettier.enable": false,
- "oxc.enable": false,
- "editor.formatOnSave": true,
- "editor.codeActionsOnSave": {
- "source.fixAll.biome": "explicit"
- },
- "[typescript]": {
- "editor.defaultFormatter": "biomejs.biome"
- },
- "[json]": {
- "editor.defaultFormatter": "biomejs.biome"
- },
- "typescript.tsdk": "node_modules/typescript/lib"
+ "biome.enabled": false,
+ "prettier.enable": false,
+ "eslint.enable": false,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.oxc": "always"
+ },
+ "oxc.fmt.configPath": "oxfmt.config.ts",
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true,
+ "[typescript]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[javascript]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ },
+ "[json]": {
+ "editor.defaultFormatter": "oxc.oxc-vscode",
+ "editor.formatOnSave": true
+ }
}
diff --git a/DOCKER.md b/DOCKER.md
index 2011742..432b0f5 100644
--- a/DOCKER.md
+++ b/DOCKER.md
@@ -40,6 +40,7 @@ docker compose --profile dev up
```
In development mode:
+
- Code changes in `src/` are automatically detected and the bot restarts
- Full development dependencies are available
- Logs are streamed to your terminal
@@ -67,6 +68,7 @@ docker compose --profile prod up -d
```
In production mode:
+
- Minimal image size (only production dependencies)
- Optimized for runtime performance
- Runs in detached mode by default
@@ -124,6 +126,7 @@ docker run -it \
### Bot Not Starting
1. Check if environment variables are set correctly:
+
```bash
docker compose --profile dev run --rm bot-dev env | grep DISCORD
```
@@ -149,6 +152,7 @@ chmod 755 src/
If code changes aren't being detected in development mode:
1. Verify the volume mounts are correct:
+
```bash
docker inspect webdev-bot-dev | grep Mounts -A 10
```
@@ -161,6 +165,7 @@ If code changes aren't being detected in development mode:
### Image Size Concerns
The production image is optimized for size using:
+
- Alpine Linux base image
- Multi-stage builds (dev dependencies not included)
- Only compiled `dist/` folder and production dependencies
@@ -193,27 +198,31 @@ docker compose build --no-cache
## Additional Commands
View running containers:
+
```bash
docker ps
```
View all containers (including stopped):
+
```bash
docker ps -a
```
Remove all stopped containers:
+
```bash
docker compose down -v
```
View container logs in real-time:
+
```bash
docker compose --profile dev logs -f bot-dev
```
Execute commands inside the container:
+
```bash
docker compose --profile dev exec bot-dev sh
```
-
diff --git a/README.md b/README.md
index 560c23b..d9f3fe0 100644
--- a/README.md
+++ b/README.md
@@ -4,20 +4,22 @@ A comprehensive Discord bot designed specifically for the Web Dev Discord server
# Features
-
### Documentation Commands
+
- **`/mdn [query]`** - Search MDN Web Docs for web development documentation
- **`/npm [query]`** - Search npm registry for packages
- **`/baseline [query]`** - Check browser baseline compatibility for web features
-
### Educational Resources
+
- **`/guides [subject]`** - Access detailed guides on various web development topics
### Discord Server Tips
+
- **`/tips [subject]`** - Get quick tips on interacting within the Web Dev Discord server
### Moderation Tools
+
- **`/repel [target] [reason]`** - Advanced moderation command (meant to be given to a high quantity of volunteer assistants) that:
- 1: Checks bot permissions.
- 2: Checks target's role to make sure it's under in hierarchy.
@@ -25,8 +27,8 @@ A comprehensive Discord bot designed specifically for the Web Dev Discord server
- 4: Deletes target user's very recent messages across channels.
- 5: Logs the action to a channel.
-
### Utility Commands
+
- **`/ping`** - Basic connectivity test to verify bot responsiveness
@@ -34,25 +36,28 @@ A comprehensive Discord bot designed specifically for the Web Dev Discord server
# Installation & Setup
### Prerequisites
+
- Node.js (version specified in `.nvmrc`)
- pnpm package manager
- Discord Bot Token
-
1. Clone the repository:
+
```bash
git clone
cd webdev-bot
```
2. Install dependencies:
+
```bash
pnpm install
```
3. Create a `.env` file based on `.env.example` and fill in the required environment variables:
+
```bash
cp .env.example .env
# Edit .env to add your Discord bot token and other configurations
@@ -71,9 +76,10 @@ A comprehensive Discord bot designed specifically for the Web Dev Discord server
-
## Docker Support
+
To use docker with the bot, run:
+
```bash
# Development
pnpm run docker:dev
@@ -87,9 +93,6 @@ pnpm run docker:build
-
-
-
# Required Permissions
- Send Messages
@@ -100,7 +103,6 @@ pnpm run docker:build
-
# Contributing
1. Fork the repository
@@ -111,8 +113,8 @@ pnpm run docker:build
-
### Adding New Guides or Tips
+
1. Add markdown files to `src/commands/guides/subjects/` or `src/commands/tips/subjects/`
2. Include frontmatter with `name` field
3. The bot will automatically detect and load new content
@@ -122,10 +124,10 @@ pnpm run docker:build
# Support
For issues, questions, or feature requests:
+
- Open an issue on GitHub
- Contact the Web Dev Discord server moderators
-
**Made with ❤️ for the Web Dev Discord community**
diff --git a/assets/guides/a11y.md b/assets/guides/a11y.md
index 7312e84..888ba10 100644
--- a/assets/guides/a11y.md
+++ b/assets/guides/a11y.md
@@ -4,9 +4,10 @@ name: Accessibility (A11y)
Improve your web accessibility skills with these valuable free resources:
-**Reference & Guidelines**
-- [**MDN Web Accessibility**]() - Comprehensive guides and best practices.
-- [**WAI (Web Accessibility Initiative)**]() - Official guidelines and resources for web accessibility.
-- [**a11y Project**]() - Community-driven resources and checklists.
-- [**WebAIM**]() - Articles, tools, and training on web accessibility.
-- [**web.dev Accessibility**]() - Tutorials and best practices for building accessible web experiences.
\ No newline at end of file
+**Reference & Guidelines**
+
+- [**MDN Web Accessibility**](https://developer.mozilla.org/en-US/docs/Learn/Accessibility) - Comprehensive guides and best practices.
+- [**WAI (Web Accessibility Initiative)**](https://www.w3.org/WAI/) - Official guidelines and resources for web accessibility.
+- [**a11y Project**](https://www.a11yproject.com/) - Community-driven resources and checklists.
+- [**WebAIM**](https://webaim.org/) - Articles, tools, and training on web accessibility.
+- [**web.dev Accessibility**](https://web.dev/accessibility/) - Tutorials and best practices for building accessible web experiences.
diff --git a/assets/guides/backend.md b/assets/guides/backend.md
index 3999423..8675bdc 100644
--- a/assets/guides/backend.md
+++ b/assets/guides/backend.md
@@ -4,13 +4,13 @@ name: Backend Development
Boost your backend development skills with these top free resources:
-**Structured Learning Paths**
-- [**Roadmap**]() - A comprehensive guide to backend technologies and concepts.
+**Structured Learning Paths**
+
+- [**Roadmap**](https://roadmap.sh/backend) - A comprehensive guide to backend technologies and concepts.
+
+**Backends**
-**Backends**
- SQLlite (recommended) - Lightweight, disk-based database.
- PostgreSQL (recommended) - Advanced, open-source relational database.
- Mongodb (only pick if you have to.) - NoSQL database for flexible, JSON-like documents.
-- Redis - In-memory data structure store, used as a database, cache, and message broker.
-
-
+- Redis - In-memory data structure store, used as a database, cache, and message broker.
diff --git a/assets/guides/css.md b/assets/guides/css.md
index b798986..c79b28a 100644
--- a/assets/guides/css.md
+++ b/assets/guides/css.md
@@ -4,12 +4,14 @@ name: CSS
Level up your CSS skills with these free and practical resources:
-**Reference & Guides**
-- [**MDN**]() - Comprehensive CSS documentation.
-- [**web.dev**]() - Modern CSS best practices and tutorials.
-- [**CSS Tricks**]() - Tips, tricks, and in-depth articles.
-
-**Interactive Learning**
-- [**CSS Grid Garden**]() - Master CSS Grid with fun exercises.
-- [**Flexbox Froggy**]() - Learn Flexbox by solving interactive challenges.
-- [**A Complete Guide to Flexbox**]() - Detailed reference and examples.
+**Reference & Guides**
+
+- [**MDN**](https://developer.mozilla.org/en-US/docs/Web/CSS) - Comprehensive CSS documentation.
+- [**web.dev**](https://web.dev/css) - Modern CSS best practices and tutorials.
+- [**CSS Tricks**](https://css-tricks.com/category/articles/) - Tips, tricks, and in-depth articles.
+
+**Interactive Learning**
+
+- [**CSS Grid Garden**](https://cssgridgarden.com/) - Master CSS Grid with fun exercises.
+- [**Flexbox Froggy**](https://flexboxfroggy.com/) - Learn Flexbox by solving interactive challenges.
+- [**A Complete Guide to Flexbox**](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) - Detailed reference and examples.
diff --git a/assets/guides/frontend.md b/assets/guides/frontend.md
index 0de338c..403bf8f 100644
--- a/assets/guides/frontend.md
+++ b/assets/guides/frontend.md
@@ -4,15 +4,17 @@ name: Frontend Development
Sharpen your frontend skills with these curated free resources:
-**Structured Learning Paths**
-- [**The Odin Project**]() - A full curriculum from basics to projects.
-- [**Roadmap**]() - Step-by-step guide of what to learn next in frontend development.
+**Structured Learning Paths**
-**Practice & Challenges**
-- [**Frontend Mentor**]() - Build real projects and improve your coding skills.
+- [**The Odin Project**](https://www.theodinproject.com/paths/foundations/courses/foundations) - A full curriculum from basics to projects.
+- [**Roadmap**](https://roadmap.sh/frontend) - Step-by-step guide of what to learn next in frontend development.
-**Reference & Guides**
-- [**JavaScript Info**]() - Deep dive into JavaScript concepts and examples.
-- [**CSS Tricks**]() - Tips, tricks, and guides for CSS and modern layouts.
-- [**MDN Web Docs**]() - Comprehensive reference for HTML, CSS, and JS.
+**Practice & Challenges**
+- [**Frontend Mentor**](https://www.frontendmentor.io/) - Build real projects and improve your coding skills.
+
+**Reference & Guides**
+
+- [**JavaScript Info**](https://javascript.info/) - Deep dive into JavaScript concepts and examples.
+- [**CSS Tricks**](https://css-tricks.com/) - Tips, tricks, and guides for CSS and modern layouts.
+- [**MDN Web Docs**](https://developer.mozilla.org/en-US/curriculum/) - Comprehensive reference for HTML, CSS, and JS.
diff --git a/assets/guides/javascript.md b/assets/guides/javascript.md
index 6c97469..65d113a 100644
--- a/assets/guides/javascript.md
+++ b/assets/guides/javascript.md
@@ -4,13 +4,15 @@ name: JavaScript
Enhance your JavaScript skills with these essential free resources:
-**Reference & Guides**
-- [**MDN JavaScript**]() - Complete documentation and tutorials.
-- [**JavaScript Info**]() - In-depth guides from basics to advanced concepts.
-- [**Eloquent JavaScript**]() - A modern introduction to programming using JavaScript.
-- [**web.dev**]() - Learn JavaScript with practical examples and best practices.
-- [**Roadmap**]() - A structured path to mastering JavaScript.
+**Reference & Guides**
-**Interactive Learning & Challenges**
-- [**Frontend Mentor (JS Projects)**]() - Apply JS in real-world projects.
-- [**FreeCodeCamp (JS Course)**]() - Learn JavaScript through interactive coding challenges.
\ No newline at end of file
+- [**MDN JavaScript**](https://developer.mozilla.org/en-US/docs/Web/JavaScript) - Complete documentation and tutorials.
+- [**JavaScript Info**](https://javascript.info/) - In-depth guides from basics to advanced concepts.
+- [**Eloquent JavaScript**](https://eloquentjavascript.net/) - A modern introduction to programming using JavaScript.
+- [**web.dev**](https://web.dev/learn/javascript/) - Learn JavaScript with practical examples and best practices.
+- [**Roadmap**](https://roadmap.sh/javascript) - A structured path to mastering JavaScript.
+
+**Interactive Learning & Challenges**
+
+- [**Frontend Mentor (JS Projects)**](https://www.frontendmentor.io/) - Apply JS in real-world projects.
+- [**FreeCodeCamp (JS Course)**](https://www.freecodecamp.org/learn/full-stack-developer) - Learn JavaScript through interactive coding challenges.
diff --git a/assets/guides/tailwind-with-vite.md b/assets/guides/tailwind-with-vite.md
index 54870e4..aeab0d1 100644
--- a/assets/guides/tailwind-with-vite.md
+++ b/assets/guides/tailwind-with-vite.md
@@ -11,4 +11,4 @@ Here is a quick rundown, which do not replace the docs however!
3. Configure Vite plugin of Tailwind
4. Import Tailwind CSS
5. Start your dev process
-6. Start using Tailwind!
\ No newline at end of file
+6. Start using Tailwind!
diff --git a/assets/guides/vue.md b/assets/guides/vue.md
index 3134458..69b6e33 100644
--- a/assets/guides/vue.md
+++ b/assets/guides/vue.md
@@ -4,12 +4,15 @@ name: Vue
Enhance your Vue.js skills with these valuable free resources:
-**Official Documentation & Guides**
-- [**Vue.js Official Documentation**]() - Comprehensive guides and API references.
+**Official Documentation & Guides**
-**Community Guides**
-- [**How to learn Vue**]() - A guide to learning Vue effectively.
+- [**Vue.js Official Documentation**](https://vuejs.org/) - Comprehensive guides and API references.
+
+**Community Guides**
+
+- [**How to learn Vue**](https://vue-land.github.io/faq/learning-vue) - A guide to learning Vue effectively.
**Tools**
-- [**Vueuse**]() - A collection of essential Vue Composition Utilities.
-- [**Vue Playground**]() - An online editor to experiment with Vue.js code snippets.
\ No newline at end of file
+
+- [**Vueuse**](https://vueuse.org/) - A collection of essential Vue Composition Utilities.
+- [**Vue Playground**](https://play.vuejs.org/) - An online editor to experiment with Vue.js code snippets.
diff --git a/assets/tips/ai-help.md b/assets/tips/ai-help.md
index 081f6a2..82ec202 100644
--- a/assets/tips/ai-help.md
+++ b/assets/tips/ai-help.md
@@ -2,5 +2,5 @@
name: AI Help
---
-If your project was entirely AI-generated, please demonstrate an understanding of the project & error(s) before asking for help.
+If your project was entirely AI-generated, please demonstrate an understanding of the project & error(s) before asking for help.
Check <#1292083042051031150> for info on this. Otherwise, post on <#1402408742514982942> with no expectation of assistance, continue iterating with AI, or hire a human in <#598513460019462144>.
diff --git a/assets/tips/english.md b/assets/tips/english.md
index 7a20c46..8c63971 100644
--- a/assets/tips/english.md
+++ b/assets/tips/english.md
@@ -2,6 +2,6 @@
name: English Only
---
-English only. Solo inglés. Anglais seulement. Nur Englisch. Solo inglese. Apenas inglês. Только английский. 仅限英语. 英語のみ. 영어만. الإنجليزية فقط. केवल अंग्रेजी. Alleen Engels. Endast engelska. Kun engelsk. Vain englanti. Tylko angielski. Sadece İngilizce. Μόνο αγγλικά. อังกฤษเท่านั้น. Chỉ tiếng Anh. Bahasa Inggris saja.
-
+English only. Solo inglés. Anglais seulement. Nur Englisch. Solo inglese. Apenas inglês. Только английский. 仅限英语. 英語のみ. 영어만. الإنجليزية فقط. केवल अंग्रेजी. Alleen Engels. Endast engelska. Kun engelsk. Vain englanti. Tylko angielski. Sadece İngilizce. Μόνο αγγλικά. อังกฤษเท่านั้น. Chỉ tiếng Anh. Bahasa Inggris saja.
+
**This allows us to effectively moderate and gives everyone an opportunity to join in on the conversation.**
diff --git a/assets/tips/format.md b/assets/tips/format.md
index abfd612..c7bf373 100644
--- a/assets/tips/format.md
+++ b/assets/tips/format.md
@@ -14,4 +14,4 @@ This will render as:
console.log('Hello, world!');
```
-You can replace \`js\` with other languages too, e.g. \`php\`, \`css\`, \`html\`, \`ts\`, \`md\`, \`sql\`, etc.
\ No newline at end of file
+You can replace \`js\` with other languages too, e.g. \`php\`, \`css\`, \`html\`, \`ts\`, \`md\`, \`sql\`, etc.
diff --git a/assets/tips/justask.md b/assets/tips/justask.md
index 5d0711f..4b3942b 100644
--- a/assets/tips/justask.md
+++ b/assets/tips/justask.md
@@ -3,4 +3,4 @@ name: Just Ask
---
**Don't ask to ask, just ask!**
-Here's why: https://sol.gfxile.net/dontask.html
\ No newline at end of file
+Here's why: https://sol.gfxile.net/dontask.html
diff --git a/biome.json b/biome.json
deleted file mode 100644
index 4813a99..0000000
--- a/biome.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
- "linter": {
- "enabled": true,
- "rules": {
- "recommended": true,
- "style": {
- "noNonNullAssertion": "off",
- "useConst": "error",
- "useTemplate": "error",
- "useBlockStatements": "error"
- },
- "suspicious": {
- "noExplicitAny": "warn",
- "noArrayIndexKey": "off"
- },
- "correctness": {
- "noUnusedVariables": "error"
- }
- }
- },
- "formatter": {
- "enabled": true,
- "indentStyle": "space",
- "indentWidth": 2,
- "lineWidth": 100
- },
- "javascript": {
- "formatter": {
- "quoteStyle": "single",
- "trailingCommas": "es5",
- "semicolons": "always"
- }
- },
- "files": {
- "includes": ["*.ts", "*.js", "*.json", "src/**/*", "test/**/*"]
- }
-}
diff --git a/docs/GUIDE_SYNC.md b/docs/GUIDE_SYNC.md
index ec873dd..e08e9c2 100644
--- a/docs/GUIDE_SYNC.md
+++ b/docs/GUIDE_SYNC.md
@@ -5,6 +5,7 @@ The bot automatically synchronizes guide markdown files from `src/commands/guide
## Setup
Add to your `.env` file:
+
```
GUIDES_CHANNEL_ID=1234567890123456789
```
@@ -24,4 +25,4 @@ name: JavaScript
---
Your guide content here...
-```
\ No newline at end of file
+```
diff --git a/oxfmt.config.ts b/oxfmt.config.ts
new file mode 100644
index 0000000..4c27dc0
--- /dev/null
+++ b/oxfmt.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'oxfmt';
+
+export default defineConfig({
+ printWidth: 80,
+ useTabs: false,
+ tabWidth: 2,
+ semi: true,
+ singleQuote: true,
+ trailingComma: 'es5',
+ bracketSpacing: true,
+ arrowParens: 'always',
+ ignorePatterns: ['node_modules', 'dist', 'build', 'coverage', '.git'],
+});
diff --git a/oxlint.config.ts b/oxlint.config.ts
new file mode 100644
index 0000000..7d5c909
--- /dev/null
+++ b/oxlint.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'oxlint';
+
+export default defineConfig({
+ options: {
+ typeAware: true,
+ typeCheck: true,
+ },
+ plugins: ['eslint', 'typescript'],
+ rules: {
+ curly: 'warn',
+ 'prefer-template': 'warn',
+ 'typescript/no-explicit-any': 'error',
+ 'prefer-const': 'error',
+ },
+ ignorePatterns: ['node_modules', 'dist', 'build', 'coverage', '.git'],
+});
diff --git a/package.json b/package.json
index 8a1ace7..a9c2915 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,9 @@
"name": "webdev-bot",
"version": "1.0.0",
"description": "Web Dev & Web Design discord bot",
+ "keywords": [],
+ "license": "MIT",
+ "author": "",
"type": "module",
"scripts": {
"build": "tsup && tsc-alias",
@@ -12,7 +15,10 @@
"docker:dev": "NODE_VERSION=$(cat .nvmrc | tr -d 'v') docker compose --profile dev up",
"docker:prod": "NODE_VERSION=$(cat .nvmrc | tr -d 'v') docker compose --profile prod up -d",
"docker:stop": "docker compose down",
- "check": "biome check --write .",
+ "lint": "oxlint",
+ "lint:fix": "oxlint --fix",
+ "fmt": "oxfmt",
+ "fmt:check": "oxfmt --check",
"typecheck": "tsc --noEmit",
"test": "tsx --test '**/*.test.ts'",
"test:ci": "NODE_ENV=test node --test dist/**/*.test.js",
@@ -21,14 +27,9 @@
"sync-guides": "tsx src/scripts/sync-guides.ts",
"sync-guides:init": "tsx src/scripts/sync-guides.ts --initialize"
},
- "keywords": [],
- "author": "",
- "license": "MIT",
- "packageManager": "pnpm@10.17.1",
"dependencies": {
"discord.js": "^14.26.4",
"node-cron": "^4.2.1",
- "typescript": "^5.9.3",
"web-features": "^3.9.2"
},
"devDependencies": {
@@ -37,14 +38,17 @@
"@types/node-cron": "^3.0.11",
"husky": "^9.1.7",
"lint-staged": "^16.2.1",
+ "oxfmt": "^0.55.0",
+ "oxlint": "^1.70.0",
+ "oxlint-tsgolint": "^0.23.0",
"tsc-alias": "^1.8.17",
"tsup": "^8.5.0",
- "tsx": "^4.20.6"
+ "tsx": "^4.20.6",
+ "typescript": "^6.0.3"
},
"lint-staged": {
- "*.{ts,js}": [
- "biome format --write",
- "biome lint --fix"
- ]
- }
+ "*.{js,ts}": "pnpm run lint",
+ "*.{js,ts,json}": "oxfmt --no-error-on-unmatched-pattern"
+ },
+ "packageManager": "pnpm@10.17.1"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index faa547a..dba88d8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,9 +14,6 @@ importers:
node-cron:
specifier: ^4.2.1
version: 4.2.1
- typescript:
- specifier: ^5.9.3
- version: 5.9.3
web-features:
specifier: ^3.9.2
version: 3.9.2
@@ -36,15 +33,27 @@ importers:
lint-staged:
specifier: ^16.2.1
version: 16.2.4
+ oxfmt:
+ specifier: ^0.55.0
+ version: 0.55.0
+ oxlint:
+ specifier: ^1.70.0
+ version: 1.70.0(oxlint-tsgolint@0.23.0)
+ oxlint-tsgolint:
+ specifier: ^0.23.0
+ version: 0.23.0
tsc-alias:
specifier: ^1.8.17
version: 1.8.17
tsup:
specifier: ^8.5.0
- version: 8.5.0(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)
+ version: 8.5.0(tsx@4.20.6)(typescript@6.0.3)(yaml@2.8.1)
tsx:
specifier: ^4.20.6
version: 4.20.6
+ typescript:
+ specifier: ^6.0.3
+ version: 6.0.3
packages:
@@ -314,6 +323,264 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@oxfmt/binding-android-arm-eabi@0.55.0':
+ resolution: {integrity: sha512-+rFDOqQe5LOWgxrAJaZgLRudr6GQm0wGI6gtu7vVkrdLGjNMUSGbAlaCr8j7F2H2Er97vYQCU8WDb30onqMM1g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxfmt/binding-android-arm64@0.55.0':
+ resolution: {integrity: sha512-ctulLq8s3x8Zmvw6+iccB09TIKERAklRSmbJ10gk8mlAn05qZxoyo52dj3Hi9IJcmDSwF54fQaTVh2CbL6PInw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxfmt/binding-darwin-arm64@0.55.0':
+ resolution: {integrity: sha512-xDQczLH9pw/RBk1h/GH0qcGMm8hQtmtVHBNLSH3lk1gEIR09hZ4L+mJQl4VqiVAvPK9VG9PYrWWuSQLt7xTbiA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxfmt/binding-darwin-x64@0.55.0':
+ resolution: {integrity: sha512-JaNoFCkF2CJdGgpPSMbuO9HVyXyoNGIhMHPvp6NYAjeVKw9XEYc0HcUWJLPQa3Q69WV5wMa9m5jPMJPtbLtcRg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxfmt/binding-freebsd-x64@0.55.0':
+ resolution: {integrity: sha512-DNbszhpg6S2MIzax5azdHFTTBIVkR5xr8yyRZuA4yoDAwOkzIp3tmldgKZM2+VlT+hJIG0xUksA+elISzMEAfA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxfmt/binding-linux-arm-gnueabihf@0.55.0':
+ resolution: {integrity: sha512-2snoaoRfFFyGnbOcKUK36rREBYxe/Xgz3uHbiA5zbCB/s6R4DQj4mHqYAaWWhgizCUSDxV8cE9zAZ0XleNpKGw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxfmt/binding-linux-arm-musleabihf@0.55.0':
+ resolution: {integrity: sha512-q1aktHF/WRpSK81BX1dE/9vWrS2jGw1Nax2kb4DBLGAewubCLcoNyp4Zl/NSMgbv3vUS46Z33wIQkBVYOP3PYg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxfmt/binding-linux-arm64-gnu@0.55.0':
+ resolution: {integrity: sha512-VD0y36aENezl/3tsclA/4G53Cc7iV+7Uoh7gz4yvcOTaEYBtJpQsE6PKDGTtUtOvGS4kv51ybfXY/nWZejO5IA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-arm64-musl@0.55.0':
+ resolution: {integrity: sha512-r8xlKJFcsRmn0H5jZrdORae6RX9jDBrZVvOoxF+bCQtampQJClv80aZEHsv+NsLsp2KCE5ql79O7DpPVzYWpXA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-ppc64-gnu@0.55.0':
+ resolution: {integrity: sha512-GRKv/HXHcwIVld/WU61rF0g0R16hl5EJ+ScKdpjevT57lnLnagj/U2YUbXf2mT+2Pg1uCzWC+mvGicPV3CDdLQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-riscv64-gnu@0.55.0':
+ resolution: {integrity: sha512-rdv57enTiPtpSYRMKfAiEbQb0Puw5t9N7isVinDoo5qeLDScro2gznmZqSgSWbVZRzLisTeCTW8Qwgw0bOHv3A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-riscv64-musl@0.55.0':
+ resolution: {integrity: sha512-7v1nNrlD43VY6+sYQ6efYyb3lE6QY182304PD/768ZxTjOmFd/3dQa3u/nGBUAXYdGSWOQc5N3PnS0QzUXyEIA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-s390x-gnu@0.55.0':
+ resolution: {integrity: sha512-f4lJLUSPOgScjFl9LiflKCTocyNRwE25JmTMbN4XQdDjoZzEHjqf3wA3VESF1/csg7i8m7+EQLbrZyYDqe10UQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxfmt/binding-linux-x64-gnu@0.55.0':
+ resolution: {integrity: sha512-MihqiPziJNoWy4MqNSV+jVA1g+07iQDjZiR0vaCaDoPgFEiJpCMsxamktzLV07cEeQsSJ04vQaU4CzCQwIvtDA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-x64-musl@0.55.0':
+ resolution: {integrity: sha512-Yqghym7KYAVjP9MmSrNZiDeerMuoejNjo0r3ox5H3GDKk8eAfl8VyJm9i+pWCLDCTnAbcTUMMN2ZKjUYXH1v3g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxfmt/binding-openharmony-arm64@0.55.0':
+ resolution: {integrity: sha512-s5SDvVVSbyQl1V5UU3Yl12M+XLUQ3rl5SglNqgAA2K4PXUtQhyNSS00wivONPEnNo5W01rCou8WkDNyvI/RGHg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxfmt/binding-win32-arm64-msvc@0.55.0':
+ resolution: {integrity: sha512-7p9FB5R32tw2KyyNX3wpQrR2WHwEHvMEiBlGXxeTCaRMCVNx3UtFMAUbaQ/pRNWIrEUZmYhJ6tcUH52uPTRYjQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxfmt/binding-win32-ia32-msvc@0.55.0':
+ resolution: {integrity: sha512-ZYqj3fDnOT1IaVGMP5kpmkQl4F3tQIm2ZyAxvqkJYmI0xgWWak4ss4XYwv3VDfM+TWXeC9K4uQ/wW5jm/5XABA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxfmt/binding-win32-x64-msvc@0.55.0':
+ resolution: {integrity: sha512-eEYT5tivGnGbPHuOHuQpi6CGLObhh0re/5jcNQHihD2GRYkTM85dyi5a19zjP8Q00t1uqAx+/QGLUGdHeqzWyg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@oxlint-tsgolint/darwin-arm64@0.23.0':
+ resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxlint-tsgolint/darwin-x64@0.23.0':
+ resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxlint-tsgolint/linux-arm64@0.23.0':
+ resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint-tsgolint/linux-x64@0.23.0':
+ resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint-tsgolint/win32-arm64@0.23.0':
+ resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxlint-tsgolint/win32-x64@0.23.0':
+ resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@oxlint/binding-android-arm-eabi@1.70.0':
+ resolution: {integrity: sha512-zFh0P4cswmRvw6nkyb89dr18rRanuaCPAsEXsFDoQY8WdaquI8Pt4NWFjaMJg6L23cy5NeN8J9cBnREbWzZhaw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxlint/binding-android-arm64@1.70.0':
+ resolution: {integrity: sha512-qI8o4HZjeGiBrWv+pJv4lH0Yi2Gl/JSp/EumBUApezJprIKa5PS4nU0lQsQngtky8k+SplQIOjv6hwu0SSxeyg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxlint/binding-darwin-arm64@1.70.0':
+ resolution: {integrity: sha512-8KjgVVHI5F9nVwHCRwwA78Ty7zNKP4Wd9OeN5PSv3iu/F/u1RVXoOCgLhWqust6HmwQG6xc8c+RCyaWENy24+w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxlint/binding-darwin-x64@1.70.0':
+ resolution: {integrity: sha512-WVydssv5PSUBXFJTdNBWlmGkbNmvPGaFt/2SUT/EZRB6bq6bEOHmMlbnupZD5jmlEvi9+mZJHi8TCw15lyfSfQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxlint/binding-freebsd-x64@1.70.0':
+ resolution: {integrity: sha512-hJucmUf8OlinHNb1R7fI4Fw6WsAstOz7i8nmkWQfiHoZXtbufNm+MxiDTIMk1ggh2Ro4vLzgQ+bKvRY54MZoRA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.70.0':
+ resolution: {integrity: sha512-1BnS7wbCYDSXwWzJJ+mc3NURoha6m6m6RT5c6vgAY3oz7C3OVXP+S0awo2mRq97arrJkVvO3qRQfyAHL+76xtQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm-musleabihf@1.70.0':
+ resolution: {integrity: sha512-yKy/UdbR55+M2yEcuiV5DCNC/gdQAjr/GioUy50QwBzSrKm8ueWADqyRLS9Xk+qjNeCYGg6A8FvUBds56ttfqg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-gnu@1.70.0':
+ resolution: {integrity: sha512-0A5XJ4alvmqFUFP/4oYSyaO+qLto/HrKEWTSaegiVl+HOufFngK2BjYw9x4RbwBt/du5QG6l5q1zeWiJYYG5yg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-musl@1.70.0':
+ resolution: {integrity: sha512-JiylyurlB0CLSedNtx1gzv3FvfWPF1h/2Y3BJszPLNt5XQFlBsH5ke0Jle3iJb3uqu5m2e7A/DwzpuCAHdiU+A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint/binding-linux-ppc64-gnu@1.70.0':
+ resolution: {integrity: sha512-J8VPG7I3/HmgaU4u8pNU2kFx2+0U+vPLS1dXFxXOaR/2TQ0f8AC7DRz0SRGRI1bfphnX2hVYTTtLuhL4nYKL+Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@oxlint/binding-linux-riscv64-gnu@1.70.0':
+ resolution: {integrity: sha512-N2+4lV2KLN+oXTIIIwmWDhwkrnvqf5oX7Hw0zPjk+RuIVgiBQSOlJWF7uQoFx2siEYX0ZQ5cfSbEAHm+J3t7Wg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxlint/binding-linux-riscv64-musl@1.70.0':
+ resolution: {integrity: sha512-1e2L7cFCvx9QDzq6NPP+0tABKb5z6nWHyddWTNKprEsjO9xNrAtPowuCGpjNXxkTdsMiZ4jc8YQ5SstZd4XK6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxlint/binding-linux-s390x-gnu@1.70.0':
+ resolution: {integrity: sha512-Kwu/l/8GcYibCWA9m9N5pRXMIKVSsL/YbgpLzYkqDhWTiqdRfnNJ/+nqIKRKQiFbHWsdlHEhzMwruJK+qcEruA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxlint/binding-linux-x64-gnu@1.70.0':
+ resolution: {integrity: sha512-tap04CsHYOl0nSAQJfPNIuBxqEPB2HnhQqwaOXLg1jnp2XfRo8Fa814dA4QC4zpvTWXCjAAaCY1W5LOORkEQuQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint/binding-linux-x64-musl@1.70.0':
+ resolution: {integrity: sha512-hzJa/WgvtJpbBD9rgfy0qe+MjbxOXNUT0bfR1S6EQQzfTtBFA9xg5q8KSwRrQ2QfSS+TaP4j+4mVPQrfNc6UNg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint/binding-openharmony-arm64@1.70.0':
+ resolution: {integrity: sha512-xbsaNSNzVSnaJACCUYr1HQMyY/Q/Q1LkePmHG3UvZPvGCYGNxrsZp9OmtA6ick8xH47ltRRbRrPCM1YXYcyC+A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxlint/binding-win32-arm64-msvc@1.70.0':
+ resolution: {integrity: sha512-icAEsUI7JbW1TMRdEXV83mVAInhRVQYuuAlPpxdGwJ95chNdnCzjloRW8GglT0WvzOEZSio6fnYSk2DJ2Hv7LQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxlint/binding-win32-ia32-msvc@1.70.0':
+ resolution: {integrity: sha512-FHMSWbVsPVs/f+Jcl04ws4JJ2wUnauyTzlpxWRG/lSO/8GpX08Fo2gQZqdA6CrRFI+zvkxl+N/KwJGWfUwYVZA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxlint/binding-win32-x64-msvc@1.70.0':
+ resolution: {integrity: sha512-ptOlKwCz7n4AKs5VweMqG6DAg677FmKOK+vBkkL9DMNgFATIQ+upqUYBTOEwRQyRAx1ncGlPlXleV2hIcm3z4g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -805,6 +1072,36 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
+ oxfmt@0.55.0:
+ resolution: {integrity: sha512-jSj2wCTakwgPMxkfiVZX0jf+nX+Nz6xlyAZjqNE0qXTFdCBPYlP6JAN+ODjmealw7DXBjOzYbdsqwBMAZnPZ6A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ svelte: ^5.0.0
+ vite-plus: '*'
+ peerDependenciesMeta:
+ svelte:
+ optional: true
+ vite-plus:
+ optional: true
+
+ oxlint-tsgolint@0.23.0:
+ resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==}
+ hasBin: true
+
+ oxlint@1.70.0:
+ resolution: {integrity: sha512-D6JgHtzkhRwvEC+A0Nw5AEc5bk8x5i1pHzvZIEf/a0C4hOzmAACNGtkDGPyFaxxX3ZVGxCPeig3P3rMM8XU3/g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ oxlint-tsgolint: '>=0.22.1'
+ vite-plus: '*'
+ peerDependenciesMeta:
+ oxlint-tsgolint:
+ optional: true
+ vite-plus:
+ optional: true
+
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@@ -985,6 +1282,10 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
+ tinypool@2.1.0:
+ resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
+ engines: {node: ^20.0.0 || >=22.0.0}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -1034,8 +1335,8 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
- typescript@5.9.3:
- resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
hasBin: true
@@ -1291,6 +1592,138 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
+ '@oxfmt/binding-android-arm-eabi@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-android-arm64@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-darwin-arm64@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-darwin-x64@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-freebsd-x64@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm-gnueabihf@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm-musleabihf@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm64-gnu@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm64-musl@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-ppc64-gnu@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-riscv64-gnu@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-riscv64-musl@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-s390x-gnu@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-x64-gnu@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-linux-x64-musl@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-openharmony-arm64@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-win32-arm64-msvc@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-win32-ia32-msvc@0.55.0':
+ optional: true
+
+ '@oxfmt/binding-win32-x64-msvc@0.55.0':
+ optional: true
+
+ '@oxlint-tsgolint/darwin-arm64@0.23.0':
+ optional: true
+
+ '@oxlint-tsgolint/darwin-x64@0.23.0':
+ optional: true
+
+ '@oxlint-tsgolint/linux-arm64@0.23.0':
+ optional: true
+
+ '@oxlint-tsgolint/linux-x64@0.23.0':
+ optional: true
+
+ '@oxlint-tsgolint/win32-arm64@0.23.0':
+ optional: true
+
+ '@oxlint-tsgolint/win32-x64@0.23.0':
+ optional: true
+
+ '@oxlint/binding-android-arm-eabi@1.70.0':
+ optional: true
+
+ '@oxlint/binding-android-arm64@1.70.0':
+ optional: true
+
+ '@oxlint/binding-darwin-arm64@1.70.0':
+ optional: true
+
+ '@oxlint/binding-darwin-x64@1.70.0':
+ optional: true
+
+ '@oxlint/binding-freebsd-x64@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-musleabihf@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-gnu@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-musl@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-ppc64-gnu@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-gnu@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-musl@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-s390x-gnu@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-gnu@1.70.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-musl@1.70.0':
+ optional: true
+
+ '@oxlint/binding-openharmony-arm64@1.70.0':
+ optional: true
+
+ '@oxlint/binding-win32-arm64-msvc@1.70.0':
+ optional: true
+
+ '@oxlint/binding-win32-ia32-msvc@1.70.0':
+ optional: true
+
+ '@oxlint/binding-win32-x64-msvc@1.70.0':
+ optional: true
+
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -1735,6 +2168,62 @@ snapshots:
dependencies:
mimic-function: 5.0.1
+ oxfmt@0.55.0:
+ dependencies:
+ tinypool: 2.1.0
+ optionalDependencies:
+ '@oxfmt/binding-android-arm-eabi': 0.55.0
+ '@oxfmt/binding-android-arm64': 0.55.0
+ '@oxfmt/binding-darwin-arm64': 0.55.0
+ '@oxfmt/binding-darwin-x64': 0.55.0
+ '@oxfmt/binding-freebsd-x64': 0.55.0
+ '@oxfmt/binding-linux-arm-gnueabihf': 0.55.0
+ '@oxfmt/binding-linux-arm-musleabihf': 0.55.0
+ '@oxfmt/binding-linux-arm64-gnu': 0.55.0
+ '@oxfmt/binding-linux-arm64-musl': 0.55.0
+ '@oxfmt/binding-linux-ppc64-gnu': 0.55.0
+ '@oxfmt/binding-linux-riscv64-gnu': 0.55.0
+ '@oxfmt/binding-linux-riscv64-musl': 0.55.0
+ '@oxfmt/binding-linux-s390x-gnu': 0.55.0
+ '@oxfmt/binding-linux-x64-gnu': 0.55.0
+ '@oxfmt/binding-linux-x64-musl': 0.55.0
+ '@oxfmt/binding-openharmony-arm64': 0.55.0
+ '@oxfmt/binding-win32-arm64-msvc': 0.55.0
+ '@oxfmt/binding-win32-ia32-msvc': 0.55.0
+ '@oxfmt/binding-win32-x64-msvc': 0.55.0
+
+ oxlint-tsgolint@0.23.0:
+ optionalDependencies:
+ '@oxlint-tsgolint/darwin-arm64': 0.23.0
+ '@oxlint-tsgolint/darwin-x64': 0.23.0
+ '@oxlint-tsgolint/linux-arm64': 0.23.0
+ '@oxlint-tsgolint/linux-x64': 0.23.0
+ '@oxlint-tsgolint/win32-arm64': 0.23.0
+ '@oxlint-tsgolint/win32-x64': 0.23.0
+
+ oxlint@1.70.0(oxlint-tsgolint@0.23.0):
+ optionalDependencies:
+ '@oxlint/binding-android-arm-eabi': 1.70.0
+ '@oxlint/binding-android-arm64': 1.70.0
+ '@oxlint/binding-darwin-arm64': 1.70.0
+ '@oxlint/binding-darwin-x64': 1.70.0
+ '@oxlint/binding-freebsd-x64': 1.70.0
+ '@oxlint/binding-linux-arm-gnueabihf': 1.70.0
+ '@oxlint/binding-linux-arm-musleabihf': 1.70.0
+ '@oxlint/binding-linux-arm64-gnu': 1.70.0
+ '@oxlint/binding-linux-arm64-musl': 1.70.0
+ '@oxlint/binding-linux-ppc64-gnu': 1.70.0
+ '@oxlint/binding-linux-riscv64-gnu': 1.70.0
+ '@oxlint/binding-linux-riscv64-musl': 1.70.0
+ '@oxlint/binding-linux-s390x-gnu': 1.70.0
+ '@oxlint/binding-linux-x64-gnu': 1.70.0
+ '@oxlint/binding-linux-x64-musl': 1.70.0
+ '@oxlint/binding-openharmony-arm64': 1.70.0
+ '@oxlint/binding-win32-arm64-msvc': 1.70.0
+ '@oxlint/binding-win32-ia32-msvc': 1.70.0
+ '@oxlint/binding-win32-x64-msvc': 1.70.0
+ oxlint-tsgolint: 0.23.0
+
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
@@ -1909,6 +2398,8 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
+ tinypool@2.1.0: {}
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -1935,7 +2426,7 @@ snapshots:
tslib@2.8.1: {}
- tsup@8.5.0(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1):
+ tsup@8.5.0(tsx@4.20.6)(typescript@6.0.3)(yaml@2.8.1):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.11)
cac: 6.7.14
@@ -1955,7 +2446,7 @@ snapshots:
tinyglobby: 0.2.15
tree-kill: 1.2.2
optionalDependencies:
- typescript: 5.9.3
+ typescript: 6.0.3
transitivePeerDependencies:
- jiti
- supports-color
@@ -1969,7 +2460,7 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
- typescript@5.9.3: {}
+ typescript@6.0.3: {}
ufo@1.6.1: {}
diff --git a/src/common/commands/create-commands.ts b/src/common/commands/create-commands.ts
index a0d8306..a6128af 100644
--- a/src/common/commands/create-commands.ts
+++ b/src/common/commands/create-commands.ts
@@ -1,5 +1,9 @@
import { ApplicationCommandType } from 'discord.js';
-import type { MessageContextMenuCommand, SlashCommand, UserContextMenuCommand } from './types.js';
+import type {
+ MessageContextMenuCommand,
+ SlashCommand,
+ UserContextMenuCommand,
+} from './types.js';
export const createSlashCommand = (command: {
data: Omit;
diff --git a/src/common/commands/types.ts b/src/common/commands/types.ts
index c348027..af71f73 100644
--- a/src/common/commands/types.ts
+++ b/src/common/commands/types.ts
@@ -10,13 +10,17 @@ import type {
export type UserContextMenuCommand = {
commandType: ApplicationCommandType.User;
data: RESTPostAPIContextMenuApplicationCommandsJSONBody;
- execute: (interaction: UserContextMenuCommandInteraction) => Promise | void;
+ execute: (
+ interaction: UserContextMenuCommandInteraction
+ ) => Promise | void;
};
export type MessageContextMenuCommand = {
commandType: ApplicationCommandType.Message;
data: RESTPostAPIContextMenuApplicationCommandsJSONBody;
- execute: (interaction: MessageContextMenuCommandInteraction) => Promise | void;
+ execute: (
+ interaction: MessageContextMenuCommandInteraction
+ ) => Promise | void;
};
export type SlashCommand = {
@@ -25,7 +29,10 @@ export type SlashCommand = {
execute: (interaction: ChatInputCommandInteraction) => Promise | void;
};
-export type Command = SlashCommand | UserContextMenuCommand | MessageContextMenuCommand;
+export type Command =
+ | SlashCommand
+ | UserContextMenuCommand
+ | MessageContextMenuCommand;
export type CommandInteraction =
| ChatInputCommandInteraction
diff --git a/src/common/interactions/autocomplete-interaction.ts b/src/common/interactions/autocomplete-interaction.ts
index 26dc836..3060073 100644
--- a/src/common/interactions/autocomplete-interaction.ts
+++ b/src/common/interactions/autocomplete-interaction.ts
@@ -5,10 +5,17 @@ export type AutoCompleteSubmitInteraction = {
handler: (interaction: AutocompleteInteraction) => Promise | void;
};
-export const autoCompleteInteractions = new Map();
+export const autoCompleteInteractions = new Map<
+ string,
+ AutoCompleteSubmitInteraction
+>();
-export const registerAutocompleteInteraction = (interaction: AutoCompleteSubmitInteraction) => {
- console.log(`Registering autocomplete interaction: ${interaction.commandName}`);
+export const registerAutocompleteInteraction = (
+ interaction: AutoCompleteSubmitInteraction
+) => {
+ console.log(
+ `Registering autocomplete interaction: ${interaction.commandName}`
+ );
autoCompleteInteractions.set(interaction.commandName, interaction);
};
diff --git a/src/common/interactions/button-interaction.ts b/src/common/interactions/button-interaction.ts
index 2b433ac..1b51220 100644
--- a/src/common/interactions/button-interaction.ts
+++ b/src/common/interactions/button-interaction.ts
@@ -6,14 +6,23 @@ export type ButtonSubmitInteraction = {
handler: (interaction: ButtonInteraction) => Promise | void;
};
-export const buttonSubmitInteractions = new Map();
+export const buttonSubmitInteractions = new Map<
+ string,
+ ButtonSubmitInteraction
+>();
-export const registerButtonSubmitInteraction = (interaction: ButtonSubmitInteraction) => {
- console.log(`Registering button submit interaction: ${interaction.commandName}`);
+export const registerButtonSubmitInteraction = (
+ interaction: ButtonSubmitInteraction
+) => {
+ console.log(
+ `Registering button submit interaction: ${interaction.commandName}`
+ );
buttonSubmitInteractions.set(interaction.commandName, interaction);
};
-export const handleButtonInteraction = async (interaction: ButtonInteraction): Promise => {
+export const handleButtonInteraction = async (
+ interaction: ButtonInteraction
+): Promise => {
const commandName = parseCustomId(interaction.customId)[0];
await buttonSubmitInteractions.get(commandName)?.handler(interaction);
};
diff --git a/src/common/interactions/modal-interaction.ts b/src/common/interactions/modal-interaction.ts
index 2579282..949bc17 100644
--- a/src/common/interactions/modal-interaction.ts
+++ b/src/common/interactions/modal-interaction.ts
@@ -6,14 +6,23 @@ export type ModalSubmitInteraction = {
handler: (interaction: ModalInteraction) => Promise | void;
};
-export const modalSubmitInteractions = new Map();
+export const modalSubmitInteractions = new Map<
+ string,
+ ModalSubmitInteraction
+>();
-export const registerModalSubmitInteraction = (interaction: ModalSubmitInteraction) => {
- console.log(`Registering modal submit interaction: ${interaction.commandName}`);
+export const registerModalSubmitInteraction = (
+ interaction: ModalSubmitInteraction
+) => {
+ console.log(
+ `Registering modal submit interaction: ${interaction.commandName}`
+ );
modalSubmitInteractions.set(interaction.commandName, interaction);
};
-export const handleModalInteraction = async (interaction: ModalInteraction): Promise => {
+export const handleModalInteraction = async (
+ interaction: ModalInteraction
+): Promise => {
const commandName = parseCustomId(interaction.customId)[0];
await modalSubmitInteractions.get(commandName)?.handler(interaction);
};
diff --git a/src/env.ts b/src/env.ts
index 6488015..ad1535c 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -58,5 +58,9 @@ export type Config = typeof config;
// Log loaded configuration (without sensitive values)
console.log('✅ Configuration loaded successfully');
-console.log(`📋 Client ID: ${config.discord.clientId ? config.discord.clientId : '❌ missing'}`);
-console.log(`🔑 Token: ${config.discord.token ? '***configured***' : '❌ missing'}`);
+console.log(
+ `📋 Client ID: ${config.discord.clientId ? config.discord.clientId : '❌ missing'}`
+);
+console.log(
+ `🔑 Token: ${config.discord.token ? '***configured***' : '❌ missing'}`
+);
diff --git a/src/features/auto-roles/index.ts b/src/features/auto-roles/index.ts
index 6262350..cad0276 100644
--- a/src/features/auto-roles/index.ts
+++ b/src/features/auto-roles/index.ts
@@ -12,7 +12,11 @@ export const autoRoleEvent = createEvent(
async (_, newMember) => {
const hasRoleC = hasAllRoles(newMember, config.roleIds.c);
if (!hasRoleC) {
- const hasRequiredRoles = hasAllRoles(newMember, config.roleIds.a, config.roleIds.b);
+ const hasRequiredRoles = hasAllRoles(
+ newMember,
+ config.roleIds.a,
+ config.roleIds.b
+ );
if (hasRequiredRoles) {
try {
await newMember.roles.add(config.roleIds.c);
diff --git a/src/features/docs/baseline.ts b/src/features/docs/baseline.ts
index 4171a3b..d47c4e2 100644
--- a/src/features/docs/baseline.ts
+++ b/src/features/docs/baseline.ts
@@ -25,10 +25,12 @@ export type FeatureData = {
type FeatureItem = FeatureData & { key: string };
// Prepare baseline features by excluding non-feature entries and converting to array
-const features: FeatureItem[] = Object.entries(getBaselineFeatures(data)).map(([key, feature]) => ({
- ...feature,
- key,
-}));
+const features: FeatureItem[] = Object.entries(getBaselineFeatures(data)).map(
+ ([key, feature]) => ({
+ ...feature,
+ key,
+ })
+);
const baselines = {
high: {
@@ -37,7 +39,8 @@ const baselines = {
description: 'Widely supported',
},
low: {
- image: 'https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-word-dark.png',
+ image:
+ 'https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-word-dark.png',
description: 'Newly supported',
},
none: {
@@ -50,7 +53,8 @@ const baselines = {
const baseConfig = createBaseConfig({
color: 0x4e_8c_2f,
icon: '',
- commandDescription: 'Get baseline support information for web platform features',
+ commandDescription:
+ 'Get baseline support information for web platform features',
});
export const baselineProvider: ProviderConfig = {
@@ -63,29 +67,32 @@ export const baselineProvider: ProviderConfig = {
limit: 20,
});
},
- createCollection: (items) => new Collection(items.map((item) => [item.key, item])),
+ createCollection: (items) =>
+ new Collection(items.map((item) => [item.key, item])),
createActionBuilders: (data) => {
- const selectRow = new ActionRowBuilder().addComponents(
- new StringSelectMenuBuilder()
- .setCustomId('baseline-select')
- .setPlaceholder('Select one feature')
- .setMinValues(1)
- .setMaxValues(1)
- .addOptions(
- ...data.map((feature) => ({
- label: clampText(feature.name, 100),
- description: clampText(feature.description, 100),
- value: feature.key,
- }))
- )
- );
+ const selectRow =
+ new ActionRowBuilder().addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId('baseline-select')
+ .setPlaceholder('Select one feature')
+ .setMinValues(1)
+ .setMaxValues(1)
+ .addOptions(
+ ...data.map((feature) => ({
+ label: clampText(feature.name, 100),
+ description: clampText(feature.description, 100),
+ value: feature.key,
+ }))
+ )
+ );
- const buttonRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder()
- .setLabel('Cancel')
- .setStyle(ButtonStyle.Danger)
- .setCustomId('baseline-cancel')
- );
+ const buttonRow =
+ new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setLabel('Cancel')
+ .setStyle(ButtonStyle.Danger)
+ .setCustomId('baseline-cancel')
+ );
return { selectRow, buttonRow };
},
diff --git a/src/features/docs/index.ts b/src/features/docs/index.ts
index c5f08bf..6c4f851 100644
--- a/src/features/docs/index.ts
+++ b/src/features/docs/index.ts
@@ -12,27 +12,28 @@ const docProviders = {
baseline: baselineProvider,
};
-export const docsCommands = Object.entries(docProviders).map(([providerKey, providerConfig]) =>
- createSlashCommand({
- data: {
- name: providerKey,
- description: providerConfig.commandDescription,
- options: [
- {
- name: 'query',
- type: ApplicationCommandOptionType.String,
- description: 'The search query',
- required: true,
- },
- {
- name: 'user',
- type: ApplicationCommandOptionType.User,
- description: 'The user to mention in the response',
- required: false,
- },
- ],
- },
- execute: async (interaction) =>
- executeDocCommand(providerConfig as ProviderConfig, interaction),
- })
+export const docsCommands = Object.entries(docProviders).map(
+ ([providerKey, providerConfig]) =>
+ createSlashCommand({
+ data: {
+ name: providerKey,
+ description: providerConfig.commandDescription,
+ options: [
+ {
+ name: 'query',
+ type: ApplicationCommandOptionType.String,
+ description: 'The search query',
+ required: true,
+ },
+ {
+ name: 'user',
+ type: ApplicationCommandOptionType.User,
+ description: 'The user to mention in the response',
+ required: false,
+ },
+ ],
+ },
+ execute: async (interaction) =>
+ executeDocCommand(providerConfig as ProviderConfig, interaction),
+ })
);
diff --git a/src/features/docs/mdn.ts b/src/features/docs/mdn.ts
index e43d9a9..77f24bd 100644
--- a/src/features/docs/mdn.ts
+++ b/src/features/docs/mdn.ts
@@ -40,32 +40,40 @@ export const mdnProvider: ProviderConfig = {
);
if (!response.ok) {
- throw new Error(`Error fetching MDN data: ${response.status} ${response.statusText}`);
+ throw new Error(
+ `Error fetching MDN data: ${response.status} ${response.statusText}`
+ );
}
const data = (await response.json()) as SearchResult;
return data.documents;
},
- createCollection: (items) => new Collection(items.map((item) => [item.slug, item])),
+ createCollection: (items) =>
+ new Collection(items.map((item) => [item.slug, item])),
createActionBuilders: (data) => {
- const selectRow = new ActionRowBuilder().addComponents(
- new StringSelectMenuBuilder()
- .setCustomId('mdn-select')
- .setPlaceholder('Select 1 to 5 results')
- .setMinValues(1)
- .setMaxValues(Math.min(5, data.size))
- .addOptions(
- ...data.map((doc) => ({
- label: clampText(doc.title, 100),
- description: clampText(doc.summary, 100),
- value: doc.slug,
- }))
- )
- );
+ const selectRow =
+ new ActionRowBuilder().addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId('mdn-select')
+ .setPlaceholder('Select 1 to 5 results')
+ .setMinValues(1)
+ .setMaxValues(Math.min(5, data.size))
+ .addOptions(
+ ...data.map((doc) => ({
+ label: clampText(doc.title, 100),
+ description: clampText(doc.summary, 100),
+ value: doc.slug,
+ }))
+ )
+ );
- const buttonRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder().setLabel('Cancel').setStyle(ButtonStyle.Danger).setCustomId('mdn-cancel')
- );
+ const buttonRow =
+ new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setLabel('Cancel')
+ .setStyle(ButtonStyle.Danger)
+ .setCustomId('mdn-cancel')
+ );
return { selectRow, buttonRow };
},
diff --git a/src/features/docs/npm.ts b/src/features/docs/npm.ts
index 4428ac5..989dde9 100644
--- a/src/features/docs/npm.ts
+++ b/src/features/docs/npm.ts
@@ -42,36 +42,47 @@ export const npmProvider: ProviderConfig = {
...baseConfig,
getFilteredData: async (query: string) => {
const response = await fetch(
- getSearchUrl(`https://registry.npmjs.org/-/v1/search?text=${SEARCH_TERM}&size=10`, query)
+ getSearchUrl(
+ `https://registry.npmjs.org/-/v1/search?text=${SEARCH_TERM}&size=10`,
+ query
+ )
);
if (!response.ok) {
- throw new Error(`Error fetching NPM data: ${response.status} ${response.statusText}`);
+ throw new Error(
+ `Error fetching NPM data: ${response.status} ${response.statusText}`
+ );
}
const data = (await response.json()) as SearchResult;
return data.objects.map((obj) => obj.package);
},
- createCollection: (items) => new Collection(items.map((item) => [item.name, item])),
+ createCollection: (items) =>
+ new Collection(items.map((item) => [item.name, item])),
createActionBuilders: (data) => {
- const selectRow = new ActionRowBuilder().addComponents(
- new StringSelectMenuBuilder()
- .setCustomId('npm-select')
- .setPlaceholder('Select a package')
- .setMinValues(1)
- .setMaxValues(1)
- .addOptions(
- ...data.map((pkg) =>
- new StringSelectMenuOptionBuilder()
- .setLabel(pkg.name)
- .setDescription(clampText(pkg.description, 100))
- .setValue(pkg.name)
+ const selectRow =
+ new ActionRowBuilder().addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId('npm-select')
+ .setPlaceholder('Select a package')
+ .setMinValues(1)
+ .setMaxValues(1)
+ .addOptions(
+ ...data.map((pkg) =>
+ new StringSelectMenuOptionBuilder()
+ .setLabel(pkg.name)
+ .setDescription(clampText(pkg.description, 100))
+ .setValue(pkg.name)
+ )
)
- )
- );
- const buttonRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder().setCustomId('npm-cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger)
- );
+ );
+ const buttonRow =
+ new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId('npm-cancel')
+ .setLabel('Cancel')
+ .setStyle(ButtonStyle.Danger)
+ );
return { selectRow, buttonRow };
},
createResultEmbeds: (selectedItems) =>
@@ -90,7 +101,9 @@ export const npmProvider: ProviderConfig = {
value,
}))
)
- .setFooter({ text: `Version ${pkg.version} | License: ${pkg.license ?? 'N/A'}` })
+ .setFooter({
+ text: `Version ${pkg.version} | License: ${pkg.license ?? 'N/A'}`,
+ })
.setTimestamp()
),
getDisplayTitle: (item) => item.name,
diff --git a/src/features/docs/types.ts b/src/features/docs/types.ts
index bccbb24..8dd2544 100644
--- a/src/features/docs/types.ts
+++ b/src/features/docs/types.ts
@@ -23,7 +23,9 @@ export type ProviderConfig- = {
createActionBuilders: (data: Collection) => ActionBuilders;
// Create result embeds to show after selection
- createResultEmbeds: (data: Collection) => EmbedBuilder | EmbedBuilder[];
+ createResultEmbeds: (
+ data: Collection
+ ) => EmbedBuilder | EmbedBuilder[];
// Get display title for an item
getDisplayTitle: (item: Item) => string;
diff --git a/src/features/docs/utils.test.ts b/src/features/docs/utils.test.ts
index b10f6f3..a2439da 100644
--- a/src/features/docs/utils.test.ts
+++ b/src/features/docs/utils.test.ts
@@ -3,8 +3,8 @@ import { describe, it } from 'node:test';
import { features } from 'web-features';
import { getBaselineFeatures, NON_BASELINE_FEATURES } from './utils.js';
-describe('getBaselineFeatures', () => {
- it('should return the correct baseline features when provided with non-features key array', () => {
+void describe('getBaselineFeatures', () => {
+ void it('should return the correct baseline features when provided with non-features key array', () => {
const originalFeatures = {
'feature-1': {
name: 'Feature 1',
@@ -49,7 +49,7 @@ describe('getBaselineFeatures', () => {
assert.deepStrictEqual(result, expectedFeatures);
});
- it('NON_BASELINE_FEATURES should contain the correct features to exclude', () => {
+ void it('NON_BASELINE_FEATURES should contain the correct features to exclude', () => {
const expectedNonBaselineFeatures = Object.entries(features)
.filter(([, feature]) => feature.kind !== 'feature')
.map(([key]) => key);
diff --git a/src/features/docs/utils.ts b/src/features/docs/utils.ts
index 8c00f22..e728055 100644
--- a/src/features/docs/utils.ts
+++ b/src/features/docs/utils.ts
@@ -16,7 +16,10 @@ export const createBaseConfig = (options: {
icon: string;
commandDescription: string;
directUrl?: string;
-}): Pick => options;
+}): Pick<
+ ProviderConfig,
+ 'color' | 'icon' | 'commandDescription' | 'directUrl'
+> => options;
export const executeDocCommand = async (
config: ProviderConfig,
@@ -43,7 +46,9 @@ export const executeDocCommand = async (
const items = await config.getFilteredData(query);
if (items.length === 0) {
- await interaction.editReply({ content: `No results found for "${query}"` });
+ await interaction.editReply({
+ content: `No results found for "${query}"`,
+ });
return;
}
@@ -56,13 +61,16 @@ export const executeDocCommand = async (
});
const collector = choiceInteraction.createMessageComponentCollector({
- filter: (componentInteraction) => componentInteraction.user.id === interaction.user.id,
+ filter: (componentInteraction) =>
+ componentInteraction.user.id === interaction.user.id,
});
collector.once('collect', async (componentInteraction) => {
if (componentInteraction.isStringSelectMenu()) {
const selectedSet = new Set(componentInteraction.values);
- const selectedItems = collection.filter((_, key) => selectedSet.has(key));
+ const selectedItems = collection.filter((_, key) =>
+ selectedSet.has(key)
+ );
const selectedTitles = selectedItems.map(config.getDisplayTitle);
await interaction.editReply({
@@ -72,7 +80,7 @@ export const executeDocCommand = async (
const embeds = config.createResultEmbeds(selectedItems);
- logToChannel({
+ void logToChannel({
channel: interaction.channel,
content: {
type: 'embed',
@@ -89,7 +97,7 @@ export const executeDocCommand = async (
});
} catch (error) {
console.error(`executeDocCommand FAILED:`, error);
- await interaction.editReply({ content: `Error: ${error}` });
+ await interaction.editReply({ content: `Error: ${String(error)}` });
}
};
diff --git a/src/features/guides/index.ts b/src/features/guides/index.ts
index 299d461..11f4a8d 100644
--- a/src/features/guides/index.ts
+++ b/src/features/guides/index.ts
@@ -70,7 +70,10 @@ export const guidesCommand = createSlashCommand({
},
});
- await interaction.reply({ content: 'Guide sent!', flags: MessageFlags.Ephemeral });
+ await interaction.reply({
+ content: 'Guide sent!',
+ flags: MessageFlags.Ephemeral,
+ });
return;
},
diff --git a/src/features/has-var/index.ts b/src/features/has-var/index.ts
index e2a177c..cbe5f32 100644
--- a/src/features/has-var/index.ts
+++ b/src/features/has-var/index.ts
@@ -8,12 +8,19 @@ import { rateLimit } from '../../util/rate-limit.js';
const { canRun, reset } = rateLimit(5 * MINUTE);
const hasVarDeclaration = (code: string, language: string): boolean => {
- if (!['js', 'javascript', 'ts', 'typescript'].includes(language.toLowerCase())) {
+ if (
+ !['js', 'javascript', 'ts', 'typescript'].includes(language.toLowerCase())
+ ) {
return false;
}
try {
- const sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true);
+ const sourceFile = ts.createSourceFile(
+ 'temp.ts',
+ code,
+ ts.ScriptTarget.Latest,
+ true
+ );
let foundVar = false;
@@ -21,7 +28,8 @@ const hasVarDeclaration = (code: string, language: string): boolean => {
if (ts.isVariableStatement(node)) {
const declList = node.declarationList;
const isVar =
- (declList.flags & ts.NodeFlags.Let) === 0 && (declList.flags & ts.NodeFlags.Const) === 0;
+ (declList.flags & ts.NodeFlags.Let) === 0 &&
+ (declList.flags & ts.NodeFlags.Const) === 0;
if (isVar) {
foundVar = true;
}
diff --git a/src/features/interaction-create/index.ts b/src/features/interaction-create/index.ts
index 0e7ed42..69ade02 100644
--- a/src/features/interaction-create/index.ts
+++ b/src/features/interaction-create/index.ts
@@ -12,7 +12,9 @@ export const interactionCreateEvent = createEvent(
},
async (interaction) => {
if (!interaction.guildId || !isAllowedServer(interaction.guildId)) {
- console.log(`⚠️ Command blocked from unauthorized server: ${interaction.guildId}`);
+ console.log(
+ `⚠️ Command blocked from unauthorized server: ${interaction.guildId}`
+ );
if (interaction.isRepliable()) {
await interaction.reply({
content: '❌ This bot is not authorized to operate in this server.',
@@ -22,19 +24,25 @@ export const interactionCreateEvent = createEvent(
return;
}
if (interaction.isButton()) {
- console.log(`Received button interaction with custom ID: ${interaction.customId}`);
+ console.log(
+ `Received button interaction with custom ID: ${interaction.customId}`
+ );
await handleButtonInteraction(interaction);
return;
}
if (interaction.isModalSubmit()) {
- console.log(`Received modal submit interaction with custom ID: ${interaction.customId}`);
+ console.log(
+ `Received modal submit interaction with custom ID: ${interaction.customId}`
+ );
await handleModalInteraction(interaction);
return;
}
if (interaction.isAutocomplete()) {
- console.log(`Received autocomplete interaction with custom ID: ${interaction.commandName}`);
+ console.log(
+ `Received autocomplete interaction with custom ID: ${interaction.commandName}`
+ );
await handleAutoCompleteInteraction(interaction);
return;
}
@@ -55,17 +63,23 @@ export const interactionCreateEvent = createEvent(
if (command.commandType === ApplicationCommandType.ChatInput) {
if (!interaction.isChatInputCommand()) {
- throw new Error('Command type mismatch: expected ChatInput interaction');
+ throw new Error(
+ 'Command type mismatch: expected ChatInput interaction'
+ );
}
await command.execute(interaction);
} else if (command.commandType === ApplicationCommandType.Message) {
if (!interaction.isMessageContextMenuCommand()) {
- throw new Error('Command type mismatch: expected Message context menu interaction');
+ throw new Error(
+ 'Command type mismatch: expected Message context menu interaction'
+ );
}
await command.execute(interaction);
} else if (command.commandType === ApplicationCommandType.User) {
if (!interaction.isUserContextMenuCommand()) {
- throw new Error('Command type mismatch: expected User context menu interaction');
+ throw new Error(
+ 'Command type mismatch: expected User context menu interaction'
+ );
}
await command.execute(interaction);
}
diff --git a/src/features/just-ask/index.test.ts b/src/features/just-ask/index.test.ts
index 5a17ffd..57a09aa 100644
--- a/src/features/just-ask/index.test.ts
+++ b/src/features/just-ask/index.test.ts
@@ -2,7 +2,7 @@ import assert from 'node:assert';
import { describe, it } from 'node:test';
import { isAskingToAsk } from './just-ask.js';
-describe('justAsk Regex', () => {
+void describe('justAsk Regex', () => {
const testCases = [
{ input: 'Anyone knows js?', expected: true },
{ input: 'Somebody can help with Python?', expected: true },
@@ -14,7 +14,7 @@ describe('justAsk Regex', () => {
{ input: 'This is a question without the pattern.', expected: false },
];
testCases.forEach(({ input, expected }) => {
- it(`should return ${expected} for input: "${input}"`, () => {
+ void it(`should return ${expected} for input: "${input}"`, () => {
const result = isAskingToAsk(input);
assert.strictEqual(result, expected);
});
diff --git a/src/features/just-ask/just-ask.ts b/src/features/just-ask/just-ask.ts
index e36b3c6..fb6070a 100644
--- a/src/features/just-ask/just-ask.ts
+++ b/src/features/just-ask/just-ask.ts
@@ -48,7 +48,8 @@ const shouldCheck = (message: Message) => {
// check roles/permissions
if (
message.member !== null &&
- (message.member.roles.highest.comparePositionTo(config.roleIds.repel) >= 0 ||
+ (message.member.roles.highest.comparePositionTo(config.roleIds.repel) >=
+ 0 ||
message.member.permissions.has(PermissionFlagsBits.ModerateMembers))
) {
return false;
diff --git a/src/features/moderation/cache-messages.ts b/src/features/moderation/cache-messages.ts
index 90dbd69..3fd1072 100644
--- a/src/features/moderation/cache-messages.ts
+++ b/src/features/moderation/cache-messages.ts
@@ -1,4 +1,8 @@
-import { ApplicationCommandOptionType, PermissionFlagsBits, PermissionsBitField } from 'discord.js';
+import {
+ ApplicationCommandOptionType,
+ PermissionFlagsBits,
+ PermissionsBitField,
+} from 'discord.js';
import { createSlashCommand } from '../../common/commands/create-commands.js';
import { fetchAndCachePublicChannelsMessages } from '../../util/cache.js';
@@ -25,15 +29,21 @@ export default createSlashCommand({
return;
}
- if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageMessages)) {
- await interaction.editReply('You do not have permission to use this command.');
+ if (
+ !interaction.memberPermissions?.has(PermissionFlagsBits.ManageMessages)
+ ) {
+ await interaction.editReply(
+ 'You do not have permission to use this command.'
+ );
return;
}
const guild = interaction.guild;
const force = interaction.options.getBoolean('force') ?? false;
- await interaction.editReply('Caching messages in all public text channels...');
+ await interaction.editReply(
+ 'Caching messages in all public text channels...'
+ );
const { cachedChannels, totalChannels, failedChannels } =
await fetchAndCachePublicChannelsMessages(guild, force);
diff --git a/src/features/moderation/repel.ts b/src/features/moderation/repel.ts
index 518d818..b6826c8 100644
--- a/src/features/moderation/repel.ts
+++ b/src/features/moderation/repel.ts
@@ -45,7 +45,10 @@ const checkCanRepel = ({
commandUser.permissions.has(PermissionFlagsBits.ModerateMembers) ||
commandUser.roles.cache.has(repelRole.id);
if (!hasPermission) {
- return { ok: false, message: 'You do not have permission to use this command.' };
+ return {
+ ok: false,
+ message: 'You do not have permission to use this command.',
+ };
}
return { ok: true };
};
@@ -80,11 +83,17 @@ const checkCanRepelTarget = ({
}
if (target.roles.highest.position >= commandUser.roles.highest.position) {
- return { ok: false, message: 'You cannot repel this user due to role hierarchy.' };
+ return {
+ ok: false,
+ message: 'You cannot repel this user due to role hierarchy.',
+ };
}
if (target.roles.highest.position >= botMember.roles.highest.position) {
- return { ok: false, message: 'I cannot repel this user due to role hierarchy.' };
+ return {
+ ok: false,
+ message: 'I cannot repel this user due to role hierarchy.',
+ };
}
return { ok: true };
@@ -111,7 +120,9 @@ const sendReasonToTarget = async ({
const containerComponent = new ContainerBuilder()
.setAccentColor(Colors.DarkRed)
- .addTextDisplayComponents(new TextDisplayBuilder().setContent(messageContent));
+ .addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(messageContent)
+ );
try {
await target.send({
flags: MessageFlags.IsComponentsV2,
@@ -126,7 +137,10 @@ const sendReasonToTarget = async ({
const getTargetFromInteraction = async (
interaction: ChatInputCommandInteraction
): Promise => {
- const targetFromOption = interaction.options.getUser(RepelOptions.TARGET, true);
+ const targetFromOption = interaction.options.getUser(
+ RepelOptions.TARGET,
+ true
+ );
let target: User | GuildMember | null = null;
if (!interaction.inGuild() || interaction.guild === null) {
return targetFromOption;
@@ -147,7 +161,11 @@ const handleTimeout = async ({
target: GuildMember | User;
durationInMilliseconds: number;
}): Promise => {
- if (durationInMilliseconds === 0 || !isUserInServer(target) || isUserTimedOut(target)) {
+ if (
+ durationInMilliseconds === 0 ||
+ !isUserInServer(target) ||
+ isUserTimedOut(target)
+ ) {
return 0;
}
try {
@@ -194,9 +212,16 @@ const handleDeleteMessages = async ({
const targetMessages = new Map();
for (const [id, message] of channel.messages.cache) {
- if (message.author && message.author.id === target.id && message.deletable) {
+ if (
+ message.author &&
+ message.author.id === target.id &&
+ message.deletable
+ ) {
targetMessages.set(id, message);
- latestMessageTimestamp = Math.max(latestMessageTimestamp, message.createdTimestamp);
+ latestMessageTimestamp = Math.max(
+ latestMessageTimestamp,
+ message.createdTimestamp
+ );
}
}
@@ -228,14 +253,19 @@ const handleDeleteMessages = async ({
await channel.bulkDelete(messagesToDelete, true);
deleted += messagesToDelete.length;
} catch (error) {
- console.error(`Error deleting messages in channel ${channel.name}:`, error);
+ console.error(
+ `Error deleting messages in channel ${channel.name}:`,
+ error
+ );
failedChannels.push(channel.id);
throw error;
}
})
);
if (failedChannels.length > 0) {
- console.error(`Failed to delete messages in ${failedChannels.length} channel(s).`);
+ console.error(
+ `Failed to delete messages in ${failedChannels.length} channel(s).`
+ );
}
return { deleted, failedChannels };
};
@@ -274,12 +304,16 @@ const logRepelAction = async ({
name: isUserInServer(target)
? `${target.user.tag} | Repel | ${target.user.username}`
: `${target.tag} | Repel | ${target.username}`,
- iconURL: isUserInServer(target) ? target.user.displayAvatarURL() : target.displayAvatarURL(),
+ iconURL: isUserInServer(target)
+ ? target.user.displayAvatarURL()
+ : target.displayAvatarURL(),
};
const commandEmbed = new EmbedBuilder()
.setAuthor(memberAuthor)
- .setDescription(`Used \`repel\` command in ${channelInfo}.\n${buildCommandString(interaction)}`)
+ .setDescription(
+ `Used \`repel\` command in ${channelInfo}.\n${buildCommandString(interaction)}`
+ )
.setColor('Green')
.setTimestamp();
const resultEmbed = new EmbedBuilder()
@@ -331,11 +365,14 @@ const logRepelAction = async ({
.setTimestamp()
: null;
- const modMessage = interaction.options.getString(RepelOptions.MESSAGE_FOR_MODS) ?? false;
+ const modMessage =
+ interaction.options.getString(RepelOptions.MESSAGE_FOR_MODS) ?? false;
const mentionText = modMessage
- ? `${config.roleIds.moderators.map((id) => `<@&${id}>`)} - ${modMessage}`
+ ? `${config.roleIds.moderators.map((id) => `<@&${id}>`).join(' ')} - ${modMessage}`
: undefined;
- const channel = interaction.client.channels.cache.get(config.channelIds.repelLogs) as TextChannel;
+ const channel = interaction.client.channels.cache.get(
+ config.channelIds.repelLogs
+ ) as TextChannel;
const embed =
failedChannelsEmbed !== null
@@ -422,7 +459,8 @@ export const repelCommand = createSlashCommand({
name: RepelOptions.DM_USER,
required: false,
type: ApplicationCommandOptionType.Boolean,
- description: 'Whether to DM the user about the repel action (Defaults to true)',
+ description:
+ 'Whether to DM the user about the repel action (Defaults to true)',
},
],
},
@@ -443,7 +481,8 @@ export const repelCommand = createSlashCommand({
const repelRole = interaction.guild.roles.cache.get(config.roleIds.repel);
if (!repelRole) {
await interaction.editReply({
- content: '❌ Repel role is not configured correctly. Please contact an administrator.',
+ content:
+ '❌ Repel role is not configured correctly. Please contact an administrator.',
});
return;
}
@@ -479,12 +518,16 @@ export const repelCommand = createSlashCommand({
try {
const reason = interaction.options.getString(RepelOptions.REASON, true);
const lookBack = interaction.options.getInteger(RepelOptions.LOOK_BACK);
- const timeoutHours = interaction.options.getInteger(RepelOptions.TIMEOUT_DURATION);
+ const timeoutHours = interaction.options.getInteger(
+ RepelOptions.TIMEOUT_DURATION
+ );
const timeout = await handleTimeout({
target: target,
durationInMilliseconds:
- timeoutHours !== null ? timeoutHours * HOUR : DEFAULT_TIMEOUT_DURATION_MS,
+ timeoutHours !== null
+ ? timeoutHours * HOUR
+ : DEFAULT_TIMEOUT_DURATION_MS,
});
const channels = getTextChannels(interaction);
@@ -495,7 +538,8 @@ export const repelCommand = createSlashCommand({
lookBack: lookBack ?? DEFAULT_LOOK_BACK_MS,
});
- const shouldDMUser = interaction.options.getBoolean(RepelOptions.DM_USER) ?? true;
+ const shouldDMUser =
+ interaction.options.getBoolean(RepelOptions.DM_USER) ?? true;
let dmSent = false;
if (shouldDMUser) {
dmSent = await sendReasonToTarget({
@@ -506,7 +550,7 @@ export const repelCommand = createSlashCommand({
});
}
- logRepelAction({
+ void logRepelAction({
interaction,
member: commandUser,
target,
diff --git a/src/features/onboarding/component.ts b/src/features/onboarding/component.ts
index 4245246..af2cc5c 100644
--- a/src/features/onboarding/component.ts
+++ b/src/features/onboarding/component.ts
@@ -9,7 +9,8 @@ import {
// Exported at the bottom after all child components are added
const containerComponent = new ContainerBuilder();
-const actionRowComponent = new ActionRowBuilder();
+const actionRowComponent =
+ new ActionRowBuilder();
const buttonComponent = new ButtonBuilder()
.setCustomId('onboarding_add_role')
diff --git a/src/features/onboarding/index.ts b/src/features/onboarding/index.ts
index d810d16..5170693 100644
--- a/src/features/onboarding/index.ts
+++ b/src/features/onboarding/index.ts
@@ -27,8 +27,10 @@ export const onboardingCommand = createSlashCommand({
});
return;
}
- // @ts-expect-error: This command isn't used and shouldn't affect anything, onboarding channels are TBD
- const onboardingChannel = guild.channels.cache.get(config.onboarding.channelId);
+ const onboardingChannel = guild.channels.cache.get(
+ // @ts-expect-error: This command isn't used and shouldn't affect anything, onboarding channels are TBD
+ config.onboarding.channelId
+ );
if (!onboardingChannel || !onboardingChannel.isSendable()) {
await interaction.reply({
content:
@@ -48,7 +50,9 @@ export const onboardingCommand = createSlashCommand({
collector.on('collect', async (componentInteraction) => {
if (componentInteraction.customId === 'onboarding_add_role') {
try {
- const member = await guild.members.fetch(componentInteraction.user.id);
+ const member = await guild.members.fetch(
+ componentInteraction.user.id
+ );
await addRoleToUser(member, onboardingRole);
await componentInteraction.reply({
diff --git a/src/features/ping/index.ts b/src/features/ping/index.ts
index 8bc3576..58e7b8f 100644
--- a/src/features/ping/index.ts
+++ b/src/features/ping/index.ts
@@ -6,7 +6,10 @@ export const pingCommand = createSlashCommand({
description: 'Replies with Pong!',
},
execute: async (interaction) => {
- const sent = await interaction.reply({ content: 'pinging...', withResponse: true });
+ const sent = await interaction.reply({
+ content: 'pinging...',
+ withResponse: true,
+ });
const message = sent.resource?.message;
if (!message) {
await interaction.editReply('Failed to send ping message.');
@@ -14,6 +17,8 @@ export const pingCommand = createSlashCommand({
}
const roundTrip = message.createdTimestamp - interaction.createdTimestamp;
const apiLatency = interaction.client.ws.ping;
- await interaction.editReply(`latency: ${apiLatency}ms | Roundtrip: ${roundTrip}ms`);
+ await interaction.editReply(
+ `latency: ${apiLatency}ms | Roundtrip: ${roundTrip}ms`
+ );
},
});
diff --git a/src/features/public-guides/index.ts b/src/features/public-guides/index.ts
index 7b2b596..f56607c 100644
--- a/src/features/public-guides/index.ts
+++ b/src/features/public-guides/index.ts
@@ -6,7 +6,10 @@ import {
} from 'discord.js';
import { createSlashCommand } from '@/common/commands/create-commands.js';
import { config } from '@/env.js';
-import { initializeGuidesChannel, syncGuidesToChannel } from '@/util/post-guides.js';
+import {
+ initializeGuidesChannel,
+ syncGuidesToChannel,
+} from '@/util/post-guides.js';
export const publicGuidesCommand = createSlashCommand({
data: {
@@ -42,7 +45,9 @@ export const publicGuidesCommand = createSlashCommand({
} else {
await syncGuidesToChannel(interaction.client, channelId);
}
- await interaction.editReply('✅ Guides have been synchronized successfully.');
+ await interaction.editReply(
+ '✅ Guides have been synchronized successfully.'
+ );
} catch (error) {
console.error('Error synchronizing guides:', error);
await interaction.editReply(
diff --git a/src/features/ready/index.ts b/src/features/ready/index.ts
index f0ae255..a95c651 100644
--- a/src/features/ready/index.ts
+++ b/src/features/ready/index.ts
@@ -28,7 +28,9 @@ export const readyEvent = createEvent(
// Sync guides to channel
try {
- console.log(`🔄 Starting guide sync to channel ${config.channelIds.guides}...`);
+ console.log(
+ `🔄 Starting guide sync to channel ${config.channelIds.guides}...`
+ );
await syncGuidesToChannel(client, config.channelIds.guides);
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
diff --git a/src/features/showcase/create-showcase.ts b/src/features/showcase/create-showcase.ts
index bd61d8f..43182e9 100644
--- a/src/features/showcase/create-showcase.ts
+++ b/src/features/showcase/create-showcase.ts
@@ -26,12 +26,17 @@ import { deleteShowcase } from './delete-showcase.js';
import { editShowcaseInteraction } from './edit-showcase.js';
import { buildShowcaseModal, getShowcaseLogChannel } from './util.js';
-export const showModal = async (interaction: ButtonInteraction | ChatInputCommandInteraction) => {
+export const showModal = async (
+ interaction: ButtonInteraction | ChatInputCommandInteraction
+) => {
try {
- const channel = interaction.guild?.channels.cache.get(config.channelIds.showcase);
+ const channel = interaction.guild?.channels.cache.get(
+ config.channelIds.showcase
+ );
if (channel === undefined || channel.type !== ChannelType.GuildForum) {
await interaction.reply({
- content: 'Showcase channel is not properly configured. Please contact an administrator.',
+ content:
+ 'Showcase channel is not properly configured. Please contact an administrator.',
flags: MessageFlags.Ephemeral,
});
return;
@@ -47,7 +52,8 @@ export const showModal = async (interaction: ButtonInteraction | ChatInputComman
} catch (error) {
console.error('Error showing showcase modal:', error);
await interaction.reply({
- content: 'There was an error showing the showcase modal. Please try again later.',
+ content:
+ 'There was an error showing the showcase modal. Please try again later.',
flags: MessageFlags.Ephemeral,
});
}
@@ -74,14 +80,18 @@ const modalHandler: ModalSubmitInteraction = {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const projectName = interaction.fields.getTextInputValue('projectName');
const projectLink = interaction.fields.getTextInputValue('projectLink');
- const projectDescription = interaction.fields.getTextInputValue('projectDescription');
+ const projectDescription =
+ interaction.fields.getTextInputValue('projectDescription');
const projectTags = interaction.fields.getStringSelectValues('projectTags');
const projectMedia = interaction.fields.getUploadedFiles('projectMedia');
- const channel = interaction.guild?.channels.cache.get(config.channelIds.showcase);
+ const channel = interaction.guild?.channels.cache.get(
+ config.channelIds.showcase
+ );
if (channel === undefined || channel.type !== ChannelType.GuildForum) {
await interaction.editReply({
- content: 'Showcase channel is not properly configured. Please contact an administrator.',
+ content:
+ 'Showcase channel is not properly configured. Please contact an administrator.',
});
return;
}
@@ -142,7 +152,11 @@ const modalHandler: ModalSubmitInteraction = {
.setTitle('Showcase Created')
.addFields(
{ name: 'Project Name', value: projectName || '—', inline: false },
- { name: 'Author', value: `<@${interaction.user.id}>`, inline: true },
+ {
+ name: 'Author',
+ value: `<@${interaction.user.id}>`,
+ inline: true,
+ },
{ name: 'Thread', value: thread.url ?? '—', inline: true }
)
.setColor(Colors.Green)
@@ -163,7 +177,8 @@ const modalHandler: ModalSubmitInteraction = {
} catch (error) {
console.error('Error creating showcase thread:', error);
await interaction.editReply({
- content: 'There was an error showcasing your project. Please try again later.',
+ content:
+ 'There was an error showcasing your project. Please try again later.',
});
}
},
diff --git a/src/features/showcase/delete-showcase.ts b/src/features/showcase/delete-showcase.ts
index 3bd7b85..9377f99 100644
--- a/src/features/showcase/delete-showcase.ts
+++ b/src/features/showcase/delete-showcase.ts
@@ -19,7 +19,10 @@ export const deleteShowcase: ButtonSubmitInteraction = {
return;
}
- if (interactionUser.id !== ownerId && !isUserModerator(interaction.member, interaction)) {
+ if (
+ interactionUser.id !== ownerId &&
+ !isUserModerator(interaction.member, interaction)
+ ) {
await interaction.reply({
content: '❌ You do not have permission to delete this showcase.',
flags: MessageFlags.Ephemeral,
@@ -29,7 +32,9 @@ export const deleteShowcase: ButtonSubmitInteraction = {
try {
const forumPost =
- interaction.channel?.type === ChannelType.PublicThread ? interaction.channel : null;
+ interaction.channel?.type === ChannelType.PublicThread
+ ? interaction.channel
+ : null;
if (forumPost === null) {
await interaction.reply({
content: '❌ This command can only be used in a forum post.',
@@ -67,7 +72,10 @@ export const deleteShowcase: ButtonSubmitInteraction = {
`**Project Name:** ${projectName}\n**Deleted By:** <@${interactionUser.id}>`
)
.setColor(Colors.Red)
- .setAuthor({ name: interactionUser.tag, iconURL: interactionUser.displayAvatarURL() }),
+ .setAuthor({
+ name: interactionUser.tag,
+ iconURL: interactionUser.displayAvatarURL(),
+ }),
},
});
} catch (error) {
diff --git a/src/features/showcase/edit-showcase.ts b/src/features/showcase/edit-showcase.ts
index 20c9f7f..a9443a7 100644
--- a/src/features/showcase/edit-showcase.ts
+++ b/src/features/showcase/edit-showcase.ts
@@ -1,4 +1,10 @@
-import { ChannelType, Colors, EmbedBuilder, escapeCodeBlock, MessageFlags } from 'discord.js';
+import {
+ ChannelType,
+ Colors,
+ EmbedBuilder,
+ escapeCodeBlock,
+ MessageFlags,
+} from 'discord.js';
import type { ButtonSubmitInteraction } from '@/common/interactions/button-interaction.js';
import {
type ModalSubmitInteraction,
@@ -31,7 +37,10 @@ export const editShowcaseInteraction: ButtonSubmitInteraction = {
return;
}
- if (interactionUser.id !== ownerId && !isUserModerator(interaction.member, interaction)) {
+ if (
+ interactionUser.id !== ownerId &&
+ !isUserModerator(interaction.member, interaction)
+ ) {
await interaction.reply({
content: '❌ You do not have permission to edit this showcase.',
flags: MessageFlags.Ephemeral,
@@ -40,7 +49,9 @@ export const editShowcaseInteraction: ButtonSubmitInteraction = {
}
const forumPost =
- interaction.channel?.type === ChannelType.PublicThread ? interaction.channel : null;
+ interaction.channel?.type === ChannelType.PublicThread
+ ? interaction.channel
+ : null;
if (forumPost === null) {
await interaction.reply({
content: '❌ This command can only be used in a forum post.',
@@ -60,7 +71,9 @@ export const editShowcaseInteraction: ButtonSubmitInteraction = {
}
const parentChannel =
- forumPost.parent?.type === ChannelType.GuildForum ? forumPost.parent : null;
+ forumPost.parent?.type === ChannelType.GuildForum
+ ? forumPost.parent
+ : null;
if (parentChannel === null) {
await interaction.reply({
content: '❌ Could not find the parent forum channel.',
@@ -69,7 +82,9 @@ export const editShowcaseInteraction: ButtonSubmitInteraction = {
return;
}
- const { projectName, link, description } = parseShowcaseMessage(message.content);
+ const { projectName, link, description } = parseShowcaseMessage(
+ message.content
+ );
const appliedTags = forumPost.appliedTags;
const tags = parentChannel.availableTags;
@@ -108,7 +123,10 @@ const modalHandler: ModalSubmitInteraction = {
return;
}
- if (interactionUser.id !== ownerId && !isUserModerator(interaction.member, interaction)) {
+ if (
+ interactionUser.id !== ownerId &&
+ !isUserModerator(interaction.member, interaction)
+ ) {
await interaction.reply({
content: '❌ You do not have permission to edit this showcase.',
flags: MessageFlags.Ephemeral,
@@ -117,7 +135,9 @@ const modalHandler: ModalSubmitInteraction = {
}
const forumPost =
- interaction.channel?.type === ChannelType.PublicThread ? interaction.channel : null;
+ interaction.channel?.type === ChannelType.PublicThread
+ ? interaction.channel
+ : null;
if (forumPost === null) {
await interaction.reply({
content: '❌ This command can only be used in a forum post.',
@@ -142,7 +162,9 @@ const modalHandler: ModalSubmitInteraction = {
});
const threadParent =
- forumPost.parent?.type === ChannelType.GuildForum ? forumPost.parent : null;
+ forumPost.parent?.type === ChannelType.GuildForum
+ ? forumPost.parent
+ : null;
if (threadParent === null) {
await interaction.reply({
content: '❌ Could not find the parent forum channel.',
@@ -159,10 +181,14 @@ const modalHandler: ModalSubmitInteraction = {
const prevTagIds = forumPost.appliedTags ?? [];
const prevAttachmentsCount = getAttachmentsCount(message.attachments);
- const newProjectName = interaction.fields.getTextInputValue('projectName');
- const newProjectLink = interaction.fields.getTextInputValue('projectLink');
- const newProjectDescription = interaction.fields.getTextInputValue('projectDescription');
- const newProjectTags = interaction.fields.getStringSelectValues('projectTags');
+ const newProjectName =
+ interaction.fields.getTextInputValue('projectName');
+ const newProjectLink =
+ interaction.fields.getTextInputValue('projectLink');
+ const newProjectDescription =
+ interaction.fields.getTextInputValue('projectDescription');
+ const newProjectTags =
+ interaction.fields.getStringSelectValues('projectTags');
const projectMedia = interaction.fields.getUploadedFiles('projectMedia');
let newAttachmentsCount = prevAttachmentsCount;
@@ -174,10 +200,18 @@ const modalHandler: ModalSubmitInteraction = {
const changes: ShowcaseEditChange[] = [];
if (prevTitle !== newProjectName) {
- changes.push({ field: 'title', before: prevTitle, after: newProjectName });
+ changes.push({
+ field: 'title',
+ before: prevTitle,
+ after: newProjectName,
+ });
}
if (prevLink !== newProjectLink) {
- changes.push({ field: 'link', before: prevLink, after: newProjectLink });
+ changes.push({
+ field: 'link',
+ before: prevLink,
+ after: newProjectLink,
+ });
}
if (prevDescription !== newProjectDescription) {
changes.push({
@@ -196,8 +230,13 @@ const modalHandler: ModalSubmitInteraction = {
if (!tagsEqual()) {
changes.push({
field: 'tags',
- before: resolveTagNames(prevTagIds, threadParent.availableTags).join(', '),
- after: resolveTagNames(newProjectTags, threadParent.availableTags).join(', '),
+ before: resolveTagNames(prevTagIds, threadParent.availableTags).join(
+ ', '
+ ),
+ after: resolveTagNames(
+ newProjectTags,
+ threadParent.availableTags
+ ).join(', '),
});
}
if (newAttachmentsCount > 0) {
@@ -242,7 +281,10 @@ const modalHandler: ModalSubmitInteraction = {
acc.push(
wrapInDiffBlock(
clampText(
- toDiscordDiff(escapeCodeBlock(change.before), escapeCodeBlock(change.after)),
+ toDiscordDiff(
+ escapeCodeBlock(change.before),
+ escapeCodeBlock(change.after)
+ ),
3500
)
)
diff --git a/src/features/showcase/send-pinned-message.ts b/src/features/showcase/send-pinned-message.ts
index 7ac9d09..1097246 100644
--- a/src/features/showcase/send-pinned-message.ts
+++ b/src/features/showcase/send-pinned-message.ts
@@ -23,7 +23,9 @@ export const sendShowcasePinnedMessage = createSlashCommand({
},
execute: async (interaction) => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
- const showcaseChannel = interaction.guild?.channels.cache.get(config.channelIds.showcase);
+ const showcaseChannel = interaction.guild?.channels.cache.get(
+ config.channelIds.showcase
+ );
if (showcaseChannel === undefined || !showcaseChannel.isThreadOnly()) {
await interaction.editReply({
content: 'Showcase channel not found or is not a forum channel.',
@@ -51,12 +53,13 @@ export const sendShowcasePinnedMessage = createSlashCommand({
'Press the button below to create a new post and share your project with the community!',
].join('\n');
- const actionRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder()
- .setCustomId('create_showcase')
- .setLabel('Create Showcase Post')
- .setStyle(ButtonStyle.Primary)
- );
+ const actionRow =
+ new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId('create_showcase')
+ .setLabel('Create Showcase Post')
+ .setStyle(ButtonStyle.Primary)
+ );
if (pinnedThread) {
if (!pinnedThread.locked) {
@@ -83,8 +86,8 @@ export const sendShowcasePinnedMessage = createSlashCommand({
components: [actionRow],
},
});
- thread.pin();
- thread.setLocked(true);
+ void thread.pin();
+ void thread.setLocked(true);
await interaction.editReply({
content: 'Showcase pinned message sent successfully.',
diff --git a/src/features/showcase/types.ts b/src/features/showcase/types.ts
index ce9e529..af52a39 100644
--- a/src/features/showcase/types.ts
+++ b/src/features/showcase/types.ts
@@ -7,7 +7,12 @@ export type ShowcaseCreateLog = {
tagIds?: string[];
};
-export type ShowcaseEditChangeField = 'title' | 'link' | 'description' | 'tags' | 'attachments';
+export type ShowcaseEditChangeField =
+ | 'title'
+ | 'link'
+ | 'description'
+ | 'tags'
+ | 'attachments';
export type ShowcaseEditChange = {
field: ShowcaseEditChangeField;
diff --git a/src/features/showcase/util.ts b/src/features/showcase/util.ts
index 3f0511d..52d9d15 100644
--- a/src/features/showcase/util.ts
+++ b/src/features/showcase/util.ts
@@ -24,7 +24,9 @@ export const parseShowcaseMessage = (content: string): ShowcaseMessageData => {
const headerLines = header.split('\n');
const projectName = headerLines[0]?.replace(/^## Project Name:\s*/, '') ?? '';
- const authorLine = headerLines.find((line) => line.startsWith('**Author:** '));
+ const authorLine = headerLines.find((line) =>
+ line.startsWith('**Author:** ')
+ );
const authorId = authorLine?.match(/<@(\d+)>/)?.[1] ?? '';
const linkLine = headerLines.find((line) => line.startsWith('**Link:** '));
const link = linkLine?.replace(/^\*\*Link:\*\*\s*/, '') ?? '';
@@ -115,7 +117,9 @@ export const buildShowcaseModal = ({
),
new LabelBuilder()
.setLabel('Media')
- .setDescription('Attach images or videos showcasing your project (optional)')
+ .setDescription(
+ 'Attach images or videos showcasing your project (optional)'
+ )
.setFileUploadComponent(
new FileUploadBuilder()
.setCustomId('projectMedia')
@@ -126,7 +130,9 @@ export const buildShowcaseModal = ({
);
};
-export const getAttachmentsCount = (files: Collection): number => {
+export const getAttachmentsCount = (
+ files: Collection
+): number => {
return files.size;
};
diff --git a/src/features/tips/index.ts b/src/features/tips/index.ts
index 4cf8883..47bac17 100644
--- a/src/features/tips/index.ts
+++ b/src/features/tips/index.ts
@@ -76,7 +76,10 @@ const slashCommand = createSlashCommand({
},
});
- await interaction.reply({ content: 'Tip sent!', flags: MessageFlags.Ephemeral });
+ await interaction.reply({
+ content: 'Tip sent!',
+ flags: MessageFlags.Ephemeral,
+ });
},
});
@@ -91,7 +94,10 @@ const contextMenuCommands = Array.from(subjectChoices).map(([key, value]) =>
}
const message = interaction.targetMessage;
- await interaction.reply({ content: 'Fetching tip...', flags: MessageFlags.Ephemeral });
+ await interaction.reply({
+ content: 'Fetching tip...',
+ flags: MessageFlags.Ephemeral,
+ });
await message.reply({
content: value,
diff --git a/src/index.ts b/src/index.ts
index c174771..6d427af 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -17,7 +17,9 @@ const client = new Client({
GatewayIntentBits.GuildMessageTyping,
],
presence: {
- activities: [{ type: ActivityType.Custom, name: 'Helping you out in the community' }],
+ activities: [
+ { type: ActivityType.Custom, name: 'Helping you out in the community' },
+ ],
status: 'online',
},
});
diff --git a/src/sanity.test.ts b/src/sanity.test.ts
index 7130a62..a79bde3 100644
--- a/src/sanity.test.ts
+++ b/src/sanity.test.ts
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
-test('sanity: 1 + 1 equals 2', () => {
+void test('sanity: 1 + 1 equals 2', () => {
assert.equal(1 + 1, 2);
});
diff --git a/src/scripts/deploy-commands.ts b/src/scripts/deploy-commands.ts
index 1974633..a08e87f 100644
--- a/src/scripts/deploy-commands.ts
+++ b/src/scripts/deploy-commands.ts
@@ -1,4 +1,8 @@
-import { REST, type RESTPutAPIApplicationCommandsResult, Routes } from 'discord.js';
+import {
+ REST,
+ type RESTPutAPIApplicationCommandsResult,
+ Routes,
+} from 'discord.js';
import { commands } from '@/common/commands/index.js';
import { config } from '@/env.js';
export async function deployCommands() {
@@ -6,9 +10,17 @@ export async function deployCommands() {
const rest = new REST({ version: '10' }).setToken(config.discord.token);
+ const random = Math.floor(Math.random() * 1000);
+ if (random < 10) {
+ console.log(`ds ${random}`);
+ }
+
try {
const result = (await rest.put(
- Routes.applicationGuildCommands(config.discord.clientId, config.discord.serverId),
+ Routes.applicationGuildCommands(
+ config.discord.clientId,
+ config.discord.serverId
+ ),
{
body: commandData,
}
@@ -23,5 +35,5 @@ export async function deployCommands() {
// If run directly with `node deploy.ts`
if (import.meta.url === `file://${process.argv[1]}`) {
- deployCommands();
+ void deployCommands();
}
diff --git a/src/scripts/sync-guides.ts b/src/scripts/sync-guides.ts
index e3108d4..9674326 100644
--- a/src/scripts/sync-guides.ts
+++ b/src/scripts/sync-guides.ts
@@ -7,7 +7,10 @@
import { Client, GatewayIntentBits } from 'discord.js';
import { config } from '@/env.js';
-import { initializeGuidesChannel, syncGuidesToChannel } from '@/util/post-guides.js';
+import {
+ initializeGuidesChannel,
+ syncGuidesToChannel,
+} from '@/util/post-guides.js';
async function main() {
const args = process.argv.slice(2);
@@ -30,7 +33,9 @@ async function main() {
console.log(`✅ Logged in as ${client.user?.tag}`);
if (shouldInitialize) {
- console.log('🚀 Initializing guides channel (will post all guides fresh)...');
+ console.log(
+ '🚀 Initializing guides channel (will post all guides fresh)...'
+ );
await initializeGuidesChannel(client, config.channelIds.guides);
} else {
console.log('🔄 Synchronizing guides...');
diff --git a/src/util/add-role-to-user.ts b/src/util/add-role-to-user.ts
index e4cdf02..6bdbe1f 100644
--- a/src/util/add-role-to-user.ts
+++ b/src/util/add-role-to-user.ts
@@ -1,6 +1,9 @@
import type { GuildMember, Role } from 'discord.js';
-export async function addRoleToUser(member: GuildMember, role: Role): Promise {
+export async function addRoleToUser(
+ member: GuildMember,
+ role: Role
+): Promise {
const hasRole = member.roles.cache.has(role.id);
if (!hasRole) {
try {
diff --git a/src/util/advent-scheduler.test.ts b/src/util/advent-scheduler.test.ts
index 34ae1c1..ae201cc 100644
--- a/src/util/advent-scheduler.test.ts
+++ b/src/util/advent-scheduler.test.ts
@@ -9,17 +9,20 @@ const { loadTracker, saveTracker } = await import('./advent-scheduler.js');
async function cleanupTestTracker() {
try {
await fs.unlink(config.adventOfCodeTrackerPath);
- } catch (_error) {
+ } catch {
// File might not exist, that's fine
}
}
-test('advent scheduler: tracker file operations', async (t) => {
- await t.test('should create empty tracker if file does not exist', async () => {
- await cleanupTestTracker();
- const tracker = await loadTracker();
- assert.deepEqual(tracker, {});
- });
+void test('advent scheduler: tracker file operations', async (t) => {
+ await t.test(
+ 'should create empty tracker if file does not exist',
+ async () => {
+ await cleanupTestTracker();
+ const tracker = await loadTracker();
+ assert.deepEqual(tracker, {});
+ }
+ );
await t.test('should save and load tracker data correctly', async () => {
const testData = {
diff --git a/src/util/advent-scheduler.ts b/src/util/advent-scheduler.ts
index a73ffc3..bbbbd82 100644
--- a/src/util/advent-scheduler.ts
+++ b/src/util/advent-scheduler.ts
@@ -59,7 +59,9 @@ async function createAdventPost(
}
if (channel.type !== ChannelType.GuildForum) {
- console.error(`❌ Advent of Code channel is not a forum channel. Type: ${channel.type}`);
+ console.error(
+ `❌ Advent of Code channel is not a forum channel. Type: ${channel.type}`
+ );
return false;
}
@@ -77,12 +79,18 @@ async function createAdventPost(
console.log(`✅ Created Advent of Code post: ${title}`);
return true;
} catch (error) {
- console.error(`❌ Failed to create Advent of Code post for day ${day}:`, error);
+ console.error(
+ `❌ Failed to create Advent of Code post for day ${day}:`,
+ error
+ );
return false;
}
}
-async function checkAndCreateTodaysPost(client: Client, channelId: string): Promise {
+async function checkAndCreateTodaysPost(
+ client: Client,
+ channelId: string
+): Promise {
const now = new Date();
const month = now.getUTCMonth(); // 0-indexed, so December is 11
const day = now.getUTCDate();
@@ -113,11 +121,17 @@ async function checkAndCreateTodaysPost(client: Client, channelId: string): Prom
* Initialize the Advent of Code scheduler
* Runs every day at midnight UTC-5 and checks if we should create a post
*/
-export function initializeAdventScheduler(client: Client, channelId: string): void {
+export function initializeAdventScheduler(
+ client: Client,
+ channelId: string
+): void {
console.log('🎄 Initializing Advent of Code scheduler...');
checkAndCreateTodaysPost(client, channelId).catch((error) => {
- console.error('❌ Error checking for Advent of Code post on startup:', error);
+ console.error(
+ '❌ Error checking for Advent of Code post on startup:',
+ error
+ );
});
// Schedule to run every day at midnight UTC-5
@@ -129,5 +143,7 @@ export function initializeAdventScheduler(client: Client, channelId: string): vo
});
});
- console.log('✅ Advent of Code scheduler initialized (runs daily at midnight UTC)');
+ console.log(
+ '✅ Advent of Code scheduler initialized (runs daily at midnight UTC)'
+ );
}
diff --git a/src/util/build-command-string.ts b/src/util/build-command-string.ts
index 89a8469..501d97c 100644
--- a/src/util/build-command-string.ts
+++ b/src/util/build-command-string.ts
@@ -1,6 +1,8 @@
import type { ChatInputCommandInteraction } from 'discord.js';
-export const buildCommandString = (interaction: ChatInputCommandInteraction): string => {
+export const buildCommandString = (
+ interaction: ChatInputCommandInteraction
+): string => {
const commandName = interaction.commandName;
return `/${commandName} ${interaction.options.data.map((option) => `${option.name}:${option.value}`).join(' ')}`;
};
diff --git a/src/util/cache.ts b/src/util/cache.ts
index 24e7366..2ea348a 100644
--- a/src/util/cache.ts
+++ b/src/util/cache.ts
@@ -4,7 +4,10 @@ import { getPublicChannels } from './channel.js';
const PER_CHANNEL_CACHE_LIMIT = 100;
export const cachedChannelsMap = new Set();
-export const fetchAndCachePublicChannelsMessages = async (guild: Guild, force = false) => {
+export const fetchAndCachePublicChannelsMessages = async (
+ guild: Guild,
+ force = false
+) => {
let cachedChannels = 0;
const failedChannels: string[] = [];
@@ -14,7 +17,9 @@ export const fetchAndCachePublicChannelsMessages = async (guild: Guild, force =
channels.map(async (channel) => {
if (force || !cachedChannelsMap.has(channel.id)) {
try {
- const messages = await channel.messages.fetch({ limit: PER_CHANNEL_CACHE_LIMIT });
+ const messages = await channel.messages.fetch({
+ limit: PER_CHANNEL_CACHE_LIMIT,
+ });
console.log(
`Fetched and cached ${messages.size} messages from channel ${channel.name} (${channel.id})`
);
diff --git a/src/util/channel-logging.ts b/src/util/channel-logging.ts
index 892c532..e6a944a 100644
--- a/src/util/channel-logging.ts
+++ b/src/util/channel-logging.ts
@@ -30,7 +30,10 @@ export type LoggerOptions = {
silent?: boolean;
};
-const sendMessage = async (channel: SendableChannels, content: LogContent): Promise => {
+const sendMessage = async (
+ channel: SendableChannels,
+ content: LogContent
+): Promise => {
try {
let options: MessageCreateOptions;
switch (content.type) {
@@ -39,7 +42,9 @@ const sendMessage = async (channel: SendableChannels, content: LogContent): Prom
break;
case 'embed':
options = {
- embeds: Array.isArray(content.embed) ? content.embed : [content.embed],
+ embeds: Array.isArray(content.embed)
+ ? content.embed
+ : [content.embed],
content: content.content,
};
break;
@@ -58,7 +63,9 @@ const sendMessage = async (channel: SendableChannels, content: LogContent): Prom
}
};
-export const logToChannel = async (options: LoggerOptions): Promise => {
+export const logToChannel = async (
+ options: LoggerOptions
+): Promise => {
try {
const { channel, content } = options;
if (!channel || !channel.isSendable()) {
@@ -81,7 +88,9 @@ export const logToChannel = async (options: LoggerOptions): Promise =>
return sendMessage(fallbackChannel, options.content);
} else {
if (!options.silent) {
- console.error('Fallback channel is not a text-based channel or could not be fetched');
+ console.error(
+ 'Fallback channel is not a text-based channel or could not be fetched'
+ );
}
return false;
}
diff --git a/src/util/channel.ts b/src/util/channel.ts
index a55617a..233dfa1 100644
--- a/src/util/channel.ts
+++ b/src/util/channel.ts
@@ -1,4 +1,9 @@
-import { ChannelType, type Guild, PermissionFlagsBits, type TextChannel } from 'discord.js';
+import {
+ ChannelType,
+ type Guild,
+ PermissionFlagsBits,
+ type TextChannel,
+} from 'discord.js';
export const getPublicChannels = (guild: Guild) => {
return guild.channels.cache.filter(
diff --git a/src/util/fuzzy-search.ts b/src/util/fuzzy-search.ts
index 1ecffb1..4f3e20b 100644
--- a/src/util/fuzzy-search.ts
+++ b/src/util/fuzzy-search.ts
@@ -1,5 +1,7 @@
export const levenshtein = (a: string, b: string) => {
- const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
+ const dp = Array.from({ length: a.length + 1 }, () =>
+ Array(b.length + 1).fill(0)
+ );
for (let i = 0; i <= a.length; i++) {
dp[i][0] = i;
@@ -80,7 +82,10 @@ export function fuzzySearch({
// Apply a large, non-normalized boost for exact or prefix matches
if (text === query) {
titleMatchScore = EXACT_MATCH_BOOST; // Query "array" matches title "Array"
- } else if (titleMatchScore < EXACT_MATCH_BOOST && text.startsWith(query)) {
+ } else if (
+ titleMatchScore < EXACT_MATCH_BOOST &&
+ text.startsWith(query)
+ ) {
// If not a perfect match, check for prefix match
titleMatchScore = Math.max(titleMatchScore, PREFIX_BOOST);
} else if (titleMatchScore === 0 && text.includes(` ${query} `)) {
diff --git a/src/util/member.ts b/src/util/member.ts
index 2648104..09463bf 100644
--- a/src/util/member.ts
+++ b/src/util/member.ts
@@ -7,11 +7,17 @@ import {
} from 'discord.js';
import { config } from '@/env.js';
-export const hasAllRoles = (member: GuildMember, ...roles: string[]): boolean => {
+export const hasAllRoles = (
+ member: GuildMember,
+ ...roles: string[]
+): boolean => {
return roles.every((roleId) => member.roles.cache.has(roleId));
};
-export const hasAnyRole = (member: GuildMember, ...roles: string[]): boolean => {
+export const hasAnyRole = (
+ member: GuildMember,
+ ...roles: string[]
+): boolean => {
return roles.some((roleId) => member.roles.cache.has(roleId));
};
@@ -21,7 +27,10 @@ export const isUserInServer = (
return target instanceof GuildMember;
};
-export const isUserModerator = (member: GuildMember, interaction: BaseInteraction): boolean => {
+export const isUserModerator = (
+ member: GuildMember,
+ interaction: BaseInteraction
+): boolean => {
return (
hasAnyRole(member, ...config.roleIds.moderators) ||
member.permissions.has(
diff --git a/src/util/post-guides.ts b/src/util/post-guides.ts
index 6fc51e4..f833bd1 100644
--- a/src/util/post-guides.ts
+++ b/src/util/post-guides.ts
@@ -1,7 +1,12 @@
import { createHash } from 'node:crypto';
import { readdir, readFile, writeFile } from 'node:fs/promises';
import path, { join } from 'node:path';
-import { ChannelType, type Client, EmbedBuilder, type TextChannel } from 'discord.js';
+import {
+ ChannelType,
+ type Client,
+ EmbedBuilder,
+ type TextChannel,
+} from 'discord.js';
import { config } from '@/env.js';
import { parseMarkdown } from './markdown.js';
@@ -14,8 +19,11 @@ export type GuideInfo = {
frontmatter: Record;
};
-const guidesColors = [0xff5733, 0x33ff57, 0x3357ff, 0xff33a8, 0xa833ff, 0x33fff5];
-const getRandomColor = () => guidesColors[Math.floor(Math.random() * guidesColors.length)];
+const guidesColors = [
+ 0xff5733, 0x33ff57, 0x3357ff, 0xff33a8, 0xa833ff, 0x33fff5,
+];
+const getRandomColor = () =>
+ guidesColors[Math.floor(Math.random() * guidesColors.length)];
const createGuideEmbed = (guide: GuideInfo) =>
new EmbedBuilder()
.setTitle(guide.name)
@@ -34,7 +42,8 @@ export type GuideTracker = {
const GUIDES_DIR = path.join(process.cwd(), 'assets/guides');
// const TRACKER_FILE = config.guidesTrackerPath ?? 'guides-tracker.json';
-const TRACKER_FILE = config.guidesTrackerPath ?? path.join(process.cwd(), 'guides-tracker.json');
+const TRACKER_FILE =
+ config.guidesTrackerPath ?? path.join(process.cwd(), 'guides-tracker.json');
const calculateHash = (content: string): string => {
return createHash('sha256').update(content, 'utf8').digest('hex');
@@ -65,7 +74,8 @@ const scanGuideFiles = async (): Promise => {
const filePath = join(GUIDES_DIR, filename);
const content = await readFile(filePath, 'utf8');
- const { frontmatter, content: markdownContent } = await parseMarkdown(content);
+ const { frontmatter, content: markdownContent } =
+ await parseMarkdown(content);
const hash = calculateHash(content);
const name = (frontmatter.name as string) || filename.replace('.md', '');
@@ -82,7 +92,10 @@ const scanGuideFiles = async (): Promise => {
return guides;
};
-const postGuideToChannel = async (channel: TextChannel, guide: GuideInfo): Promise => {
+const postGuideToChannel = async (
+ channel: TextChannel,
+ guide: GuideInfo
+): Promise => {
const message = await channel.send({
embeds: [createGuideEmbed(guide)],
});
@@ -104,7 +117,10 @@ const editGuideMessage = async (
console.log(`📝 Updated guide "${guide.name}" (${guide.filename})`);
} catch (error) {
- console.error(`Failed to edit message ${messageId} for guide "${guide.name}":`, error);
+ console.error(
+ `Failed to edit message ${messageId} for guide "${guide.name}":`,
+ error
+ );
throw error;
}
};
@@ -120,16 +136,26 @@ const deleteGuideMessage = async (
console.log(`🗑️ Deleted guide "${guideName}"`);
} catch (error) {
- console.error(`Failed to delete message ${messageId} for guide "${guideName}":`, error);
+ console.error(
+ `Failed to delete message ${messageId} for guide "${guideName}":`,
+ error
+ );
}
};
-export const syncGuidesToChannel = async (client: Client, channelId: string): Promise => {
+export const syncGuidesToChannel = async (
+ client: Client,
+ channelId: string
+): Promise => {
console.log('🔄 Starting guide synchronization...');
try {
const channel = await client.channels.fetch(channelId);
- if (!channel || !channel.isTextBased() || channel.type !== ChannelType.GuildText) {
+ if (
+ !channel ||
+ !channel.isTextBased() ||
+ channel.type !== ChannelType.GuildText
+ ) {
throw new Error(`Channel ${channelId} is not a valid text channel`);
}
// Load current state
@@ -137,16 +163,24 @@ export const syncGuidesToChannel = async (client: Client, channelId: string): Pr
const currentGuides = await scanGuideFiles();
// Create maps for easier lookup
- const currentGuideMap = new Map(currentGuides.map((guide) => [guide.filename, guide]));
+ const currentGuideMap = new Map(
+ currentGuides.map((guide) => [guide.filename, guide])
+ );
const trackedFiles = new Set(Object.keys(tracker));
const currentFiles = new Set(currentGuides.map((guide) => guide.filename));
// Find changes
- const newFiles = [...currentFiles].filter((file) => !trackedFiles.has(file));
- const deletedFiles = [...trackedFiles].filter((file) => !currentFiles.has(file));
+ const newFiles = [...currentFiles].filter(
+ (file) => !trackedFiles.has(file)
+ );
+ const deletedFiles = [...trackedFiles].filter(
+ (file) => !currentFiles.has(file)
+ );
const modifiedFiles = [...currentFiles].filter((file) => {
const guide = currentGuideMap.get(file);
- return guide && trackedFiles.has(file) && tracker[file].hash !== guide.hash;
+ return (
+ guide && trackedFiles.has(file) && tracker[file].hash !== guide.hash
+ );
});
console.log(
@@ -191,11 +225,14 @@ export const syncGuidesToChannel = async (client: Client, channelId: string): Pr
await saveTracker(tracker);
- const totalChanges = newFiles.length + modifiedFiles.length + deletedFiles.length;
+ const totalChanges =
+ newFiles.length + modifiedFiles.length + deletedFiles.length;
if (totalChanges === 0) {
console.log('✨ All guides are up to date!');
} else {
- console.log(`✅ Guide synchronization complete! Made ${totalChanges} changes.`);
+ console.log(
+ `✅ Guide synchronization complete! Made ${totalChanges} changes.`
+ );
}
} catch (error) {
console.error('❌ Guide synchronization failed:', error);
@@ -203,7 +240,10 @@ export const syncGuidesToChannel = async (client: Client, channelId: string): Pr
}
};
-export const initializeGuidesChannel = async (client: Client, channelId: string): Promise => {
+export const initializeGuidesChannel = async (
+ client: Client,
+ channelId: string
+): Promise => {
console.log('🚀 Initializing guides channel...');
// Clear existing tracker for fresh start
diff --git a/src/util/server-guard.ts b/src/util/server-guard.ts
index 20acdc4..6ccd6ce 100644
--- a/src/util/server-guard.ts
+++ b/src/util/server-guard.ts
@@ -14,7 +14,9 @@ export function isAllowedServer(guildId: string): boolean {
*/
export async function leaveIfNotAllowedServer(guild: Guild): Promise {
if (!isAllowedServer(guild.id)) {
- console.log(`⚠️ Bot added to unauthorized server: ${guild.name} (${guild.id})`);
+ console.log(
+ `⚠️ Bot added to unauthorized server: ${guild.name} (${guild.id})`
+ );
console.log(`🚪 Leaving server...`);
await guild.leave();
console.log(`✅ Left unauthorized server: ${guild.name}`);
diff --git a/tsconfig.json b/tsconfig.json
index b7432b3..3a07130 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,8 @@
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "esnext",
- "outDir": "dist",
+ "rootDir": ".",
+ "outDir": "./dist",
"skipLibCheck": true,
"types": ["node"],
"strict": true,