Initial Commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.env.keys
|
||||||
|
data
|
||||||
|
db
|
||||||
|
coverage
|
||||||
179
.kilocode/rules/memory-bank-instructions.md
Normal file
179
.kilocode/rules/memory-bank-instructions.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Memory Bank
|
||||||
|
|
||||||
|
I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
|
||||||
|
|
||||||
|
When I start a task, I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization.
|
||||||
|
|
||||||
|
## Memory Bank Structure
|
||||||
|
|
||||||
|
The Memory Bank consists of core files and optional context files, all in Markdown format.
|
||||||
|
|
||||||
|
### Core Files (Required)
|
||||||
|
|
||||||
|
1. `brief.md`
|
||||||
|
This file is created and maintained manually by the developer. Don't edit this file directly but suggest to user to update it if it can be improved.
|
||||||
|
- Foundation document that shapes all other files
|
||||||
|
- Created at project start if it doesn't exist
|
||||||
|
- Defines core requirements and goals
|
||||||
|
- Source of truth for project scope
|
||||||
|
|
||||||
|
2. `product.md`
|
||||||
|
- Why this project exists
|
||||||
|
- Problems it solves
|
||||||
|
- How it should work
|
||||||
|
- User experience goals
|
||||||
|
|
||||||
|
3. `context.md`
|
||||||
|
This file should be short and factual, not creative or speculative.
|
||||||
|
- Current work focus
|
||||||
|
- Recent changes
|
||||||
|
- Next steps
|
||||||
|
|
||||||
|
4. `architecture.md`
|
||||||
|
- System architecture
|
||||||
|
- Source Code paths
|
||||||
|
- Key technical decisions
|
||||||
|
- Design patterns in use
|
||||||
|
- Component relationships
|
||||||
|
- Critical implementation paths
|
||||||
|
|
||||||
|
5. `tech.md`
|
||||||
|
- Technologies used
|
||||||
|
- Development setup
|
||||||
|
- Technical constraints
|
||||||
|
- Dependencies
|
||||||
|
- Tool usage patterns
|
||||||
|
|
||||||
|
### Additional Files
|
||||||
|
|
||||||
|
Create additional files/folders within memory-bank/ when they help organize:
|
||||||
|
|
||||||
|
- `tasks.md` - Documentation of repetitive tasks and their workflows
|
||||||
|
- Complex feature documentation
|
||||||
|
- Integration specifications
|
||||||
|
- API documentation
|
||||||
|
- Testing strategies
|
||||||
|
- Deployment procedures
|
||||||
|
|
||||||
|
## Core workflows
|
||||||
|
|
||||||
|
### Memory Bank Initialization
|
||||||
|
|
||||||
|
The initialization step is CRITICALLY IMPORTANT and must be done with extreme thoroughness as it defines all future effectiveness of the Memory Bank. This is the foundation upon which all future interactions will be built.
|
||||||
|
|
||||||
|
When user requests initialization of the memory bank (command `initialize memory bank`), I'll perform an exhaustive analysis of the project, including:
|
||||||
|
|
||||||
|
- All source code files and their relationships
|
||||||
|
- Configuration files and build system setup
|
||||||
|
- Project structure and organization patterns
|
||||||
|
- Documentation and comments
|
||||||
|
- Dependencies and external integrations
|
||||||
|
- Testing frameworks and patterns
|
||||||
|
|
||||||
|
I must be extremely thorough during initialization, spending extra time and effort to build a comprehensive understanding of the project. A high-quality initialization will dramatically improve all future interactions, while a rushed or incomplete initialization will permanently limit my effectiveness.
|
||||||
|
|
||||||
|
After initialization, I will ask the user to read through the memory bank files and verify product description, used technologies and other information. I should provide a summary of what I've understood about the project to help the user verify the accuracy of the memory bank files. I should encourage the user to correct any misunderstandings or add missing information, as this will significantly improve future interactions.
|
||||||
|
|
||||||
|
### Memory Bank Update
|
||||||
|
|
||||||
|
Memory Bank updates occur when:
|
||||||
|
|
||||||
|
1. Discovering new project patterns
|
||||||
|
2. After implementing significant changes
|
||||||
|
3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files)
|
||||||
|
4. When context needs clarification
|
||||||
|
|
||||||
|
If I notice significant changes that should be preserved but the user hasn't explicitly requested an update, I should suggest: "Would you like me to update the memory bank to reflect these changes?"
|
||||||
|
|
||||||
|
To execute Memory Bank update, I will:
|
||||||
|
|
||||||
|
1. Review ALL project files
|
||||||
|
2. Document current state
|
||||||
|
3. Document Insights & Patterns
|
||||||
|
4. If requested with additional context (e.g., "update memory bank using information from @/Makefile"), focus special attention on that source
|
||||||
|
|
||||||
|
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on context.md as it tracks current state.
|
||||||
|
|
||||||
|
### Add Task
|
||||||
|
|
||||||
|
When user completes a repetitive task (like adding support for a new model version) and wants to document it for future reference, they can request: **add task** or **store this as a task**.
|
||||||
|
|
||||||
|
This workflow is designed for repetitive tasks that follow similar patterns and require editing the same files. Examples include:
|
||||||
|
|
||||||
|
- Adding support for new AI model versions
|
||||||
|
- Implementing new API endpoints following established patterns
|
||||||
|
- Adding new features that follow existing architecture
|
||||||
|
|
||||||
|
Tasks are stored in the file `tasks.md` in the memory bank folder. The file is optional and can be empty. The file can store many tasks.
|
||||||
|
|
||||||
|
To execute Add Task workflow:
|
||||||
|
|
||||||
|
1. Create or update `tasks.md` in the memory bank folder
|
||||||
|
2. Document the task with:
|
||||||
|
- Task name and description
|
||||||
|
- Files that need to be modified
|
||||||
|
- Step-by-step workflow followed
|
||||||
|
- Important considerations or gotchas
|
||||||
|
- Example of the completed implementation
|
||||||
|
3. Include any context that was discovered during task execution but wasn't previously documented
|
||||||
|
|
||||||
|
Example task entry:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Add New Model Support
|
||||||
|
|
||||||
|
**Last performed:** [date]
|
||||||
|
**Files to modify:**
|
||||||
|
|
||||||
|
- `/providers/gemini.md` - Add model to documentation
|
||||||
|
- `/src/providers/gemini-config.ts` - Add model configuration
|
||||||
|
- `/src/constants/models.ts` - Add to model list
|
||||||
|
- `/tests/providers/gemini.test.ts` - Add test cases
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Add model configuration with proper token limits
|
||||||
|
2. Update documentation with model capabilities
|
||||||
|
3. Add to constants file for UI display
|
||||||
|
4. Write tests for new model configuration
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
|
||||||
|
- Check Google's documentation for exact token limits
|
||||||
|
- Ensure backward compatibility with existing configurations
|
||||||
|
- Test with actual API calls before committing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regular Task Execution
|
||||||
|
|
||||||
|
In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional.
|
||||||
|
|
||||||
|
The memory bank files are located in `.kilocode/rules/memory-bank` folder. If the folder doesn't exist or is empty, I will warn user about potential issues with the memory bank. I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. I should briefly summarize my understanding of the project to confirm alignment with the user's expectations, like:
|
||||||
|
|
||||||
|
"[Memory Bank: Active] I understand we're building a React inventory system with barcode scanning. Currently implementing the scanner component that needs to work with the backend API."
|
||||||
|
|
||||||
|
When starting a task that matches a documented task in `tasks.md`, I should mention this and follow the documented workflow to ensure no steps are missed.
|
||||||
|
|
||||||
|
If the task was repetitive and might be needed again, I should suggest: "Would you like me to add this task to the memory bank for future reference?"
|
||||||
|
|
||||||
|
In the end of the task, when it seems to be completed, I will update `context.md` accordingly. If the change seems significant, I will suggest to the user: "Would you like me to update memory bank to reflect these changes?" I will not suggest updates for minor changes.
|
||||||
|
|
||||||
|
## Context Window Management
|
||||||
|
|
||||||
|
When the context window fills up during an extended session:
|
||||||
|
|
||||||
|
1. I should suggest updating the memory bank to preserve the current state
|
||||||
|
2. Recommend starting a fresh conversation/task
|
||||||
|
3. In the new conversation, I will automatically load the memory bank files to maintain continuity
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
Memory Bank is built on Kilo Code's Custom Rules feature, with files stored as standard markdown documents that both the user and I can access.
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
||||||
|
|
||||||
|
If I detect inconsistencies between memory bank files, I should prioritize brief.md and note any discrepancies to the user.
|
||||||
|
|
||||||
|
IMPORTANT: I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
|
||||||
213
.kilocode/rules/memory-bank/architecture.md
Normal file
213
.kilocode/rules/memory-bank/architecture.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# Architecture Overview - Star Kitten
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
|
||||||
|
Star Kitten follows a modular monorepo architecture with four main packages and two applications:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Applications"
|
||||||
|
EB[eve-bot<br/>Discord Bot]
|
||||||
|
EW[eve-web<br/>Brisa Web App]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Core Packages"
|
||||||
|
D[@star-kitten/discord<br/>Discord Framework]
|
||||||
|
E[@star-kitten/eve<br/>EVE Online Integration]
|
||||||
|
U[@star-kitten/util<br/>Shared Utilities]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "External Services"
|
||||||
|
DS[Discord API]
|
||||||
|
ESI[EVE ESI API]
|
||||||
|
JA[Janice API]
|
||||||
|
ET[EveTycoon API]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Data Layer"
|
||||||
|
DB[(SQLite Database)]
|
||||||
|
RD[(Reference Data<br/>JSON Files)]
|
||||||
|
end
|
||||||
|
|
||||||
|
EB --> D
|
||||||
|
EB --> E
|
||||||
|
EW --> E
|
||||||
|
EW --> U
|
||||||
|
D --> DS
|
||||||
|
E --> ESI
|
||||||
|
E --> JA
|
||||||
|
E --> ET
|
||||||
|
E --> DB
|
||||||
|
E --> RD
|
||||||
|
U --> DB
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source Code Paths
|
||||||
|
|
||||||
|
### Core Packages
|
||||||
|
|
||||||
|
#### [@star-kitten/discord](packages/discord/)
|
||||||
|
|
||||||
|
- **Entry Point**: [`packages/discord/src/index.ts`](packages/discord/src/index.ts)
|
||||||
|
- **Command System**: [`packages/discord/src/commands/`](packages/discord/src/commands/)
|
||||||
|
- [`import-commands.ts`](packages/discord/src/commands/import-commands.ts) - Auto-discovery of command files
|
||||||
|
- [`handle-commands.ts`](packages/discord/src/commands/handle-commands.ts) - Command execution handler
|
||||||
|
- **Localization**: [`packages/discord/src/locales.ts`](packages/discord/src/locales.ts)
|
||||||
|
|
||||||
|
#### [@star-kitten/eve](packages/eve/)
|
||||||
|
|
||||||
|
- **Entry Point**: [`packages/eve/src/index.ts`](packages/eve/src/index.ts)
|
||||||
|
- **Database Layer**: [`packages/eve/src/db/`](packages/eve/src/db/)
|
||||||
|
- [`schema.ts`](packages/eve/src/db/schema.ts) - Drizzle ORM schema definitions
|
||||||
|
- [`index.ts`](packages/eve/src/db/index.ts) - Database connection and exports
|
||||||
|
- [`models/`](packages/eve/src/db/models/) - Character and user model helpers
|
||||||
|
- **ESI Integration**: [`packages/eve/src/esi/`](packages/eve/src/esi/)
|
||||||
|
- [`auth.ts`](packages/eve/src/esi/auth.ts) - EVE SSO authentication flow
|
||||||
|
- [`scopes.ts`](packages/eve/src/esi/scopes.ts) - ESI scope definitions and token validation
|
||||||
|
- [`character.ts`](packages/eve/src/esi/character.ts) - Character API endpoints
|
||||||
|
- [`fetch.ts`](packages/eve/src/esi/fetch.ts) - ESI API client wrapper
|
||||||
|
- **Data Models**: [`packages/eve/src/models/`](packages/eve/src/models/)
|
||||||
|
- [`type.ts`](packages/eve/src/models/type.ts) - EVE item type definitions and utilities
|
||||||
|
- [`skill.ts`](packages/eve/src/models/skill.ts) - Skill-related models
|
||||||
|
- **Third-party APIs**: [`packages/eve/src/third-party/`](packages/eve/src/third-party/)
|
||||||
|
- [`janice.ts`](packages/eve/src/third-party/janice.ts) - Market appraisal integration
|
||||||
|
- [`evetycoon.ts`](packages/eve/src/third-party/evetycoon.ts) - Market data integration
|
||||||
|
- **Utilities**: [`packages/eve/src/utils/`](packages/eve/src/utils/)
|
||||||
|
- [`markdown.ts`](packages/eve/src/utils/markdown.ts) - EVE markup to Discord formatting
|
||||||
|
|
||||||
|
#### [@star-kitten/util](packages/util/)
|
||||||
|
|
||||||
|
- **Entry Point**: [`packages/util/src/`](packages/util/src/) (no index.ts, exports individual modules)
|
||||||
|
- **Core Utilities**:
|
||||||
|
- [`text.ts`](packages/util/src/text.ts) - Text processing and formatting
|
||||||
|
- [`jsonQuery.ts`](packages/util/src/jsonQuery.ts) - Streaming JSON querying for large files
|
||||||
|
- [`time.ts`](packages/util/src/time.ts) - Time utilities
|
||||||
|
- [`logger.ts`](packages/util/src/logger.ts) - Logging utilities
|
||||||
|
- **Scheduler System**: [`packages/util/src/scheduler/`](packages/util/src/scheduler/)
|
||||||
|
- [`scheduler.service.ts`](packages/util/src/scheduler/scheduler.service.ts) - Job scheduling service
|
||||||
|
- [`queue.ts`](packages/util/src/scheduler/queue.ts) - Job queue implementation
|
||||||
|
- [`workers/`](packages/util/src/scheduler/workers/) - Background worker implementations
|
||||||
|
|
||||||
|
### Applications
|
||||||
|
|
||||||
|
#### [eve-bot](packages/eve-bot/)
|
||||||
|
|
||||||
|
- **Entry Point**: [`packages/eve-bot/src/main.ts`](packages/eve-bot/src/main.ts)
|
||||||
|
- **Commands**: [`packages/eve-bot/src/commands/`](packages/eve-bot/src/commands/)
|
||||||
|
- [`appraise.command.ts`](packages/eve-bot/src/commands/appraise.command.ts) - Market appraisal command
|
||||||
|
- **Types**: [`packages/eve-bot/src/types/global.d.ts`](packages/eve-bot/src/types/global.d.ts)
|
||||||
|
|
||||||
|
#### [eve-web](packages/eve-web/)
|
||||||
|
|
||||||
|
- **Entry Point**: [`packages/eve-web/src/pages/index.tsx`](packages/eve-web/src/pages/index.tsx)
|
||||||
|
- **Middleware**: [`packages/eve-web/src/middleware.ts`](packages/eve-web/src/middleware.ts) - EVE SSO integration
|
||||||
|
- **Components**: [`packages/eve-web/src/components/stats/`](packages/eve-web/src/components/stats/)
|
||||||
|
- [`skill-queue.tsx`](packages/eve-web/src/components/stats/skill-queue.tsx) - Real-time skill training display
|
||||||
|
- [`wallet.tsx`](packages/eve-web/src/components/stats/wallet.tsx) - Wallet balance and changes
|
||||||
|
- **API Routes**: [`packages/eve-web/src/api/auth/`](packages/eve-web/src/api/auth/) - Authentication endpoints
|
||||||
|
- **Utilities**: [`packages/eve-web/src/utils/cookies.ts`](packages/eve-web/src/utils/cookies.ts)
|
||||||
|
|
||||||
|
## Key Technical Decisions
|
||||||
|
|
||||||
|
### Command System Architecture
|
||||||
|
|
||||||
|
- **Auto-discovery**: Commands are automatically discovered using glob patterns (`**/*.command.{js,ts}`)
|
||||||
|
- **Convention-based**: Each command exports a default Discord command definition
|
||||||
|
- **Event-driven**: Commands handle their own interaction events using global event listeners
|
||||||
|
- **Localized**: Full internationalization support with description translations
|
||||||
|
|
||||||
|
### Database Architecture
|
||||||
|
|
||||||
|
- **ORM**: Drizzle ORM with SQLite for local data storage
|
||||||
|
- **Schema**: [`packages/eve/src/db/schema.ts`](packages/eve/src/db/schema.ts) defines all tables
|
||||||
|
- **Models**: Active Record pattern with helper classes for complex operations
|
||||||
|
- **Migrations**: Drizzle Kit handles schema migrations
|
||||||
|
|
||||||
|
### EVE Online Integration
|
||||||
|
|
||||||
|
- **OAuth Flow**: Complete EVE SSO implementation with token management
|
||||||
|
- **Scope Management**: Comprehensive ESI scope definitions and validation
|
||||||
|
- **Token Refresh**: Automatic token refresh with scope preservation
|
||||||
|
- **Data Caching**: Smart caching of EVE reference data and API responses
|
||||||
|
|
||||||
|
### Web Interface Architecture
|
||||||
|
|
||||||
|
- **Framework**: Brisa for server-side rendering with progressive enhancement
|
||||||
|
- **Authentication**: EVE SSO integrated at middleware level
|
||||||
|
- **Components**: Server-side components with suspense for async data loading
|
||||||
|
- **Styling**: Tailwind CSS with DaisyUI components
|
||||||
|
|
||||||
|
## Design Patterns
|
||||||
|
|
||||||
|
### Package Exports
|
||||||
|
|
||||||
|
Each package uses structured exports for different concerns:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// @star-kitten/eve exports
|
||||||
|
export * from "./esi"; // ESI API integration
|
||||||
|
export * as CharacterAPI from "./esi/character";
|
||||||
|
export * as models from "./models"; // Data models
|
||||||
|
export * as db from "./db"; // Database access
|
||||||
|
export * from "./third-party"; // External APIs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- **API Errors**: Consistent error wrapping for external API calls
|
||||||
|
- **Token Validation**: Graceful handling of expired/invalid tokens
|
||||||
|
- **Database Errors**: Transaction rollback and error logging
|
||||||
|
|
||||||
|
### Data Processing
|
||||||
|
|
||||||
|
- **Streaming**: Large JSON files processed via streaming for memory efficiency
|
||||||
|
- **Caching**: Multi-level caching (in-memory, database, file system)
|
||||||
|
- **Type Safety**: Full TypeScript coverage with strict type checking
|
||||||
|
|
||||||
|
## Component Relationships
|
||||||
|
|
||||||
|
### Discord Bot Flow
|
||||||
|
|
||||||
|
1. [`main.ts`](packages/eve-bot/src/main.ts) initializes Dysnomia client
|
||||||
|
2. [`importCommands()`](packages/discord/src/commands/import-commands.ts) discovers command files
|
||||||
|
3. Commands register interaction handlers on global client
|
||||||
|
4. User interactions trigger command-specific handlers
|
||||||
|
|
||||||
|
### Web Authentication Flow
|
||||||
|
|
||||||
|
1. [`middleware.ts`](packages/eve-web/src/middleware.ts) intercepts requests
|
||||||
|
2. EVE SSO redirect initiated for unauthenticated users
|
||||||
|
3. OAuth callback validates tokens and creates/updates user records
|
||||||
|
4. Character data synchronized from ESI API
|
||||||
|
5. Dashboard components display real-time character statistics
|
||||||
|
|
||||||
|
### Data Synchronization
|
||||||
|
|
||||||
|
1. Reference data downloaded from external sources
|
||||||
|
2. JSON files processed using streaming query utilities
|
||||||
|
3. Character data fetched from ESI API
|
||||||
|
4. Local database updated with character and user information
|
||||||
|
5. Web components display cached data with real-time updates
|
||||||
|
|
||||||
|
## Critical Implementation Paths
|
||||||
|
|
||||||
|
### Adding New Commands
|
||||||
|
|
||||||
|
1. Create `*.command.ts` file in [`packages/eve-bot/src/commands/`](packages/eve-bot/src/commands/)
|
||||||
|
2. Export Discord command definition as default
|
||||||
|
3. Add interaction handlers to global client event listeners
|
||||||
|
4. Commands automatically discovered and registered
|
||||||
|
|
||||||
|
### EVE API Integration
|
||||||
|
|
||||||
|
1. Define required scopes in [`scopes.ts`](packages/eve/src/esi/scopes.ts)
|
||||||
|
2. Implement API client in appropriate ESI module
|
||||||
|
3. Add character model methods for data access
|
||||||
|
4. Create web components for data display
|
||||||
|
|
||||||
|
### Database Schema Changes
|
||||||
|
|
||||||
|
1. Update [`schema.ts`](packages/eve/src/db/schema.ts) with new tables/columns
|
||||||
|
2. Generate migration using Drizzle Kit
|
||||||
|
3. Update model classes with new methods
|
||||||
|
4. Add database exports to [`db/index.ts`](packages/eve/src/db/index.ts)
|
||||||
7
.kilocode/rules/memory-bank/brief.md
Normal file
7
.kilocode/rules/memory-bank/brief.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
This project, Star Kitten, is a personal project of mine which will be a set of packges for building discord bots and websites focused on the games I play. I am starting with an eve online bot and webpage and will be building a star citizen discord bot later.
|
||||||
|
|
||||||
|
This project used bun, Dysnomia js, and currently Brisa, but I plan to change that to an Elysia server with Ripple SPA.
|
||||||
|
|
||||||
|
I use TypeScript and focus on re-usable functional components as much as I can.
|
||||||
|
|
||||||
|
I would like to do my best to maintain at least 80% coverage with all major functionality tested using bun:test.
|
||||||
167
.kilocode/rules/memory-bank/context.md
Normal file
167
.kilocode/rules/memory-bank/context.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Project Context - Star Kitten
|
||||||
|
|
||||||
|
## Current Development State
|
||||||
|
|
||||||
|
### Project Maturity
|
||||||
|
|
||||||
|
- **Phase**: Active Development
|
||||||
|
- **Version**: 0.0.0 (pre-release)
|
||||||
|
- **Status**: Core framework established, basic functionality implemented
|
||||||
|
- **Last Major Update**: Memory bank initialization (January 2025)
|
||||||
|
|
||||||
|
### Current Work Focus
|
||||||
|
|
||||||
|
- **Primary**: EVE Online Discord bot and web interface development
|
||||||
|
- **Secondary**: Framework stabilization and documentation
|
||||||
|
- **Future**: Star Citizen integration planning
|
||||||
|
|
||||||
|
### Recent Changes
|
||||||
|
|
||||||
|
- Memory bank system initialized with comprehensive documentation
|
||||||
|
- Core architecture established across four packages
|
||||||
|
- Basic EVE Online integration functional
|
||||||
|
- Discord command system with auto-discovery implemented
|
||||||
|
- Web interface with EVE SSO authentication working
|
||||||
|
|
||||||
|
## Active Features
|
||||||
|
|
||||||
|
### Discord Bot (`eve-bot`)
|
||||||
|
|
||||||
|
- **Status**: Basic functionality working
|
||||||
|
- **Commands**:
|
||||||
|
- Market appraisal command (partial implementation)
|
||||||
|
- **Authentication**: Global client setup with Dysnomia
|
||||||
|
- **Deployment**: Environment-based configuration ready
|
||||||
|
|
||||||
|
### Web Interface (`eve-web`)
|
||||||
|
|
||||||
|
- **Status**: Basic dashboard working
|
||||||
|
- **Authentication**: EVE SSO OAuth flow implemented
|
||||||
|
- **Components**:
|
||||||
|
- Skill queue display with progress tracking
|
||||||
|
- Wallet balance with daily change calculation
|
||||||
|
- **Framework**: Brisa SSR with Tailwind CSS + DaisyUI
|
||||||
|
|
||||||
|
### Core Packages
|
||||||
|
|
||||||
|
- **@star-kitten/discord**: Command auto-discovery and handling
|
||||||
|
- **@star-kitten/eve**: ESI API integration, database models, third-party APIs
|
||||||
|
- **@star-kitten/util**: Streaming JSON queries, text processing, job scheduling
|
||||||
|
|
||||||
|
## Technical Debt & Known Issues
|
||||||
|
|
||||||
|
### Architecture Decisions Pending
|
||||||
|
|
||||||
|
- **Web Framework Migration**: Planned switch from Brisa to Elysia + Ripple SPA
|
||||||
|
- **Package Versioning**: All packages at 0.0.0, versioning strategy needed
|
||||||
|
- **Testing Coverage**: Current coverage unknown, target is 80%
|
||||||
|
|
||||||
|
### Implementation Gaps
|
||||||
|
|
||||||
|
- **Command System**: [`handle-commands.ts`](packages/discord/src/commands/handle-commands.ts) references global commands object not used in current pattern
|
||||||
|
- **Appraisal Command**: Modal submission handling incomplete
|
||||||
|
- **Mining Fleet Module**: Database schema exists but no implementation
|
||||||
|
- **API Error Handling**: Needs standardization across packages
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
- **Current Tables**: users, characters, resumeCommands, miningFleets, miningFleetParticipants
|
||||||
|
- **Migration Status**: Drizzle Kit configured but migration history unclear
|
||||||
|
- **Data Location**: SQLite database at configurable path
|
||||||
|
|
||||||
|
## Development Environment
|
||||||
|
|
||||||
|
### Setup Requirements
|
||||||
|
|
||||||
|
- **Runtime**: Bun (JavaScript runtime and package manager)
|
||||||
|
- **Language**: TypeScript with strict type checking
|
||||||
|
- **Database**: SQLite with Drizzle ORM
|
||||||
|
- **Development**: VS Code with format-on-save configuration
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
- **Package Manager**: Bun workspaces
|
||||||
|
- **Build Tool**: tsdown for TypeScript compilation
|
||||||
|
- **Scripts**: Unified commands across packages (build, dev, test)
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
|
||||||
|
- **EVE Online**: ESI API access requires client credentials
|
||||||
|
- **Janice API**: Market appraisal service requires API key
|
||||||
|
- **Discord**: Bot token required for Discord API access
|
||||||
|
|
||||||
|
## Next Steps Priority
|
||||||
|
|
||||||
|
### Immediate (Current Sprint)
|
||||||
|
|
||||||
|
1. Complete appraisal command implementation
|
||||||
|
2. Add comprehensive error handling to ESI integration
|
||||||
|
3. Implement basic test coverage for core functionality
|
||||||
|
4. Document API authentication setup process
|
||||||
|
|
||||||
|
### Short Term (Next Month)
|
||||||
|
|
||||||
|
1. Implement mining fleet management functionality
|
||||||
|
2. Add more Discord commands (character lookup, skill planning)
|
||||||
|
3. Expand web interface with more character statistics
|
||||||
|
4. Establish proper versioning and release process
|
||||||
|
|
||||||
|
### Medium Term (Next Quarter)
|
||||||
|
|
||||||
|
1. Migrate web framework to Elysia + Ripple SPA
|
||||||
|
2. Add support for corporation and alliance management
|
||||||
|
3. Implement data synchronization and caching strategies
|
||||||
|
4. Create documentation for third-party developers
|
||||||
|
|
||||||
|
### Long Term (Next Year)
|
||||||
|
|
||||||
|
1. Star Citizen integration architecture
|
||||||
|
2. Multi-game framework abstraction
|
||||||
|
3. Plugin system for community extensions
|
||||||
|
4. Performance optimization and scaling
|
||||||
|
|
||||||
|
## Key Stakeholders
|
||||||
|
|
||||||
|
### Internal Development
|
||||||
|
|
||||||
|
- **Primary Developer**: j-b-3 (project owner and main developer)
|
||||||
|
- **Target Users**: EVE Online corporations and alliances
|
||||||
|
- **Future Contributors**: Open source community (planned)
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
|
||||||
|
- **CCP Games**: EVE Online ESI API provider
|
||||||
|
- **Discord**: Platform and API provider
|
||||||
|
- **Community**: EVE Online player community feedback
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### Technical Risks
|
||||||
|
|
||||||
|
- **Framework Migration**: Brisa to Elysia migration complexity
|
||||||
|
- **API Rate Limits**: EVE ESI and third-party API limitations
|
||||||
|
- **Database Performance**: SQLite scalability for large datasets
|
||||||
|
- **Token Management**: OAuth token refresh reliability
|
||||||
|
|
||||||
|
### Project Risks
|
||||||
|
|
||||||
|
- **Single Developer**: No backup maintainer currently
|
||||||
|
- **Community Adoption**: Uncertain uptake by EVE Online communities
|
||||||
|
- **Game API Changes**: Dependency on external game APIs
|
||||||
|
- **Competition**: Existing EVE Online bot solutions
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Current Measurements
|
||||||
|
|
||||||
|
- **Code Quality**: Type safety across all packages
|
||||||
|
- **Architecture**: Modular design with clear separation
|
||||||
|
- **Documentation**: Comprehensive memory bank established
|
||||||
|
- **Functionality**: Basic bot and web interface working
|
||||||
|
|
||||||
|
### Target Metrics
|
||||||
|
|
||||||
|
- **Test Coverage**: 80% across major functionality
|
||||||
|
- **Performance**: <2 second Discord command response times
|
||||||
|
- **Reliability**: 99%+ uptime for deployed services
|
||||||
|
- **Adoption**: Usage by multiple EVE Online communities
|
||||||
100
.kilocode/rules/memory-bank/product.md
Normal file
100
.kilocode/rules/memory-bank/product.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Product Overview - Star Kitten
|
||||||
|
|
||||||
|
## What Star Kitten Is
|
||||||
|
|
||||||
|
Star Kitten is a comprehensive framework for building Discord bots and web applications focused on gaming communities, specifically MMO games like EVE Online and Star Citizen. It provides reusable packages and components that handle common gaming bot functionality including authentication, API integrations, data management, and user interfaces.
|
||||||
|
|
||||||
|
## Problems It Solves
|
||||||
|
|
||||||
|
### For Gaming Community Managers
|
||||||
|
|
||||||
|
- **Fragmented Tools**: Gaming communities often use multiple disconnected tools for different functions (Discord bots, web dashboards, data analysis)
|
||||||
|
- **Complex Authentication**: Managing OAuth flows for game APIs (EVE ESI, future Star Citizen APIs) is complex and error-prone
|
||||||
|
- **Data Integration**: Combining game data with Discord interactions and web interfaces requires significant development effort
|
||||||
|
- **Repetitive Development**: Building similar functionality across different gaming communities involves recreating the same patterns
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
- **Discord Bot Boilerplate**: Eliminates the need to write command handling, interaction management, and Discord client setup from scratch
|
||||||
|
- **Game API Complexity**: Abstracts complex game API authentication, token management, and data fetching
|
||||||
|
- **Database Management**: Provides pre-built schemas and models for common gaming bot use cases
|
||||||
|
- **Type Safety**: Offers comprehensive TypeScript definitions for all game data types and API responses
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Architecture Philosophy
|
||||||
|
|
||||||
|
Star Kitten follows a modular, package-based architecture where each package serves a specific purpose:
|
||||||
|
|
||||||
|
1. **[@star-kitten/discord](packages/discord/)** - Core Discord bot functionality with command handling and interaction management
|
||||||
|
2. **[@star-kitten/eve](packages/eve/)** - EVE Online specific integrations including ESI API, authentication, and data models
|
||||||
|
3. **[@star-kitten/util](packages/util/)** - Shared utilities including schedulers, text processing, and JSON querying
|
||||||
|
4. **[eve-bot](packages/eve-bot/)** - Complete EVE Online Discord bot implementation
|
||||||
|
5. **[eve-web](packages/eve-web/)** - Web interface for EVE Online bot management and statistics
|
||||||
|
|
||||||
|
### Key Workflows
|
||||||
|
|
||||||
|
#### Discord Bot Creation
|
||||||
|
|
||||||
|
1. Import command handling from `@star-kitten/discord`
|
||||||
|
2. Create command files with `.command.ts` extension
|
||||||
|
3. Bot automatically discovers and registers commands
|
||||||
|
4. Handle interactions through standardized command structure
|
||||||
|
|
||||||
|
#### EVE Online Integration
|
||||||
|
|
||||||
|
1. User authenticates via EVE SSO through web interface
|
||||||
|
2. Tokens stored securely with automatic refresh handling
|
||||||
|
3. ESI API calls abstracted through helper functions
|
||||||
|
4. Character data synchronized and cached in local database
|
||||||
|
|
||||||
|
#### Web Dashboard
|
||||||
|
|
||||||
|
1. Users authenticate via EVE SSO
|
||||||
|
2. Character data displayed through reactive components
|
||||||
|
3. Real-time statistics from ESI API
|
||||||
|
4. Admin functions for bot management
|
||||||
|
|
||||||
|
## User Experience Goals
|
||||||
|
|
||||||
|
### Discord Users
|
||||||
|
|
||||||
|
- **Intuitive Commands**: Slash commands with clear descriptions and localization support
|
||||||
|
- **Rich Interactions**: Modal forms, select menus, and interactive components for complex workflows
|
||||||
|
- **Immediate Feedback**: Fast response times with proper loading states and error handling
|
||||||
|
- **Contextual Help**: Commands provide guidance and examples for proper usage
|
||||||
|
|
||||||
|
### Web Users
|
||||||
|
|
||||||
|
- **Seamless Authentication**: Single sign-on through EVE Online credentials
|
||||||
|
- **Responsive Design**: Works well on desktop and mobile devices
|
||||||
|
- **Real-time Data**: Live updates of character stats, wallet balances, and skill queues
|
||||||
|
- **Progressive Enhancement**: Basic functionality works without JavaScript
|
||||||
|
|
||||||
|
### Bot Administrators
|
||||||
|
|
||||||
|
- **Easy Deployment**: Simple configuration through environment variables
|
||||||
|
- **Monitoring**: Built-in logging and error tracking
|
||||||
|
- **Scalability**: Modular architecture supports adding new features without breaking existing functionality
|
||||||
|
- **Data Control**: Local database with full control over user data and privacy
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Technical Success
|
||||||
|
|
||||||
|
- **Reliability**: 99%+ uptime for bot and web services
|
||||||
|
- **Performance**: < 2 second response times for Discord commands
|
||||||
|
- **Code Quality**: 80%+ test coverage maintained across all packages
|
||||||
|
- **Developer Experience**: New features can be added without modifying core packages
|
||||||
|
|
||||||
|
### User Adoption
|
||||||
|
|
||||||
|
- **Community Growth**: Framework adoption by multiple EVE Online corporations/alliances
|
||||||
|
- **Feature Usage**: High engagement with key features like appraisals, fleet management, and character tracking
|
||||||
|
- **Feedback Integration**: Regular updates based on user feedback and pain points
|
||||||
|
|
||||||
|
### Long-term Vision
|
||||||
|
|
||||||
|
- **Multi-Game Support**: Expand to Star Citizen and other MMO games
|
||||||
|
- **Ecosystem Growth**: Third-party packages extending Star Kitten functionality
|
||||||
|
- **Open Source Community**: Active contributor base improving and extending the framework
|
||||||
151
.kilocode/rules/memory-bank/tasks.md
Normal file
151
.kilocode/rules/memory-bank/tasks.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Tasks - Star Kitten
|
||||||
|
|
||||||
|
## Add New Discord Command
|
||||||
|
|
||||||
|
**Last performed:** Initial setup
|
||||||
|
**Files to modify:**
|
||||||
|
|
||||||
|
- `packages/eve-bot/src/commands/[command-name].command.ts` - New command file
|
||||||
|
- Global Discord client automatically registers new commands
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Create new command file following naming convention `[name].command.ts`
|
||||||
|
2. Export Discord command definition as default export
|
||||||
|
3. Add interaction handlers using global client event listeners
|
||||||
|
4. Include localization for command names and descriptions
|
||||||
|
5. Test command registration and interaction handling
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
|
||||||
|
- Commands are auto-discovered using glob patterns
|
||||||
|
- Each command handles its own interactions via global client
|
||||||
|
- Follow existing pattern from `appraise.command.ts`
|
||||||
|
- Include comprehensive error handling for all interactions
|
||||||
|
|
||||||
|
## Add New EVE ESI Integration
|
||||||
|
|
||||||
|
**Last performed:** Character and wallet integration
|
||||||
|
**Files to modify:**
|
||||||
|
|
||||||
|
- `packages/eve/src/esi/[module].ts` - New ESI module
|
||||||
|
- `packages/eve/src/esi/index.ts` - Export new module
|
||||||
|
- `packages/eve/src/esi/scopes.ts` - Add required scopes if needed
|
||||||
|
- `packages/eve/src/db/models/character.model.ts` - Add helper methods if needed
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Define required ESI scopes in scopes.ts
|
||||||
|
2. Create new ESI module with fetch functions
|
||||||
|
3. Add proper TypeScript interfaces for API responses
|
||||||
|
4. Implement caching where appropriate
|
||||||
|
5. Add character model helper methods for data access
|
||||||
|
6. Update exports in index.ts
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
|
||||||
|
- Always handle token refresh automatically
|
||||||
|
- Include comprehensive error handling for API failures
|
||||||
|
- Follow existing patterns from character.ts and wallet integrations
|
||||||
|
- Cache responses appropriately to respect rate limits
|
||||||
|
|
||||||
|
## Database Schema Migration
|
||||||
|
|
||||||
|
**Last performed:** Initial schema setup
|
||||||
|
**Files to modify:**
|
||||||
|
|
||||||
|
- `packages/eve/src/db/schema.ts` - Schema definitions
|
||||||
|
- `packages/eve/src/db/models/` - Model helpers if needed
|
||||||
|
- `packages/eve/src/db/index.ts` - Export new tables
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Update schema.ts with new tables/columns
|
||||||
|
2. Generate migration using `bun run generate-migrations`
|
||||||
|
3. Test migration with `bun run migrate`
|
||||||
|
4. Update model classes with new methods
|
||||||
|
5. Add exports to db/index.ts
|
||||||
|
6. Update database initialization in main applications
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
|
||||||
|
- Always backup database before migrations
|
||||||
|
- Test migrations on development database first
|
||||||
|
- Update all references to schema changes
|
||||||
|
- Consider data migration scripts for existing data
|
||||||
|
|
||||||
|
## Add New Web Component
|
||||||
|
|
||||||
|
**Last performed:** Skill queue and wallet components
|
||||||
|
**Files to modify:**
|
||||||
|
|
||||||
|
- `packages/eve-web/src/components/[category]/[component].tsx` - New component
|
||||||
|
- `packages/eve-web/src/pages/index.tsx` - Import and use component
|
||||||
|
- Related ESI integration if data source needed
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Create new component following existing patterns
|
||||||
|
2. Implement async data loading with suspense fallback
|
||||||
|
3. Add proper TypeScript interfaces for props
|
||||||
|
4. Style using Tailwind CSS and DaisyUI components
|
||||||
|
5. Import and integrate into appropriate page
|
||||||
|
6. Test with real data and loading states
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
|
||||||
|
- Always provide suspense fallback for loading states
|
||||||
|
- Follow existing component patterns from skill-queue.tsx and wallet.tsx
|
||||||
|
- Use server-side rendering capabilities of Brisa
|
||||||
|
- Ensure responsive design for mobile devices
|
||||||
|
|
||||||
|
## Add Third-Party API Integration
|
||||||
|
|
||||||
|
**Last performed:** Janice API integration
|
||||||
|
**Files to modify:**
|
||||||
|
|
||||||
|
- `packages/eve/src/third-party/[service].ts` - New API integration
|
||||||
|
- `packages/eve/src/third-party/index.ts` - Export new service
|
||||||
|
- Environment configuration for API keys
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Create new service module with TypeScript interfaces
|
||||||
|
2. Implement API client with proper error handling
|
||||||
|
3. Add caching layer for performance
|
||||||
|
4. Include comprehensive validation for inputs/outputs
|
||||||
|
5. Add tests for all API functions
|
||||||
|
6. Update exports and environment configuration
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
|
||||||
|
- Always implement proper rate limiting and caching
|
||||||
|
- Include comprehensive error handling and validation
|
||||||
|
- Follow existing patterns from janice.ts
|
||||||
|
- Add API key management through environment variables
|
||||||
|
- Test with real API to ensure compatibility
|
||||||
|
|
||||||
|
## Update Reference Data
|
||||||
|
|
||||||
|
**Last performed:** Initial data setup
|
||||||
|
**Files to modify:**
|
||||||
|
|
||||||
|
- `data/reference-data/` - Static reference files
|
||||||
|
- `data/hoboleaks/` - SDE data files
|
||||||
|
- `packages/eve/src/models/` - Data model updates if needed
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Run `bun get-data` to download latest reference data
|
||||||
|
2. Verify data integrity and format consistency
|
||||||
|
3. Update data models if schema changes detected
|
||||||
|
4. Test applications with new reference data
|
||||||
|
5. Commit updated data files to repository
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
|
||||||
|
- Reference data updates can be large (hundreds of MB)
|
||||||
|
- Always verify data integrity before committing
|
||||||
|
- Test critical functionality after data updates
|
||||||
|
- Consider data migration scripts for breaking changes
|
||||||
|
- Monitor for EVE Online patch changes that affect data structure
|
||||||
249
.kilocode/rules/memory-bank/tech.md
Normal file
249
.kilocode/rules/memory-bank/tech.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Technology Stack - Star Kitten
|
||||||
|
|
||||||
|
## Core Technologies
|
||||||
|
|
||||||
|
### Runtime & Package Management
|
||||||
|
|
||||||
|
- **Bun** - JavaScript runtime and package manager
|
||||||
|
- Fast package installation and script execution
|
||||||
|
- Native TypeScript support
|
||||||
|
- Built-in test runner (`bun test`)
|
||||||
|
- WebAPI compatibility for modern web standards
|
||||||
|
|
||||||
|
### Language & Type System
|
||||||
|
|
||||||
|
- **TypeScript** - Primary development language
|
||||||
|
- Strict type checking enabled across all packages
|
||||||
|
- Comprehensive type definitions for EVE Online data structures
|
||||||
|
- Interface-driven development for API integrations
|
||||||
|
- Global type declarations for shared client instances
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
- **Workspace Architecture** - Bun workspaces for monorepo management
|
||||||
|
- Four core packages: `@star-kitten/discord`, `@star-kitten/eve`, `@star-kitten/util`, plus applications
|
||||||
|
- Cross-package dependencies managed through workspace references
|
||||||
|
- Unified build commands across all packages (`bun build`, `bun dev`, `bun test`)
|
||||||
|
- **tsdown** - TypeScript bundler for package builds
|
||||||
|
- Zero-config TypeScript compilation
|
||||||
|
- Watch mode support for development
|
||||||
|
- Declaration file generation
|
||||||
|
|
||||||
|
## Discord Integration
|
||||||
|
|
||||||
|
### Discord Framework
|
||||||
|
|
||||||
|
- **Dysnomia** - Discord API library
|
||||||
|
- Modern JavaScript Discord library with full TypeScript support
|
||||||
|
- Gateway intents configuration for minimal resource usage
|
||||||
|
- Built-in interaction handling (slash commands, modals, components)
|
||||||
|
- Event-driven architecture for command processing
|
||||||
|
|
||||||
|
### Command Architecture
|
||||||
|
|
||||||
|
- **Auto-discovery Pattern** - Commands automatically registered via file system scanning
|
||||||
|
- Glob patterns (`**/*.command.{js,ts}`) for command file detection
|
||||||
|
- Convention-based naming and structure
|
||||||
|
- Global client registration with event listeners
|
||||||
|
- **Internationalization** - Full i18n support
|
||||||
|
- Command names and descriptions localized across multiple languages
|
||||||
|
- Structured translation files for consistent localization
|
||||||
|
|
||||||
|
## Database & Data Management
|
||||||
|
|
||||||
|
### Database Layer
|
||||||
|
|
||||||
|
- **SQLite** - Local database storage
|
||||||
|
- Single-file database for simple deployment
|
||||||
|
- ACID compliance for data integrity
|
||||||
|
- Excellent performance for read-heavy workloads
|
||||||
|
- **Drizzle ORM** - Type-safe database access
|
||||||
|
- Schema-first approach with TypeScript definitions
|
||||||
|
- Automatic migration generation via Drizzle Kit
|
||||||
|
- Relationship mapping and query builder
|
||||||
|
- Active Record pattern implementation in model helpers
|
||||||
|
|
||||||
|
### Data Processing
|
||||||
|
|
||||||
|
- **Streaming JSON Processing** - Memory-efficient handling of large datasets
|
||||||
|
- `stream-json` library for parsing large EVE reference data files
|
||||||
|
- Query-based filtering during stream processing
|
||||||
|
- In-memory caching with TTL for frequently accessed data
|
||||||
|
- **Reference Data Management**
|
||||||
|
- External data sources (everef.net, hoboleaks)
|
||||||
|
- Automated download and extraction scripts
|
||||||
|
- Static export processes for optimized data formats
|
||||||
|
|
||||||
|
## Web Framework
|
||||||
|
|
||||||
|
### Frontend Framework
|
||||||
|
|
||||||
|
- **Brisa** - Server-side rendering framework
|
||||||
|
- Modern SSR with progressive enhancement
|
||||||
|
- Component-based architecture
|
||||||
|
- Built-in suspense support for async data loading
|
||||||
|
- TypeScript-first development experience
|
||||||
|
|
||||||
|
### Styling & UI
|
||||||
|
|
||||||
|
- **Tailwind CSS** - Utility-first CSS framework
|
||||||
|
- Responsive design system
|
||||||
|
- Component-level styling
|
||||||
|
- **DaisyUI** - Tailwind CSS component library
|
||||||
|
- Pre-built UI components (stats, progress bars, modals)
|
||||||
|
- Consistent design language across web interface
|
||||||
|
|
||||||
|
### Authentication & Middleware
|
||||||
|
|
||||||
|
- **EVE SSO Integration** - OAuth2 flow implementation
|
||||||
|
- Complete authentication middleware stack
|
||||||
|
- Token management with automatic refresh
|
||||||
|
- Scope validation and permission handling
|
||||||
|
- **Cookie Management** - Custom utilities for session handling
|
||||||
|
- Secure cookie implementation
|
||||||
|
- State management for OAuth flows
|
||||||
|
|
||||||
|
## External API Integrations
|
||||||
|
|
||||||
|
### EVE Online APIs
|
||||||
|
|
||||||
|
- **ESI (EVE Swagger Interface)** - Official EVE Online API
|
||||||
|
- Complete OAuth2 flow with scope management
|
||||||
|
- Character, corporation, and alliance data access
|
||||||
|
- Market data, skill queues, wallet information
|
||||||
|
- Automatic token refresh with scope preservation
|
||||||
|
- **JWT Token Validation** - Secure token handling
|
||||||
|
- Public key verification against EVE's JWKS endpoint
|
||||||
|
- Token scope extraction and validation
|
||||||
|
- Character ID resolution from JWT payloads
|
||||||
|
|
||||||
|
### Third-Party APIs
|
||||||
|
|
||||||
|
- **Janice API** - Market appraisal service
|
||||||
|
- Price checking and market analysis
|
||||||
|
- Bulk item appraisal functionality
|
||||||
|
- Caching layer for improved performance
|
||||||
|
- **EveTycoon API** - Market data integration
|
||||||
|
- Historical price data
|
||||||
|
- Market trend analysis
|
||||||
|
|
||||||
|
## Development Tools & Practices
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- **Prettier** - Code formatting
|
||||||
|
- Consistent formatting across all packages
|
||||||
|
- Integration with VS Code for format-on-save
|
||||||
|
- Shared configuration files (`.prettierrc.yaml`)
|
||||||
|
- **ESLint** - Code linting (implied by VS Code settings)
|
||||||
|
- TypeScript-aware linting rules
|
||||||
|
- Automatic fixing on save
|
||||||
|
|
||||||
|
### Testing Framework
|
||||||
|
|
||||||
|
- **Bun Test** - Native test runner
|
||||||
|
- Fast test execution with TypeScript support
|
||||||
|
- Coverage reporting capabilities
|
||||||
|
- Target: 80%+ test coverage across major functionality
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
|
||||||
|
- **VS Code Configuration**
|
||||||
|
- Consistent editor settings across team
|
||||||
|
- Format on save enabled
|
||||||
|
- Trailing whitespace removal
|
||||||
|
- Final newline insertion
|
||||||
|
- **Environment Management**
|
||||||
|
- dotenvx for environment variable management
|
||||||
|
- Separate configurations for development and production
|
||||||
|
- Secure key management for API credentials
|
||||||
|
|
||||||
|
## Utility Libraries
|
||||||
|
|
||||||
|
### Scheduling & Background Jobs
|
||||||
|
|
||||||
|
- **Cron Parser** - Schedule management
|
||||||
|
- Cron expression parsing and validation
|
||||||
|
- Job scheduling with repeat patterns
|
||||||
|
- **Custom Queue System** - Background job processing
|
||||||
|
- SQLite-based job queue
|
||||||
|
- Worker thread implementation
|
||||||
|
- Email notifications and other background tasks
|
||||||
|
|
||||||
|
### Text & Data Processing
|
||||||
|
|
||||||
|
- **Date Utilities** - Time manipulation
|
||||||
|
- date-fns for date calculations
|
||||||
|
- EVE time conversion utilities
|
||||||
|
- **Logging** - Structured application logs
|
||||||
|
- Winston logging framework
|
||||||
|
- Configurable log levels and outputs
|
||||||
|
- **Text Processing** - Formatting and conversion
|
||||||
|
- Number formatting (K, M, B suffixes)
|
||||||
|
- EVE markup to Discord markdown conversion
|
||||||
|
- Text truncation and cleaning utilities
|
||||||
|
|
||||||
|
## Deployment & Configuration
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
- **Multi-Environment Support**
|
||||||
|
- Development, production environment files
|
||||||
|
- Secure API key management
|
||||||
|
- Database path configuration
|
||||||
|
- **Docker Ready** - Containerization support
|
||||||
|
- Bun-based container builds
|
||||||
|
- SQLite database volume mounting
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
|
||||||
|
- **Migration System** - Schema version control
|
||||||
|
- Drizzle Kit migration generation
|
||||||
|
- Automated migration execution
|
||||||
|
- Schema evolution tracking
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Package Organization
|
||||||
|
|
||||||
|
- **Modular Monorepo** - Clear separation of concerns
|
||||||
|
- Core packages for reusable functionality
|
||||||
|
- Application packages for specific implementations
|
||||||
|
- Shared utilities across packages
|
||||||
|
- **Export Patterns** - Structured package APIs
|
||||||
|
- Namespace exports for logical grouping
|
||||||
|
- Direct exports for commonly used functions
|
||||||
|
- Type-only exports where appropriate
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- **Consistent Error Patterns** - Standardized error handling
|
||||||
|
- API error wrapping and propagation
|
||||||
|
- Graceful degradation for external service failures
|
||||||
|
- Comprehensive error logging
|
||||||
|
|
||||||
|
### Performance Optimizations
|
||||||
|
|
||||||
|
- **Caching Strategy** - Multi-level caching
|
||||||
|
- In-memory caching for frequently accessed data
|
||||||
|
- Database-level caching for computed results
|
||||||
|
- HTTP response caching for external APIs
|
||||||
|
- **Streaming Processing** - Memory-efficient data handling
|
||||||
|
- Large file processing without memory overflow
|
||||||
|
- Real-time data updates through streaming APIs
|
||||||
|
|
||||||
|
## Future Technology Considerations
|
||||||
|
|
||||||
|
### Migration Plans
|
||||||
|
|
||||||
|
- **Web Framework Migration** - Planned transition from Brisa to Elysia + Ripple SPA
|
||||||
|
- Backend: Elysia for high-performance HTTP server
|
||||||
|
- Frontend: Ripple SPA for modern client-side architecture
|
||||||
|
- Gradual migration strategy maintaining backward compatibility
|
||||||
|
|
||||||
|
### Expansion Capabilities
|
||||||
|
|
||||||
|
- **Multi-Game Support** - Architecture designed for game expansion
|
||||||
|
- Pluggable game integration modules
|
||||||
|
- Shared utilities across different game APIs
|
||||||
|
- Common authentication and data patterns
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
12
.vscode/settings.json
vendored
Normal file
12
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit"
|
||||||
|
},
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
|
}
|
||||||
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "star-kitten",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Star Kitten framework",
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun --filter '*' build",
|
||||||
|
"dev": "bun --filter '*' dev",
|
||||||
|
"test": "bun --filter '*' test",
|
||||||
|
"get-data": "bun refresh:reference-data && bun refresh:hoboleaks && bun static-export",
|
||||||
|
"refresh:reference-data": "bun run ./packages/util/dist/downloadAndExtract.js https://data.everef.net/reference-data/reference-data-latest.tar.xz ./data/reference-data",
|
||||||
|
"refresh:hoboleaks": "bun run ./packages/util/dist/downloadAndExtract.js https://data.everef.net/hoboleaks-sde/hoboleaks-sde-latest.tar.xz ./data/hoboleaks",
|
||||||
|
"static-export": "bun run ./packages/eve/scripts/exportSolarSystems.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-cache": "^5.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/discord/.github/workflows/release.yml
vendored
Normal file
26
packages/discord/.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- run: npx changelogithub
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
38
packages/discord/.github/workflows/unit-test.yml
vendored
Normal file
38
packages/discord/.github/workflows/unit-test.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Unit Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4.1.0
|
||||||
|
|
||||||
|
- name: Set node LTS
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm run typecheck
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm run test
|
||||||
4
packages/discord/.gitignore
vendored
Normal file
4
packages/discord/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
8
packages/discord/.prettierrc.yaml
Normal file
8
packages/discord/.prettierrc.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
trailingComma: all
|
||||||
|
tabWidth: 2
|
||||||
|
useTabs: false
|
||||||
|
semi: true
|
||||||
|
singleQuote: true
|
||||||
|
printWidth: 140
|
||||||
|
experimentalTernaries: true
|
||||||
|
quoteProps: consistent
|
||||||
3
packages/discord/.vscode/settings.json
vendored
Normal file
3
packages/discord/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
23
packages/discord/README.md
Normal file
23
packages/discord/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# tsdown-starter
|
||||||
|
|
||||||
|
A starter for creating a TypeScript package.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run the unit tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
- Build the library:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
215
packages/discord/bun.lock
Normal file
215
packages/discord/bun.lock
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "tsdown-starter",
|
||||||
|
"dependencies": {
|
||||||
|
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.2.21",
|
||||||
|
"@types/node": "^22.15.17",
|
||||||
|
"bumpp": "^10.1.0",
|
||||||
|
"tsdown": "^0.11.9",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||||
|
|
||||||
|
"@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
|
"@oxc-project/types": ["@oxc-project/types@0.70.0", "", {}, "sha512-ngyLUpUjO3dpqygSRQDx7nMx8+BmXbWOU4oIwTJFV2MVIDG7knIZwgdwXlQWLg3C3oxg1lS7ppMtPKqKFb7wzw=="],
|
||||||
|
|
||||||
|
"@projectdysnomia/dysnomia": ["@projectdysnomia/dysnomia@github:projectdysnomia/dysnomia#5e3300e", { "dependencies": { "ws": "^8.18.0" }, "optionalDependencies": { "@stablelib/xchacha20poly1305": "~1.0.1", "opusscript": "^0.1.1" }, "peerDependencies": { "@discordjs/opus": "^0.9.0", "erlpack": "github:discord/erlpack", "eventemitter3": "^5.0.1", "pako": "^2.1.0", "sodium-native": "^4.1.1", "zlib-sync": "^0.1.9" }, "optionalPeers": ["@discordjs/opus", "erlpack", "eventemitter3", "pako", "sodium-native", "zlib-sync"] }, "projectdysnomia-dysnomia-5e3300e"],
|
||||||
|
|
||||||
|
"@quansync/fs": ["@quansync/fs@0.1.5", "", { "dependencies": { "quansync": "^0.2.11" } }, "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-geUG/FUpm+membLC0NQBb39vVyOfguYZ2oyXc7emr6UjH6TeEECT4b0CPZXKFnELareTiU/Jfl70/eEgNxyQeA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-7wPXDwcOtv2I+pWTL2UNpNAxMAGukgBT90Jz4DCfwaYdGvQncF7J0S7IWrRVsRFhBavxM+65RcueE3VXw5UIbg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-agO5mONTNKVrcIt4SRxw5Ni0FOVV3gaH8dIiNp1A4JeU91b9kw7x+JRuNJAQuM2X3pYqVvA6qh13UTNOsaqM/Q=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9", "", { "os": "linux", "cpu": "arm" }, "sha512-dDNDV9p/8WYDriS9HCcbH6y6+JP38o3enj/pMkdkmkxEnZ0ZoHIfQ9RGYWeRYU56NKBCrya4qZBJx49Jk9LRug=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-kZKegmHG1ZvfsFIwYU6DeFSxSIcIliXzeznsJHUo9D9/dlVSDi/PUvsRKcuJkQjZoejM6pk8MHN/UfgGdIhPHw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-f+VL8mO31pyMJiJPr2aA1ryYONkP2UqgbwK7fKtKHZIeDd/AoUGn3+ujPqDhuy2NxgcJ5H8NaSvDpG1tJMHh+g=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.9", "", { "os": "linux", "cpu": "x64" }, "sha512-GiUEZ0WPjX5LouDoC3O8aJa4h6BLCpIvaAboNw5JoRour/3dC6rbtZZ/B5FC3/ySsN3/dFOhAH97ylQxoZJi7A=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.9", "", { "os": "linux", "cpu": "x64" }, "sha512-AMb0dicw+QHh6RxvWo4BRcuTMgS0cwUejJRMpSyIcHYnKTbj6nUW4HbWNQuDfZiF27l6F5gEwBS+YLUdVzL9vg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.9", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.4" }, "cpu": "none" }, "sha512-+pdaiTx7L8bWKvsAuCE0HAxP1ze1WOLoWGCawcrZbMSY10dMh2i82lJiH6tXGXbfYYwsNWhWE2NyG4peFZvRfQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-A7kN248viWvb8eZMzQu024TBKGoyoVYBsDG2DtoP8u2pzwoh5yDqUL291u01o4f8uzpUHq8mfwQJmcGChFu8KQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-DzKN7iEYjAP8AK8F2G2aCej3fk43Y/EQrVrR3gF0XREes56chjQ7bXIhw819jv74BbxGdnpPcslhet/cgt7WRA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.9", "", { "os": "win32", "cpu": "x64" }, "sha512-GMWgTvvbZ8TfBsAiJpoz4SRq3IN3aUMn0rYm8q4I8dcEk4J1uISyfb6ZMzvqW+cvScTWVKWZNqnrmYOKLLUt4w=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="],
|
||||||
|
|
||||||
|
"@stablelib/aead": ["@stablelib/aead@1.0.1", "", {}, "sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg=="],
|
||||||
|
|
||||||
|
"@stablelib/binary": ["@stablelib/binary@1.0.1", "", { "dependencies": { "@stablelib/int": "^1.0.1" } }, "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q=="],
|
||||||
|
|
||||||
|
"@stablelib/chacha": ["@stablelib/chacha@1.0.1", "", { "dependencies": { "@stablelib/binary": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg=="],
|
||||||
|
|
||||||
|
"@stablelib/chacha20poly1305": ["@stablelib/chacha20poly1305@1.0.1", "", { "dependencies": { "@stablelib/aead": "^1.0.1", "@stablelib/binary": "^1.0.1", "@stablelib/chacha": "^1.0.1", "@stablelib/constant-time": "^1.0.1", "@stablelib/poly1305": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA=="],
|
||||||
|
|
||||||
|
"@stablelib/constant-time": ["@stablelib/constant-time@1.0.1", "", {}, "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg=="],
|
||||||
|
|
||||||
|
"@stablelib/int": ["@stablelib/int@1.0.1", "", {}, "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w=="],
|
||||||
|
|
||||||
|
"@stablelib/poly1305": ["@stablelib/poly1305@1.0.1", "", { "dependencies": { "@stablelib/constant-time": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA=="],
|
||||||
|
|
||||||
|
"@stablelib/wipe": ["@stablelib/wipe@1.0.1", "", {}, "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg=="],
|
||||||
|
|
||||||
|
"@stablelib/xchacha20": ["@stablelib/xchacha20@1.0.1", "", { "dependencies": { "@stablelib/binary": "^1.0.1", "@stablelib/chacha": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-1YkiZnFF4veUwBVhDnDYwo6EHeKzQK4FnLiO7ezCl/zu64uG0bCCAUROJaBkaLH+5BEsO3W7BTXTguMbSLlWSw=="],
|
||||||
|
|
||||||
|
"@stablelib/xchacha20poly1305": ["@stablelib/xchacha20poly1305@1.0.1", "", { "dependencies": { "@stablelib/aead": "^1.0.1", "@stablelib/chacha20poly1305": "^1.0.1", "@stablelib/constant-time": "^1.0.1", "@stablelib/wipe": "^1.0.1", "@stablelib/xchacha20": "^1.0.1" } }, "sha512-B1Abj0sMJ8h3HNmGnJ7vHBrAvxuNka6cJJoZ1ILN7iuacXp7sUYcgOVEOTLWj+rtQMpspY9tXSCRLPmN1mQNWg=="],
|
||||||
|
|
||||||
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@22.18.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
|
||||||
|
|
||||||
|
"ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="],
|
||||||
|
|
||||||
|
"args-tokenizer": ["args-tokenizer@0.3.0", "", {}, "sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q=="],
|
||||||
|
|
||||||
|
"ast-kit": ["ast-kit@2.1.2", "", { "dependencies": { "@babel/parser": "^7.28.0", "pathe": "^2.0.3" } }, "sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g=="],
|
||||||
|
|
||||||
|
"birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="],
|
||||||
|
|
||||||
|
"bumpp": ["bumpp@10.2.3", "", { "dependencies": { "ansis": "^4.1.0", "args-tokenizer": "^0.3.0", "c12": "^3.2.0", "cac": "^6.7.14", "escalade": "^3.2.0", "jsonc-parser": "^3.3.1", "package-manager-detector": "^1.3.0", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "yaml": "^2.8.1" }, "bin": { "bumpp": "bin/bumpp.mjs" } }, "sha512-nsFBZACxuBVu6yzDSaZZaWpX5hTQ+++9WtYkmO+0Bd3cpSq0Mzvqw5V83n+fOyRj3dYuZRFCQf5Z9NNfZj+Rnw=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||||
|
|
||||||
|
"c12": ["c12@3.2.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-ixkEtbYafL56E6HiFuonMm1ZjoKtIo7TH68/uiEq4DAwv9NcUX2nJ95F8TrbMeNjqIkZpruo3ojXQJ+MGG5gcQ=="],
|
||||||
|
|
||||||
|
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||||
|
|
||||||
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
|
|
||||||
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||||
|
|
||||||
|
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
|
||||||
|
|
||||||
|
"dts-resolver": ["dts-resolver@2.1.2", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg=="],
|
||||||
|
|
||||||
|
"empathic": ["empathic@1.1.0", "", {}, "sha512-rsPft6CK3eHtrlp9Y5ALBb+hfK+DWnA4WFebbazxjWyx8vSm3rZeoM3z9irsjcqO3PYRzlfv27XIB4tz2DV7RA=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||||
|
|
||||||
|
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||||
|
|
||||||
|
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
||||||
|
|
||||||
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
|
"nypm": ["nypm@0.6.1", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w=="],
|
||||||
|
|
||||||
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|
||||||
|
"opusscript": ["opusscript@0.1.1", "", {}, "sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA=="],
|
||||||
|
|
||||||
|
"package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||||
|
|
||||||
|
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||||
|
|
||||||
|
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"rolldown": ["rolldown@1.0.0-beta.9", "", { "dependencies": { "@oxc-project/types": "0.70.0", "@rolldown/pluginutils": "1.0.0-beta.9", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.9", "@rolldown/binding-darwin-x64": "1.0.0-beta.9", "@rolldown/binding-freebsd-x64": "1.0.0-beta.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9" }, "peerDependencies": { "@oxc-project/runtime": "0.70.0" }, "optionalPeers": ["@oxc-project/runtime"], "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZgZky52n6iF0UainGKjptKGrOG4Con2S5sdc4C4y2Oj25D5PHAY8Y8E5f3M2TSd/zlhQs574JlMeTe3vREczSg=="],
|
||||||
|
|
||||||
|
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.13.14", "", { "dependencies": { "@babel/generator": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/types": "^7.28.1", "ast-kit": "^2.1.1", "birpc": "^2.5.0", "debug": "^4.4.1", "dts-resolver": "^2.1.1", "get-tsconfig": "^4.10.1" }, "peerDependencies": { "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "^2.2.0 || ^3.0.0" }, "optionalPeers": ["@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"tsdown": ["tsdown@0.11.13", "", { "dependencies": { "ansis": "^4.0.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.1", "diff": "^8.0.1", "empathic": "^1.1.0", "hookable": "^5.5.3", "rolldown": "1.0.0-beta.9", "rolldown-plugin-dts": "^0.13.3", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.13", "unconfig": "^7.3.2" }, "peerDependencies": { "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.js" } }, "sha512-VSfoNm8MJXFdg7PJ4p2javgjMRiQQHpkP9N3iBBTrmCixcT6YZ9ZtqYMW3NDHczqR0C0Qnur1HMQr1ZfZcmrng=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||||
|
|
||||||
|
"unconfig": ["unconfig@7.3.3", "", { "dependencies": { "@quansync/fs": "^0.1.5", "defu": "^6.1.4", "jiti": "^2.5.1", "quansync": "^0.2.11" } }, "sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/discord/bunfig.toml
Normal file
10
packages/discord/bunfig.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[test]
|
||||||
|
coverage = true
|
||||||
|
coverageReporter = ["text", "lcov"]
|
||||||
|
coveragePathIgnorePatterns = [
|
||||||
|
"fixtures/**",
|
||||||
|
"dist/**"
|
||||||
|
]
|
||||||
|
|
||||||
|
[run]
|
||||||
|
bun = true
|
||||||
8
packages/discord/fixtures/commands/test1.command.ts
Normal file
8
packages/discord/fixtures/commands/test1.command.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { CommandHandler } from '@/commands/command-handler.type';
|
||||||
|
|
||||||
|
const handler: CommandHandler<{ name: string; type: 1; description: string }> = {
|
||||||
|
definition: { name: 'test1', type: 1, description: 'Test command 1' },
|
||||||
|
execute: async () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
8
packages/discord/fixtures/commands/test2.command.ts
Normal file
8
packages/discord/fixtures/commands/test2.command.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { CommandHandler } from '@/commands/command-handler.type';
|
||||||
|
|
||||||
|
const handler: CommandHandler<{ name: string; type: 1; description: string }> = {
|
||||||
|
definition: { name: 'test2', type: 1, description: 'Test command 2' },
|
||||||
|
execute: async () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
21
packages/discord/fixtures/jsd/test.ts
Normal file
21
packages/discord/fixtures/jsd/test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as StarKitten from '@star-kitten/discord';
|
||||||
|
import type { ExecutableInteraction } from '@star-kitten/discord';
|
||||||
|
import { createActionRow, createButton, createContainer, createTextDisplay } from '@star-kitten/discord/components';
|
||||||
|
import type { PageContext } from '@star-kitten/discord/pages';
|
||||||
|
import { type Appraisal } from '@star-kitten/eve/third-party/janice.js';
|
||||||
|
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
|
||||||
|
|
||||||
|
export function renderAppraisal(
|
||||||
|
appraisal: Appraisal,
|
||||||
|
pageCtx: PageContext<any>,
|
||||||
|
interaction: ExecutableInteraction,
|
||||||
|
) {
|
||||||
|
const formatter = new Intl.NumberFormat(interaction.locale || 'en-US', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
const world = 'world';
|
||||||
|
return (
|
||||||
|
StarKitten.createElement("ActionRow", {}, StarKitten.createElement("Container", {"color":"0x1da57a"}, StarKitten.createElement("TextDisplay", {}, ""+ `Hello ${world}` +""), pageCtx.state.currentPage !== "share" ? StarKitten.createElement("ActionRow", {}, StarKitten.createElement("Button", {"key":"share","disabled":"{!unknown}"}, "Share in Channel")) : undefined))
|
||||||
|
)
|
||||||
|
}
|
||||||
29
packages/discord/fixtures/jsd/test.tsx
Normal file
29
packages/discord/fixtures/jsd/test.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type {} from '@star-kitten/discord/jsx';
|
||||||
|
import { ActionRow, Container, Button, TextDisplay } from '@star-kitten/discord';
|
||||||
|
|
||||||
|
export function renderAppraisal() {
|
||||||
|
const formatter = new Intl.NumberFormat('en-US', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
const world = 'world';
|
||||||
|
const rand = Math.random() * 1000;
|
||||||
|
const pageCtx = { state: { currentPage: 'home' } };
|
||||||
|
|
||||||
|
let jsx = (
|
||||||
|
<ActionRow>
|
||||||
|
<Container color="0x1da57a">
|
||||||
|
<TextDisplay content={`Hello ${world}`} />
|
||||||
|
{pageCtx.state.currentPage !== 'share' ?
|
||||||
|
<ActionRow>
|
||||||
|
<Button customId="share" label="Share in Channel" disabled={rand < 500} />
|
||||||
|
</ActionRow>
|
||||||
|
: undefined}
|
||||||
|
</Container>
|
||||||
|
</ActionRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(jsx);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAppraisal();
|
||||||
69
packages/discord/index.d.ts
vendored
Normal file
69
packages/discord/index.d.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
type ActionRow,
|
||||||
|
type Button,
|
||||||
|
type ChannelSelectMenu,
|
||||||
|
type GuildChannelTypes,
|
||||||
|
type MentionableSelectMenu,
|
||||||
|
type PartialEmoji,
|
||||||
|
type RoleSelectMenu,
|
||||||
|
type StringSelectMenu,
|
||||||
|
type TextInput,
|
||||||
|
type UserSelectMenu,
|
||||||
|
type LabelComponent,
|
||||||
|
type ContainerComponent,
|
||||||
|
type TextDisplayComponent,
|
||||||
|
type SectionComponent,
|
||||||
|
type MediaGalleryComponent,
|
||||||
|
type SeparatorComponent,
|
||||||
|
type FileComponent,
|
||||||
|
type InteractionButton,
|
||||||
|
type URLButton,
|
||||||
|
type PremiumButton,
|
||||||
|
type ThumbnailComponent,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
declare namespace JSX {
|
||||||
|
type Component =
|
||||||
|
| ActionRow
|
||||||
|
| Button
|
||||||
|
| StringSelectMenu
|
||||||
|
| UserSelectMenu
|
||||||
|
| RoleSelectMenu
|
||||||
|
| MentionableSelectMenu
|
||||||
|
| ChannelSelectMenu
|
||||||
|
| TextInput
|
||||||
|
| LabelComponent
|
||||||
|
| ContainerComponent
|
||||||
|
| {
|
||||||
|
type: 10;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
| SectionComponent
|
||||||
|
| MediaGalleryComponent
|
||||||
|
| SeparatorComponent
|
||||||
|
| FileComponent
|
||||||
|
| InteractionButton
|
||||||
|
| URLButton
|
||||||
|
| PremiumButton
|
||||||
|
| ThumbnailComponent;
|
||||||
|
|
||||||
|
type Element = Component | Promise<Component>;
|
||||||
|
|
||||||
|
interface ElementClass {
|
||||||
|
render: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElementAttributesProperty {
|
||||||
|
props: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntrinsicElements {
|
||||||
|
// Allow any element, but prefer known elements
|
||||||
|
[elemName: string]: any;
|
||||||
|
// Known elements
|
||||||
|
ActionRow: { children: any | any[] };
|
||||||
|
Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean };
|
||||||
|
Container: { accent?: number; spoiler?: boolean; children: any | any[] };
|
||||||
|
TextDisplay: { content: string };
|
||||||
|
}
|
||||||
|
}
|
||||||
58
packages/discord/package.json
Normal file
58
packages/discord/package.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "@star-kitten/discord",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Star Kitten Discord library",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/author/library#readme",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/author/library/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/author/library.git"
|
||||||
|
},
|
||||||
|
"author": "Author Name <author.name@mail.com>",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.js",
|
||||||
|
"./commands": "./dist/commands/index.js",
|
||||||
|
"./components": "./dist/components/index.js",
|
||||||
|
"./pages": "./dist/pages/index.js",
|
||||||
|
"./common": "./dist/common/index.js",
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
"./jsx": "./src/jsx/jsx.ts",
|
||||||
|
"./jsx-runtime": "./dist/jsx/index.js",
|
||||||
|
"./jsx-dev-runtime": "./dist/jsx/index.js"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsdown",
|
||||||
|
"dev": "tsdown --watch",
|
||||||
|
"test": "bun test",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"release": "bumpp && npm publish"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.2.21",
|
||||||
|
"@types/node": "^22.15.17",
|
||||||
|
"bumpp": "^10.1.0",
|
||||||
|
"tsdown": "^0.11.9",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
|
||||||
|
"@star-kitten/util": "workspace:^0.0.0",
|
||||||
|
"acorn": "^8.14.0",
|
||||||
|
"acorn-jsx": "^5.3.2",
|
||||||
|
"html-dom-parser": "^5.1.1",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/discord/src/commands/command-context.type.ts
Normal file
14
packages/discord/src/commands/command-context.type.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Cache } from '@core/cache.type';
|
||||||
|
import type { KVStore } from '@core/kv-store.type.ts';
|
||||||
|
import type { Client } from '@projectdysnomia/dysnomia';
|
||||||
|
import type { CommandState } from './command-state';
|
||||||
|
|
||||||
|
export interface PartialContext<T = any> {
|
||||||
|
client: Client;
|
||||||
|
cache: Cache;
|
||||||
|
kv: KVStore;
|
||||||
|
id?: string; // unique id for this command instance
|
||||||
|
state?: CommandState<T>; // state associated with this command instance
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandContext<T = any> = Required<PartialContext<T>>;
|
||||||
32
packages/discord/src/commands/command-handler.ts
Normal file
32
packages/discord/src/commands/command-handler.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
AutocompleteInteraction,
|
||||||
|
CommandInteraction,
|
||||||
|
ComponentInteraction,
|
||||||
|
Constants,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
type ApplicationCommandOptionAutocomplete,
|
||||||
|
type ApplicationCommandOptions,
|
||||||
|
type ApplicationCommandStructure,
|
||||||
|
type ChatInputApplicationCommandStructure,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
import type { CommandContext, PartialContext } from './command-context.type';
|
||||||
|
|
||||||
|
export interface CommandHandler<T extends ApplicationCommandStructure> {
|
||||||
|
definition: T;
|
||||||
|
execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExecutableInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction | AutocompleteInteraction;
|
||||||
|
|
||||||
|
export type ChatCommandDefinition = Omit<ChatInputApplicationCommandStructure, 'type'>;
|
||||||
|
export function createChatCommand(
|
||||||
|
definition: ChatCommandDefinition,
|
||||||
|
execute: (interaction: CommandInteraction, ctx: CommandContext) => Promise<void>,
|
||||||
|
): CommandHandler<ChatInputApplicationCommandStructure> {
|
||||||
|
const def = definition as ChatInputApplicationCommandStructure;
|
||||||
|
def.type = 1; // CHAT_INPUT
|
||||||
|
return {
|
||||||
|
definition: def,
|
||||||
|
execute,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
packages/discord/src/commands/command-helpers.ts
Normal file
45
packages/discord/src/commands/command-helpers.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
Interaction,
|
||||||
|
CommandInteraction,
|
||||||
|
Constants,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
ComponentInteraction,
|
||||||
|
AutocompleteInteraction,
|
||||||
|
PingInteraction,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
import type { ExecutableInteraction } from './command-handler';
|
||||||
|
|
||||||
|
export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction {
|
||||||
|
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isModalSubmit(interaction: Interaction): interaction is ModalSubmitInteraction {
|
||||||
|
return interaction.type === Constants.InteractionTypes.MODAL_SUBMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMessageComponent(interaction: Interaction): interaction is ComponentInteraction {
|
||||||
|
return interaction.type === Constants.InteractionTypes.MESSAGE_COMPONENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAutocomplete(interaction: Interaction): interaction is AutocompleteInteraction {
|
||||||
|
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPing(interaction: Interaction): interaction is PingInteraction {
|
||||||
|
return interaction.type === Constants.InteractionTypes.PING;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commandHasName(interaction: Interaction, name: string): boolean {
|
||||||
|
return isApplicationCommand(interaction) && interaction.data.name === name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commandHasIdPrefix(interaction: Interaction, prefix: string): boolean {
|
||||||
|
return (isModalSubmit(interaction) || isMessageComponent(interaction)) && interaction.data.custom_id.startsWith(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommandName(interaction: ExecutableInteraction): string | undefined {
|
||||||
|
if (isApplicationCommand(interaction) || isAutocomplete(interaction)) {
|
||||||
|
return interaction.data.name;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
63
packages/discord/src/commands/command-injection.ts
Normal file
63
packages/discord/src/commands/command-injection.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { type InteractionModalContent, type Component } from '@projectdysnomia/dysnomia';
|
||||||
|
import type { CommandContext, PartialContext } from './command-context.type';
|
||||||
|
import { isApplicationCommand, isMessageComponent } from './command-helpers';
|
||||||
|
import type { ExecutableInteraction } from './command-handler';
|
||||||
|
|
||||||
|
export function injectInteraction(interaction: ExecutableInteraction, ctx: PartialContext): [ExecutableInteraction, CommandContext] {
|
||||||
|
// Wrap the interaction methods to inject command tracking ids into all custom_ids for modals and components.
|
||||||
|
if (ctx.state.name && (isApplicationCommand(interaction) || isMessageComponent(interaction))) {
|
||||||
|
const _originalCreateModal = interaction.createModal.bind(interaction);
|
||||||
|
interaction.createModal = (content: InteractionModalContent) => {
|
||||||
|
validateCustomIdLength(content.custom_id);
|
||||||
|
content.custom_id = `${content.custom_id}_${ctx.state.id}`;
|
||||||
|
return _originalCreateModal(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _originalCreateMessage = interaction.createMessage.bind(interaction);
|
||||||
|
interaction.createMessage = (content) => {
|
||||||
|
if (typeof content === 'string') return _originalCreateMessage(content);
|
||||||
|
if (content.components) {
|
||||||
|
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||||
|
}
|
||||||
|
return _originalCreateMessage(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _originalEditMessage = interaction.editMessage.bind(interaction);
|
||||||
|
interaction.editMessage = (messageID, content) => {
|
||||||
|
if (typeof content === 'string') return _originalEditMessage(messageID, content);
|
||||||
|
if (content.components) {
|
||||||
|
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||||
|
}
|
||||||
|
return _originalEditMessage(messageID, content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _originalCreateFollowup = interaction.createFollowup.bind(interaction);
|
||||||
|
interaction.createFollowup = (content) => {
|
||||||
|
if (typeof content === 'string') return _originalCreateFollowup(content);
|
||||||
|
if (content.components) {
|
||||||
|
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||||
|
}
|
||||||
|
return _originalCreateFollowup(content);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return [interaction, ctx as CommandContext];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCustomIdLength(customId: string) {
|
||||||
|
if (customId.length > 80) {
|
||||||
|
throw new Error(`Custom ID too long: ${customId.length} characters (max 80) with this framework. Consider using shorter IDs.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCommandIdToComponentCustomIds(components: Component[], commandId: string) {
|
||||||
|
components.forEach((component) => {
|
||||||
|
if (!component) return;
|
||||||
|
if ('custom_id' in component) {
|
||||||
|
validateCustomIdLength(component.custom_id as string);
|
||||||
|
component.custom_id = `${component.custom_id}_${commandId}`;
|
||||||
|
}
|
||||||
|
if ('components' in component && Array.isArray(component.components)) {
|
||||||
|
addCommandIdToComponentCustomIds(component.components, commandId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
56
packages/discord/src/commands/command-state.ts
Normal file
56
packages/discord/src/commands/command-state.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { createReactiveState } from '@star-kitten/util/reactive-state.js';
|
||||||
|
import type { PartialContext } from './command-context.type';
|
||||||
|
import { isApplicationCommand, isAutocomplete } from './command-helpers';
|
||||||
|
import type { ExecutableInteraction } from './command-handler';
|
||||||
|
|
||||||
|
export interface CommandState<T = any> {
|
||||||
|
id: string; // unique id for this command instance
|
||||||
|
name: string; // command name
|
||||||
|
data: T; // internal data storage
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCommandState<T>(interaction: ExecutableInteraction, ctx: PartialContext): Promise<CommandState<T>> {
|
||||||
|
const id = instanceIdFromInteraction(interaction);
|
||||||
|
|
||||||
|
let state: CommandState<T>;
|
||||||
|
|
||||||
|
// get state from kv store if possible
|
||||||
|
if (ctx.kv.has(`command-state:${id}`)) {
|
||||||
|
state = await ctx.kv.get<CommandState<T>>(`command-state:${id}`);
|
||||||
|
}
|
||||||
|
if (!state) {
|
||||||
|
state = { id: id, name: '', data: {} as T };
|
||||||
|
}
|
||||||
|
const [reactiveState, subscribe] = createReactiveState(state);
|
||||||
|
subscribe(async (newState) => {
|
||||||
|
if (ctx.kv) {
|
||||||
|
await ctx.kv.set(`command-state:${id}`, newState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.state = reactiveState;
|
||||||
|
return reactiveState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function instanceIdFromInteraction(interaction: ExecutableInteraction) {
|
||||||
|
if (isAutocomplete(interaction)) {
|
||||||
|
// autocomplete should not be stateful, they get no id
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isApplicationCommand(interaction)) {
|
||||||
|
// for application commands, we create a new instance id
|
||||||
|
const instance_id = crypto.randomUUID();
|
||||||
|
return instance_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interact = interaction;
|
||||||
|
const customId: string = interact.data.custom_id;
|
||||||
|
const commandId = customId.split('_').pop();
|
||||||
|
interaction;
|
||||||
|
// command id should be a uuid
|
||||||
|
if (commandId && /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(commandId)) {
|
||||||
|
return commandId;
|
||||||
|
}
|
||||||
|
console.error(`Invalid command id extracted from interaction: ${customId}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
59
packages/discord/src/commands/handle-commands.test.ts
Normal file
59
packages/discord/src/commands/handle-commands.test.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { expect, test, mock, beforeEach, afterEach } from 'bun:test';
|
||||||
|
import { handleCommands } from './handle-commands';
|
||||||
|
import { CommandInteraction, Constants, ModalSubmitInteraction, type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||||
|
import type { CommandHandler } from '../../dist';
|
||||||
|
|
||||||
|
let commands: Record<string, CommandHandler<ApplicationCommandStructure>>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
commands = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
commands = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.module('./command-helpers', () => ({
|
||||||
|
getCommandName: () => 'testCommand',
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('handleCommands executes command when interaction is CommandInteraction and command exists', async () => {
|
||||||
|
const mockExecute = mock(() => Promise.resolve());
|
||||||
|
const mockCommand = { definition: { name: 'testCommand' } as any, execute: mockExecute };
|
||||||
|
commands['testCommand'] = mockCommand;
|
||||||
|
|
||||||
|
const mockInteraction = {
|
||||||
|
type: Constants.InteractionTypes.APPLICATION_COMMAND,
|
||||||
|
data: { name: 'testCommand' },
|
||||||
|
} as any;
|
||||||
|
Object.setPrototypeOf(mockInteraction, CommandInteraction.prototype);
|
||||||
|
|
||||||
|
handleCommands(mockInteraction, commands, {} as any);
|
||||||
|
|
||||||
|
expect(mockExecute).toHaveBeenCalledWith(mockInteraction, expect.any(Object));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleCommands executes command when interaction is CommandInteraction and command exists', async () => {
|
||||||
|
const mockExecute = mock(() => Promise.resolve());
|
||||||
|
const mockCommand = { definition: { name: 'testCommand' } as any, execute: mockExecute };
|
||||||
|
commands['testCommand'] = mockCommand;
|
||||||
|
|
||||||
|
const mockInteraction = {
|
||||||
|
type: Constants.InteractionTypes.MODAL_SUBMIT,
|
||||||
|
data: { name: 'testCommand' },
|
||||||
|
} as any;
|
||||||
|
Object.setPrototypeOf(mockInteraction, ModalSubmitInteraction.prototype);
|
||||||
|
|
||||||
|
handleCommands(mockInteraction, commands, {} as any);
|
||||||
|
|
||||||
|
expect(mockExecute).toHaveBeenCalledWith(mockInteraction, expect.any(Object));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleCommands does nothing when interaction not a CommandInteraction, ModalSubmitInteraction, MessageComponentInteraction, or AutoCompleteInteraction', () => {
|
||||||
|
const mockInteraction = {
|
||||||
|
instanceof: (cls: any) => false,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Should not throw or do anything
|
||||||
|
expect(() => handleCommands(mockInteraction, commands, {} as any)).not.toThrow();
|
||||||
|
});
|
||||||
73
packages/discord/src/commands/handle-commands.ts
Normal file
73
packages/discord/src/commands/handle-commands.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||||
|
import { getCommandName, isApplicationCommand, isAutocomplete, isMessageComponent, isModalSubmit } from './command-helpers';
|
||||||
|
import type { PartialContext } from './command-context.type';
|
||||||
|
import type { CommandHandler, ExecutableInteraction } from './command-handler';
|
||||||
|
import { injectInteraction } from './command-injection';
|
||||||
|
import { getCommandState } from './command-state';
|
||||||
|
|
||||||
|
export async function handleCommands(
|
||||||
|
interaction: ExecutableInteraction,
|
||||||
|
commands: Record<string, CommandHandler<ApplicationCommandStructure>>,
|
||||||
|
ctx: PartialContext,
|
||||||
|
) {
|
||||||
|
ctx.state = await getCommandState(interaction, ctx);
|
||||||
|
if (!ctx.state.name) {
|
||||||
|
ctx.state.name = getCommandName(interaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAutocomplete(interaction) && ctx.state.name) {
|
||||||
|
const acCommand = commands[ctx.state.name];
|
||||||
|
return acCommand.execute(interaction, ctx as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.state.id) {
|
||||||
|
console.error(`No command ID found for interaction ${interaction.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = commands[ctx.state.name || ''];
|
||||||
|
if (!command) {
|
||||||
|
console.warn(`No command found for interaction: ${JSON.stringify(interaction, undefined, 2)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cleanInteractionCustomIds(interaction, ctx.state.id);
|
||||||
|
const [injectedInteraction, fullContext] = await injectInteraction(interaction, ctx);
|
||||||
|
return command.execute(injectedInteraction, fullContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) {
|
||||||
|
ctx.client.on('interactionCreate', async (interaction) => {
|
||||||
|
if (isApplicationCommand(interaction) || isModalSubmit(interaction) || isMessageComponent(interaction) || isAutocomplete(interaction)) {
|
||||||
|
handleCommands(interaction, commands, ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanInteractionCustomIds(interaction: ExecutableInteraction, id: string) {
|
||||||
|
if ('components' in interaction && Array.isArray(interaction.components) && id) {
|
||||||
|
removeCommandIdFromComponentCustomIds(interaction.components, id);
|
||||||
|
}
|
||||||
|
if ('data' in interaction && id) {
|
||||||
|
if ('custom_id' in interaction.data && typeof interaction.data.custom_id === 'string') {
|
||||||
|
interaction.data.custom_id = interaction.data.custom_id.replace(`_${id}`, '');
|
||||||
|
}
|
||||||
|
if ('components' in interaction.data && Array.isArray(interaction.data.components)) {
|
||||||
|
removeCommandIdFromComponentCustomIds(interaction.data.components as any, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCommandIdFromComponentCustomIds(components: { custom_id?: string; components?: any[] }[], commandId: string) {
|
||||||
|
components.forEach((component) => {
|
||||||
|
if ('custom_id' in component) {
|
||||||
|
component.custom_id = component.custom_id.replace(`_${commandId}`, '');
|
||||||
|
}
|
||||||
|
if ('components' in component && Array.isArray(component.components)) {
|
||||||
|
removeCommandIdFromComponentCustomIds(component.components, commandId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('component' in component && 'custom_id' in (component as any).component && Array.isArray(component.components)) {
|
||||||
|
(component.component as any).custom_id = (component.component as any).custom_id.replace(`_${commandId}`, '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
19
packages/discord/src/commands/import-commands.test.ts
Normal file
19
packages/discord/src/commands/import-commands.test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { expect, test, mock } from 'bun:test';
|
||||||
|
import { importCommands } from './import-commands';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
test('importCommands imports commands from files matching pattern', async () => {
|
||||||
|
const commands = await importCommands('**/*.command.{js,ts}', path.join(__dirname, '../../fixtures'));
|
||||||
|
|
||||||
|
expect(commands).toHaveProperty('test1');
|
||||||
|
expect(commands).toHaveProperty('test2');
|
||||||
|
expect(commands.test1.definition.name).toBe('test1');
|
||||||
|
expect(commands.test2.definition.name).toBe('test2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('importCommands uses default pattern and baseDir', async () => {
|
||||||
|
const commands = await importCommands();
|
||||||
|
|
||||||
|
// Since there are no command files in src, it should be empty
|
||||||
|
expect(commands).toEqual({});
|
||||||
|
});
|
||||||
19
packages/discord/src/commands/import-commands.ts
Normal file
19
packages/discord/src/commands/import-commands.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Glob } from 'bun';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import type { CommandHandler } from './command-handler';
|
||||||
|
import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
export async function importCommands(
|
||||||
|
pattern: string = '**/*.command.{js,ts}',
|
||||||
|
baseDir: string = join(process.cwd(), 'src'),
|
||||||
|
commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {},
|
||||||
|
): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> {
|
||||||
|
const glob = new Glob(pattern);
|
||||||
|
|
||||||
|
for await (const file of glob.scan({ cwd: baseDir, absolute: true })) {
|
||||||
|
const command = (await import(file)).default as CommandHandler<ApplicationCommandStructure>;
|
||||||
|
commandRegistry[command.definition.name] = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandRegistry;
|
||||||
|
}
|
||||||
8
packages/discord/src/commands/index.ts
Normal file
8
packages/discord/src/commands/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export * from './command-handler';
|
||||||
|
export * from './import-commands';
|
||||||
|
export * from './handle-commands';
|
||||||
|
export * from './command-helpers';
|
||||||
|
export * from './register-commands';
|
||||||
|
export * from './command-context.type';
|
||||||
|
export * from './command-state';
|
||||||
|
export * from './option-builders';
|
||||||
80
packages/discord/src/commands/option-builders.ts
Normal file
80
packages/discord/src/commands/option-builders.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
Constants,
|
||||||
|
type ApplicationCommandOptions,
|
||||||
|
type ApplicationCommandOptionsBoolean,
|
||||||
|
type ApplicationCommandOptionsInteger,
|
||||||
|
type ApplicationCommandOptionsMentionable,
|
||||||
|
type ApplicationCommandOptionsNumber,
|
||||||
|
type ApplicationCommandOptionsRole,
|
||||||
|
type ApplicationCommandOptionsString,
|
||||||
|
type ApplicationCommandOptionsSubCommand,
|
||||||
|
type ApplicationCommandOptionsSubCommandGroup,
|
||||||
|
type ApplicationCommandOptionsUser,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
export type StringOptionDefinition = Omit<ApplicationCommandOptionsString, 'type'> & { autocomplete?: boolean };
|
||||||
|
export function stringOption(options: StringOptionDefinition): ApplicationCommandOptionsString {
|
||||||
|
const def = options as ApplicationCommandOptionsString;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.STRING;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type IntegerOptionDefinition = Omit<ApplicationCommandOptionsInteger, 'type'> & { autocomplete?: boolean };
|
||||||
|
export function integerOption(options: IntegerOptionDefinition): ApplicationCommandOptionsInteger {
|
||||||
|
const def = options as ApplicationCommandOptionsInteger;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.INTEGER;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type BooleanOptionDefinition = Omit<ApplicationCommandOptionsBoolean, 'type'>;
|
||||||
|
export function booleanOption(options: BooleanOptionDefinition): ApplicationCommandOptionsBoolean {
|
||||||
|
const def = options as ApplicationCommandOptionsBoolean;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.BOOLEAN;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type UserOptionDefinition = Omit<ApplicationCommandOptionsUser, 'type'> & { autocomplete?: boolean };
|
||||||
|
export function userOption(options: UserOptionDefinition): ApplicationCommandOptionsUser {
|
||||||
|
const def = options as ApplicationCommandOptionsUser;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.USER;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type ChannelOptionDefinition = Omit<ApplicationCommandOptions, 'type'> & { autocomplete?: boolean };
|
||||||
|
export function channelOption(options: ChannelOptionDefinition): ApplicationCommandOptions {
|
||||||
|
const def = options as ApplicationCommandOptions;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.CHANNEL;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type RoleOptionDefinition = Omit<ApplicationCommandOptionsRole, 'type'> & { autocomplete?: boolean };
|
||||||
|
export function roleOption(options: RoleOptionDefinition): ApplicationCommandOptionsRole {
|
||||||
|
const def = options as ApplicationCommandOptionsRole;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.ROLE;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type MentionableOptionDefinition = Omit<ApplicationCommandOptionsMentionable, 'type'>;
|
||||||
|
export function mentionableOption(options: MentionableOptionDefinition): ApplicationCommandOptionsMentionable {
|
||||||
|
const def = options as ApplicationCommandOptionsMentionable;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.MENTIONABLE;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type NumberOptionDefinition = Omit<ApplicationCommandOptionsNumber, 'type'> & { autocomplete?: boolean };
|
||||||
|
export function numberOption(options: NumberOptionDefinition): ApplicationCommandOptionsNumber {
|
||||||
|
const def = options as ApplicationCommandOptionsNumber;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.NUMBER;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type AttachmentOptionDefinition = Omit<ApplicationCommandOptions, 'type'>;
|
||||||
|
export function attachmentOption(options: AttachmentOptionDefinition): ApplicationCommandOptions {
|
||||||
|
const def = options as ApplicationCommandOptions;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.ATTACHMENT;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type SubCommandOptionDefinition = Omit<ApplicationCommandOptionsSubCommand, 'type'>;
|
||||||
|
export function subCommandOption(options: SubCommandOptionDefinition): ApplicationCommandOptionsSubCommand {
|
||||||
|
const def = options as ApplicationCommandOptionsSubCommand;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
export type SubCommandGroupOptionDefinition = Omit<ApplicationCommandOptionsSubCommandGroup, 'type'>;
|
||||||
|
export function subCommandGroupOption(options: SubCommandGroupOptionDefinition): ApplicationCommandOptionsSubCommandGroup {
|
||||||
|
const def = options as ApplicationCommandOptionsSubCommandGroup;
|
||||||
|
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND_GROUP;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
11
packages/discord/src/commands/register-commands.ts
Normal file
11
packages/discord/src/commands/register-commands.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { ApplicationCommandStructure, Client } from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
export async function registerCommands(client: Client, commands: ApplicationCommandStructure[]) {
|
||||||
|
if (!client) throw new Error('Client not initialized');
|
||||||
|
if (!(await client.getCommands()).length || process.env.RESET_COMMANDS === 'true' || process.env.NODE_ENV === 'development') {
|
||||||
|
console.debug('Registering commands...');
|
||||||
|
const response = await client.bulkEditCommands(commands);
|
||||||
|
console.debug(`Registered ${response.length} commands.`);
|
||||||
|
}
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
1
packages/discord/src/common/index.ts
Normal file
1
packages/discord/src/common/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './text';
|
||||||
2
packages/discord/src/common/text.ts
Normal file
2
packages/discord/src/common/text.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const WHITE_SPACE = ' '; // non-breaking space
|
||||||
|
export const BREAKING_WHITE_SPACE = '\u200B';
|
||||||
314
packages/discord/src/components/builders.ts
Normal file
314
packages/discord/src/components/builders.ts
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
import {
|
||||||
|
Constants,
|
||||||
|
type ActionRow,
|
||||||
|
type Button,
|
||||||
|
type ChannelSelectMenu,
|
||||||
|
type GuildChannelTypes,
|
||||||
|
type MentionableSelectMenu,
|
||||||
|
type PartialEmoji,
|
||||||
|
type RoleSelectMenu,
|
||||||
|
type StringSelectMenu,
|
||||||
|
type TextInput,
|
||||||
|
type UserSelectMenu,
|
||||||
|
type LabelComponent,
|
||||||
|
type ContainerComponent,
|
||||||
|
type TextDisplayComponent,
|
||||||
|
type SectionComponent,
|
||||||
|
type MediaGalleryComponent,
|
||||||
|
type SeparatorComponent,
|
||||||
|
type FileComponent,
|
||||||
|
type InteractionButton,
|
||||||
|
type URLButton,
|
||||||
|
type PremiumButton,
|
||||||
|
type ThumbnailComponent,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
export type ActionRowItem = Button | StringSelectMenu | UserSelectMenu | RoleSelectMenu | MentionableSelectMenu | ChannelSelectMenu;
|
||||||
|
export const createActionRow = (...components: ActionRowItem[]): ActionRow => ({
|
||||||
|
type: Constants.ComponentTypes.ACTION_ROW,
|
||||||
|
components,
|
||||||
|
});
|
||||||
|
|
||||||
|
export enum ButtonStyle {
|
||||||
|
PRIMARY = 1,
|
||||||
|
SECONDARY = 2,
|
||||||
|
SUCCESS = 3,
|
||||||
|
DANGER = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonOptions {
|
||||||
|
style?: ButtonStyle;
|
||||||
|
emoji?: PartialEmoji;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createButton = (label: string, custom_id: string, options?: ButtonOptions): InteractionButton => ({
|
||||||
|
type: Constants.ComponentTypes.BUTTON,
|
||||||
|
style: options?.style ?? Constants.ButtonStyles.PRIMARY,
|
||||||
|
label,
|
||||||
|
custom_id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface URLButtonOptions {
|
||||||
|
emoji?: PartialEmoji;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createURLButton = (label: string, url: string, options?: URLButtonOptions): URLButton => ({
|
||||||
|
type: Constants.ComponentTypes.BUTTON,
|
||||||
|
style: Constants.ButtonStyles.LINK,
|
||||||
|
label,
|
||||||
|
url,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface PremiumButtonOptions {
|
||||||
|
emoji?: PartialEmoji;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPremiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({
|
||||||
|
type: Constants.ComponentTypes.BUTTON,
|
||||||
|
style: Constants.ButtonStyles.PREMIUM,
|
||||||
|
sku_id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface StringSelectOpts {
|
||||||
|
placeholder?: string;
|
||||||
|
min_values?: number;
|
||||||
|
max_values?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean; // Note: not actually a property of StringSelectMenu, but useful for modals
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringSelectOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
description?: string;
|
||||||
|
emoji?: {
|
||||||
|
name?: string;
|
||||||
|
id?: string;
|
||||||
|
animated?: boolean;
|
||||||
|
};
|
||||||
|
default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createStringSelect = (
|
||||||
|
custom_id: string,
|
||||||
|
selectOpts: StringSelectOpts,
|
||||||
|
...options: StringSelectOption[]
|
||||||
|
): StringSelectMenu => ({
|
||||||
|
type: Constants.ComponentTypes.STRING_SELECT,
|
||||||
|
custom_id,
|
||||||
|
options,
|
||||||
|
placeholder: selectOpts.placeholder ?? '',
|
||||||
|
min_values: selectOpts.min_values ?? 1,
|
||||||
|
max_values: selectOpts.max_values ?? 1,
|
||||||
|
disabled: selectOpts.disabled ?? false,
|
||||||
|
required: selectOpts.required ?? false, // Note: not actually a property of StringSelectMenu, but useful for modals
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface TextInputOptions {
|
||||||
|
isParagraph?: boolean;
|
||||||
|
label?: string;
|
||||||
|
min_length?: number;
|
||||||
|
max_length?: number;
|
||||||
|
required?: boolean;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTextInput = (custom_id: string, options?: TextInputOptions): TextInput => ({
|
||||||
|
type: Constants.ComponentTypes.TEXT_INPUT,
|
||||||
|
custom_id,
|
||||||
|
style: options.isParagraph ? Constants.TextInputStyles.PARAGRAPH : Constants.TextInputStyles.SHORT,
|
||||||
|
label: options?.label ?? '',
|
||||||
|
min_length: options?.min_length ?? 0,
|
||||||
|
max_length: options?.max_length ?? 4000,
|
||||||
|
required: options?.required ?? false,
|
||||||
|
value: options?.value ?? '',
|
||||||
|
placeholder: options?.placeholder ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface UserSelectOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
min_values?: number;
|
||||||
|
max_values?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
default_values?: Array<{ id: string; type: 'user' }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUserSelect = (custom_id: string, options?: UserSelectOptions): UserSelectMenu => ({
|
||||||
|
type: Constants.ComponentTypes.USER_SELECT,
|
||||||
|
custom_id,
|
||||||
|
placeholder: options?.placeholder ?? '',
|
||||||
|
min_values: options?.min_values ?? 1,
|
||||||
|
max_values: options?.max_values ?? 1,
|
||||||
|
disabled: options?.disabled ?? false,
|
||||||
|
default_values: options?.default_values ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface RoleSelectOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
min_values?: number;
|
||||||
|
max_values?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
default_values?: Array<{ id: string; type: 'role' }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createRoleSelect = (custom_id: string, options?: RoleSelectOptions): RoleSelectMenu => ({
|
||||||
|
type: Constants.ComponentTypes.ROLE_SELECT,
|
||||||
|
custom_id,
|
||||||
|
placeholder: options?.placeholder ?? '',
|
||||||
|
min_values: options?.min_values ?? 1,
|
||||||
|
max_values: options?.max_values ?? 1,
|
||||||
|
disabled: options?.disabled ?? false,
|
||||||
|
default_values: options?.default_values ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface MentionableSelectOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
min_values?: number;
|
||||||
|
max_values?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
default_values?: Array<{ id: string; type: 'user' | 'role' }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMentionableSelect = (custom_id: string, options?: MentionableSelectOptions): MentionableSelectMenu => ({
|
||||||
|
type: Constants.ComponentTypes.MENTIONABLE_SELECT,
|
||||||
|
custom_id,
|
||||||
|
placeholder: options?.placeholder ?? '',
|
||||||
|
min_values: options?.min_values ?? 1,
|
||||||
|
max_values: options?.max_values ?? 1,
|
||||||
|
disabled: options?.disabled ?? false,
|
||||||
|
default_values: options?.default_values ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ChannelSelectOptions {
|
||||||
|
channel_types?: GuildChannelTypes[];
|
||||||
|
placeholder?: string;
|
||||||
|
min_values?: number;
|
||||||
|
max_values?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
default_values?: Array<{ id: string; type: 'channel' }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createChannelSelect = (custom_id: string, options?: ChannelSelectOptions): ChannelSelectMenu => ({
|
||||||
|
type: Constants.ComponentTypes.CHANNEL_SELECT,
|
||||||
|
custom_id,
|
||||||
|
channel_types: options?.channel_types ?? [],
|
||||||
|
placeholder: options?.placeholder ?? '',
|
||||||
|
min_values: options?.min_values ?? 1,
|
||||||
|
max_values: options?.max_values ?? 1,
|
||||||
|
disabled: options?.disabled ?? false,
|
||||||
|
default_values: options?.default_values ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface SectionOptions {
|
||||||
|
components: Array<TextDisplayComponent>;
|
||||||
|
accessory: Button | ThumbnailComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSection = (accessory: Button | ThumbnailComponent, ...components: Array<TextDisplayComponent>): SectionComponent => ({
|
||||||
|
type: Constants.ComponentTypes.SECTION,
|
||||||
|
accessory,
|
||||||
|
components,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a text display component where the text will be displayed similar to a message: supports markdown
|
||||||
|
* @param content The text content to display.
|
||||||
|
* @returns The created text display component.
|
||||||
|
*/
|
||||||
|
export const createTextDisplay = (content: string) => ({
|
||||||
|
type: Constants.ComponentTypes.TEXT_DISPLAY,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ThumbnailOptions {
|
||||||
|
media: {
|
||||||
|
url: string; // Supports arbitrary urls and attachment://<filename> references
|
||||||
|
};
|
||||||
|
description?: string;
|
||||||
|
spoiler?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createThumbnail = (url: string, description?: string, spoiler?: boolean): ThumbnailComponent => ({
|
||||||
|
type: Constants.ComponentTypes.THUMBNAIL,
|
||||||
|
media: {
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
description,
|
||||||
|
spoiler,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface MediaItem {
|
||||||
|
url: string; // Supports arbitrary urls and attachment://<filename> references
|
||||||
|
description?: string;
|
||||||
|
spoiler?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMediaGallery = (...items: MediaItem[]): MediaGalleryComponent => ({
|
||||||
|
type: Constants.ComponentTypes.MEDIA_GALLERY,
|
||||||
|
items: items.map((item) => ({
|
||||||
|
type: Constants.ComponentTypes.FILE,
|
||||||
|
media: { url: item.url },
|
||||||
|
description: item.description,
|
||||||
|
spoiler: item.spoiler,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface FileOptions {
|
||||||
|
url: string; // Supports only attachment://<filename> references
|
||||||
|
spoiler?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createFile = (url: string, spoiler?: boolean): FileComponent => ({
|
||||||
|
type: Constants.ComponentTypes.FILE,
|
||||||
|
file: {
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
spoiler,
|
||||||
|
});
|
||||||
|
|
||||||
|
export enum Padding {
|
||||||
|
SMALL = 1,
|
||||||
|
LARGE = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeparatorOptions {
|
||||||
|
divider?: boolean;
|
||||||
|
spacing?: Padding;
|
||||||
|
}
|
||||||
|
export const createSeparator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({
|
||||||
|
type: Constants.ComponentTypes.SEPARATOR,
|
||||||
|
divider,
|
||||||
|
spacing: spacing ?? Padding.SMALL,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ContainerOptions {
|
||||||
|
accent_color?: number;
|
||||||
|
spoiler?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContainerItems =
|
||||||
|
| ActionRow
|
||||||
|
| TextDisplayComponent
|
||||||
|
| SectionComponent
|
||||||
|
| MediaGalleryComponent
|
||||||
|
| SeparatorComponent
|
||||||
|
| FileComponent;
|
||||||
|
|
||||||
|
export const createContainer = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({
|
||||||
|
type: Constants.ComponentTypes.CONTAINER,
|
||||||
|
...options,
|
||||||
|
components,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createModalLabel = (label: string, component: TextInput | StringSelectMenu): LabelComponent => ({
|
||||||
|
type: Constants.ComponentTypes.LABEL,
|
||||||
|
label,
|
||||||
|
component,
|
||||||
|
});
|
||||||
23
packages/discord/src/components/helpers.ts
Normal file
23
packages/discord/src/components/helpers.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
Constants,
|
||||||
|
type ComponentBase,
|
||||||
|
type ModalSubmitInteractionDataLabelComponent,
|
||||||
|
type ModalSubmitInteractionDataStringSelectComponent,
|
||||||
|
type ModalSubmitInteractionDataTextInputComponent,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
export function isModalLabel(component: ComponentBase): component is ModalSubmitInteractionDataLabelComponent {
|
||||||
|
return component.type === Constants.ComponentTypes.LABEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isModalTextInput(component: ComponentBase): component is ModalSubmitInteractionDataTextInputComponent {
|
||||||
|
return component.type === Constants.ComponentTypes.TEXT_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isModalSelect(component: ComponentBase): component is ModalSubmitInteractionDataStringSelectComponent {
|
||||||
|
return component.type === Constants.ComponentTypes.STRING_SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function componentHasIdPrefix(component: ComponentBase, prefix: string): boolean {
|
||||||
|
return (isModalTextInput(component) || isModalSelect(component)) && component.custom_id.startsWith(prefix);
|
||||||
|
}
|
||||||
2
packages/discord/src/components/index.ts
Normal file
2
packages/discord/src/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './helpers';
|
||||||
|
export * from './builders';
|
||||||
54
packages/discord/src/core/bot.ts
Normal file
54
packages/discord/src/core/bot.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { importCommands, initializeCommandHandling, registerCommands } from '@commands';
|
||||||
|
import { Client } from '@projectdysnomia/dysnomia';
|
||||||
|
import kv, { asyncKV } from '@star-kitten/util/kv.js';
|
||||||
|
import type { KVStore } from './kv-store.type.ts';
|
||||||
|
import type { Cache } from './cache.type.ts';
|
||||||
|
|
||||||
|
export interface DiscordBotOptions {
|
||||||
|
token?: string;
|
||||||
|
intents?: number[];
|
||||||
|
commandPattern?: string;
|
||||||
|
commandBaseDir?: string;
|
||||||
|
keyStore?: KVStore;
|
||||||
|
cache?: Cache;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
onReady?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startDiscordBot({
|
||||||
|
token = process.env.DISCORD_BOT_TOKEN || '',
|
||||||
|
intents = [],
|
||||||
|
commandPattern = '**/*.command.{js,ts}',
|
||||||
|
commandBaseDir = 'src',
|
||||||
|
keyStore = asyncKV,
|
||||||
|
cache = kv,
|
||||||
|
onError,
|
||||||
|
onReady,
|
||||||
|
}: DiscordBotOptions = {}): Client {
|
||||||
|
const client = new Client(`Bot ${token}`, {
|
||||||
|
gateway: {
|
||||||
|
intents,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('ready', async () => {
|
||||||
|
console.debug(`Logged in as ${client.user?.username}#${client.user?.discriminator}`);
|
||||||
|
onReady?.();
|
||||||
|
const commands = await importCommands(commandPattern, commandBaseDir);
|
||||||
|
await registerCommands(
|
||||||
|
client,
|
||||||
|
Object.values(commands).map((cmd) => cmd.definition),
|
||||||
|
);
|
||||||
|
initializeCommandHandling(commands, { client, cache, kv: keyStore });
|
||||||
|
console.debug('Bot is ready and command handling is initialized.');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (error) => {
|
||||||
|
console.error('An error occurred:', error);
|
||||||
|
onError?.(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.connect().catch(console.error);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
6
packages/discord/src/core/cache.type.ts
Normal file
6
packages/discord/src/core/cache.type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface Cache {
|
||||||
|
get: <T>(key: string) => T | undefined;
|
||||||
|
set: <T>(key: string, value: T, ttl?: number | string) => boolean;
|
||||||
|
del: (key: string | string[]) => number;
|
||||||
|
has: (key: string) => boolean;
|
||||||
|
}
|
||||||
3
packages/discord/src/core/index.ts
Normal file
3
packages/discord/src/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './bot';
|
||||||
|
export * from './cache.type';
|
||||||
|
export * from './kv-store.type.ts';
|
||||||
7
packages/discord/src/core/kv-store.type.ts.ts
Normal file
7
packages/discord/src/core/kv-store.type.ts.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface KVStore {
|
||||||
|
get: <T>(key: string) => Promise<T | undefined>;
|
||||||
|
set: (key: string, value: any) => Promise<boolean>;
|
||||||
|
delete: (key: string) => Promise<number>;
|
||||||
|
has: (key: string) => Promise<boolean>;
|
||||||
|
clear: () => Promise<void>;
|
||||||
|
}
|
||||||
4
packages/discord/src/index.ts
Normal file
4
packages/discord/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './locales';
|
||||||
|
export * from './commands';
|
||||||
|
export * from './core';
|
||||||
|
export * from './jsx';
|
||||||
7
packages/discord/src/jsd/createElement.ts
Normal file
7
packages/discord/src/jsd/createElement.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export function createElement(tag: string, attrs: Record<string, any> = {}, ...children: any[]) {
|
||||||
|
return {
|
||||||
|
tag,
|
||||||
|
attrs,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
}
|
||||||
2
packages/discord/src/jsd/index.ts
Normal file
2
packages/discord/src/jsd/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './parser';
|
||||||
|
export * from './createElement';
|
||||||
10
packages/discord/src/jsd/parser.test.ts
Normal file
10
packages/discord/src/jsd/parser.test.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
import { parseJSDFile } from './parser_new';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
describe('parseJSDFile', () => {
|
||||||
|
it('should parse a JSD file', async () => {
|
||||||
|
const result = await parseJSDFile(path.join(__dirname, '../../fixtures/jsd/test.tsd'));
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
97
packages/discord/src/jsd/parser.ts
Normal file
97
packages/discord/src/jsd/parser.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import parse, { type DOMNode } from 'html-dom-parser';
|
||||||
|
import type { ChildNode } from 'domhandler';
|
||||||
|
|
||||||
|
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
|
||||||
|
|
||||||
|
export async function parseJSDFile(filename: string) {
|
||||||
|
const content = (await fs.readFile(filename)).toString();
|
||||||
|
|
||||||
|
const matches = JSD_STRING.exec(content);
|
||||||
|
if (matches) {
|
||||||
|
let html = matches[1] + '>';
|
||||||
|
const root = parse(html);
|
||||||
|
const translated = translate(root[0]);
|
||||||
|
const str = content.replace(matches[1] + '>', translated);
|
||||||
|
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface state {
|
||||||
|
inInterpolation?: boolean;
|
||||||
|
children?: string[][];
|
||||||
|
parent?: Text[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function translate(root: DOMNode | ChildNode | null, state: state = {}): string | null {
|
||||||
|
if (!root || typeof root !== 'object') return null;
|
||||||
|
|
||||||
|
let children = [];
|
||||||
|
if ('children' in root && Array.isArray(root.children) && root.children.length > 0) {
|
||||||
|
for (const child of root.children) {
|
||||||
|
const translated = translate(child, state);
|
||||||
|
if (translated) {
|
||||||
|
if (state.inInterpolation && state.parent[state.children.length - 1] === child) {
|
||||||
|
state.children[state.children.length - 1].push(translated);
|
||||||
|
} else {
|
||||||
|
children.push(translated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('nodeType' in root && root.nodeType === 3) {
|
||||||
|
if (root.data.trim() === '') return null;
|
||||||
|
return parseText(root.data.trim(), state, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('name' in root && root.name) {
|
||||||
|
let tagName = root.name || 'unknown';
|
||||||
|
let attrs = 'attribs' in root ? root.attribs : {};
|
||||||
|
return `StarKitten.createElement("${tagName}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const JSD_INTERPOLATION = /\{(.+)\}/gs;
|
||||||
|
const JSD_START_EXP_INTERPOLATION = /\{(.+)\(/gs;
|
||||||
|
const JSD_END_EXP_INTERPOLATION = /\)(.+)\}/gs;
|
||||||
|
|
||||||
|
function parseText(text: string, state: state = {}, parent: Text = {}): string {
|
||||||
|
let interpolations = text.match(JSD_INTERPOLATION);
|
||||||
|
if (!interpolations) {
|
||||||
|
if (text.match(JSD_START_EXP_INTERPOLATION)) {
|
||||||
|
state.inInterpolation = true;
|
||||||
|
state.children = state.children || [[]];
|
||||||
|
state.parent = state.parent || [];
|
||||||
|
state.parent.push(parent);
|
||||||
|
return text.substring(1, text.length - 1);
|
||||||
|
} else if (text.match(JSD_END_EXP_INTERPOLATION)) {
|
||||||
|
const combined = state.children?.[state.children.length - 1].join(' ');
|
||||||
|
state.children?.[state.children.length - 1].splice(0);
|
||||||
|
state.children?.pop();
|
||||||
|
state.parent?.pop();
|
||||||
|
if (state.children.length === 0) {
|
||||||
|
state.inInterpolation = false;
|
||||||
|
return combined + ' ' + text.substring(1, text.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `"${text}"`;
|
||||||
|
} else {
|
||||||
|
text = replaceInterpolations(text);
|
||||||
|
return `"${text}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceInterpolations(text: string, isOnJSON: boolean = false) {
|
||||||
|
let interpolations = null;
|
||||||
|
|
||||||
|
while ((interpolations = JSD_INTERPOLATION.exec(text))) {
|
||||||
|
if (isOnJSON) {
|
||||||
|
text = text.replace(`"{${interpolations[1]}}"`, interpolations[1]);
|
||||||
|
} else {
|
||||||
|
text = text.replace(`{${interpolations[1]}}`, `"+ ${interpolations[1]} +"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
101
packages/discord/src/jsd/parser_new.ts
Normal file
101
packages/discord/src/jsd/parser_new.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import * as acorn from 'acorn';
|
||||||
|
import jsx from 'acorn-jsx';
|
||||||
|
|
||||||
|
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
|
||||||
|
|
||||||
|
const parser = acorn.Parser.extend(jsx());
|
||||||
|
|
||||||
|
export async function parseJSDFile(filename: string) {
|
||||||
|
const content = (await fs.readFile(filename)).toString();
|
||||||
|
|
||||||
|
const matches = JSD_STRING.exec(content);
|
||||||
|
if (matches) {
|
||||||
|
const jsxc = matches[1] + '>';
|
||||||
|
const ast = parser.parse(jsxc, { ecmaVersion: 2020, sourceType: 'module' });
|
||||||
|
const translated = traverseJSX((ast.body[0] as any).expression);
|
||||||
|
const str = content.replace(matches[1] + '>', translated);
|
||||||
|
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function traverseJSX(node: any): string {
|
||||||
|
if (node.type === 'JSXElement') {
|
||||||
|
const tag = node.openingElement.name.name;
|
||||||
|
const attrs: Record<string, any> = {};
|
||||||
|
for (const attr of node.openingElement.attributes) {
|
||||||
|
if (attr.type === 'JSXAttribute') {
|
||||||
|
const name = attr.name.name;
|
||||||
|
const value = attr.value;
|
||||||
|
if (value.type === 'Literal') {
|
||||||
|
attrs[name] = value.value;
|
||||||
|
} else if (value.type === 'JSXExpressionContainer') {
|
||||||
|
attrs[name] = `{${generateCode(value.expression)}}`;
|
||||||
|
} else if (value) {
|
||||||
|
attrs[name] = value.raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const children = [];
|
||||||
|
for (const child of node.children) {
|
||||||
|
const translated = traverseJSX(child);
|
||||||
|
if (translated) {
|
||||||
|
children.push(translated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `StarKitten.createElement("${tag}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
|
||||||
|
} else if (node.type === 'JSXExpressionContainer') {
|
||||||
|
const expr = generateCode(node.expression);
|
||||||
|
if (node.expression.type === 'TemplateLiteral' || (node.expression.type === 'Literal' && typeof node.expression.value === 'string')) {
|
||||||
|
return `""+ ${expr} +""`;
|
||||||
|
} else {
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
} else if (node.type === 'JSXText') {
|
||||||
|
const text = node.value.trim();
|
||||||
|
if (text) {
|
||||||
|
return `"${text}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCode(node: any): string {
|
||||||
|
if (node.type === 'JSXElement') {
|
||||||
|
return traverseJSX(node);
|
||||||
|
} else if (node.type === 'Identifier') {
|
||||||
|
return node.name;
|
||||||
|
} else if (node.type === 'Literal') {
|
||||||
|
return JSON.stringify(node.value);
|
||||||
|
} else if (node.type === 'TemplateLiteral') {
|
||||||
|
const quasis = node.quasis.map((q: any) => q.value.raw);
|
||||||
|
const expressions = node.expressions.map((e: any) => generateCode(e));
|
||||||
|
let result = quasis[0];
|
||||||
|
for (let i = 0; i < expressions.length; i++) {
|
||||||
|
result += '${' + expressions[i] + '}' + quasis[i + 1];
|
||||||
|
}
|
||||||
|
return '`' + result + '`';
|
||||||
|
} else if (node.type === 'MemberExpression') {
|
||||||
|
const op = node.optional ? '?.' : '.';
|
||||||
|
return generateCode(node.object) + op + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
|
||||||
|
} else if (node.type === 'OptionalMemberExpression') {
|
||||||
|
return generateCode(node.object) + '?.' + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
|
||||||
|
} else if (node.type === 'CallExpression') {
|
||||||
|
return generateCode(node.callee) + '(' + node.arguments.map((a: any) => generateCode(a)).join(', ') + ')';
|
||||||
|
} else if (node.type === 'BinaryExpression') {
|
||||||
|
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
|
||||||
|
} else if (node.type === 'ConditionalExpression') {
|
||||||
|
return generateCode(node.test) + ' ? ' + generateCode(node.consequent) + ' : ' + generateCode(node.alternate);
|
||||||
|
} else if (node.type === 'LogicalExpression') {
|
||||||
|
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
|
||||||
|
} else if (node.type === 'UnaryExpression') {
|
||||||
|
return node.operator + generateCode(node.argument);
|
||||||
|
} else if (node.type === 'ObjectExpression') {
|
||||||
|
return '{' + node.properties.map((p: any) => generateCode(p.key) + ': ' + generateCode(p.value)).join(', ') + '}';
|
||||||
|
} else if (node.type === 'ArrayExpression') {
|
||||||
|
return '[' + node.elements.map((e: any) => generateCode(e)).join(', ') + ']';
|
||||||
|
} else {
|
||||||
|
return node.raw || node.name || 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/discord/src/jsx/components/action-row.ts
Normal file
5
packages/discord/src/jsx/components/action-row.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createActionRow } from '@components';
|
||||||
|
|
||||||
|
export function ActionRow(props: { children: any | any[] }) {
|
||||||
|
return createActionRow(...(Array.isArray(props.children) ? props.children : [props.children]));
|
||||||
|
}
|
||||||
6
packages/discord/src/jsx/components/button.ts
Normal file
6
packages/discord/src/jsx/components/button.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createButton, type ButtonStyle } from '@components';
|
||||||
|
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
export function Button(props: { label: string; customId: string; style?: ButtonStyle; emoji?: PartialEmoji; disabled?: boolean }) {
|
||||||
|
return createButton(props.label, props.customId, { style: props.style, emoji: props.emoji, disabled: props.disabled });
|
||||||
|
}
|
||||||
8
packages/discord/src/jsx/components/container.ts
Normal file
8
packages/discord/src/jsx/components/container.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createContainer } from '@components';
|
||||||
|
|
||||||
|
export function Container(props: { accent?: number; spoiler?: boolean; children: any | any[] }) {
|
||||||
|
return createContainer(
|
||||||
|
{ accent_color: props.accent, spoiler: props.spoiler },
|
||||||
|
...(Array.isArray(props.children) ? props.children : [props.children]),
|
||||||
|
);
|
||||||
|
}
|
||||||
4
packages/discord/src/jsx/components/index.ts
Normal file
4
packages/discord/src/jsx/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './action-row';
|
||||||
|
export * from './button';
|
||||||
|
export * from './container';
|
||||||
|
export * from './text-display';
|
||||||
5
packages/discord/src/jsx/components/text-display.ts
Normal file
5
packages/discord/src/jsx/components/text-display.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createTextDisplay } from '@components/builders';
|
||||||
|
|
||||||
|
export function TextDisplay(props: { content: string }) {
|
||||||
|
return createTextDisplay(props.content);
|
||||||
|
}
|
||||||
3
packages/discord/src/jsx/index.ts
Normal file
3
packages/discord/src/jsx/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './runtime';
|
||||||
|
export * from './components';
|
||||||
|
export * as JSX from './jsx';
|
||||||
69
packages/discord/src/jsx/jsx.ts
Normal file
69
packages/discord/src/jsx/jsx.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
type ActionRow,
|
||||||
|
type Button,
|
||||||
|
type ChannelSelectMenu,
|
||||||
|
type MentionableSelectMenu,
|
||||||
|
type PartialEmoji,
|
||||||
|
type RoleSelectMenu,
|
||||||
|
type StringSelectMenu,
|
||||||
|
type TextInput,
|
||||||
|
type UserSelectMenu,
|
||||||
|
type LabelComponent,
|
||||||
|
type ContainerComponent,
|
||||||
|
type TextDisplayComponent,
|
||||||
|
type SectionComponent,
|
||||||
|
type MediaGalleryComponent,
|
||||||
|
type SeparatorComponent,
|
||||||
|
type FileComponent,
|
||||||
|
type InteractionButton,
|
||||||
|
type URLButton,
|
||||||
|
type PremiumButton,
|
||||||
|
type ThumbnailComponent,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
export type Component =
|
||||||
|
| ActionRow
|
||||||
|
| Button
|
||||||
|
| StringSelectMenu
|
||||||
|
| UserSelectMenu
|
||||||
|
| RoleSelectMenu
|
||||||
|
| MentionableSelectMenu
|
||||||
|
| ChannelSelectMenu
|
||||||
|
| TextInput
|
||||||
|
| LabelComponent
|
||||||
|
| ContainerComponent
|
||||||
|
| TextDisplayComponent
|
||||||
|
| SectionComponent
|
||||||
|
| MediaGalleryComponent
|
||||||
|
| SeparatorComponent
|
||||||
|
| FileComponent
|
||||||
|
| InteractionButton
|
||||||
|
| URLButton
|
||||||
|
| PremiumButton
|
||||||
|
| ThumbnailComponent;
|
||||||
|
|
||||||
|
export type Element = Component | Promise<Component>;
|
||||||
|
|
||||||
|
export interface ElementClass {
|
||||||
|
render: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElementAttributesProperty {
|
||||||
|
props: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IntrinsicElements {
|
||||||
|
// Allow any element, but prefer known elements
|
||||||
|
[elemName: string]: any;
|
||||||
|
// Known elements
|
||||||
|
ActionRow: { children: any | any[] };
|
||||||
|
Button: {
|
||||||
|
label: string;
|
||||||
|
customId: string;
|
||||||
|
style?: number;
|
||||||
|
emoji?: PartialEmoji;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
Container: { color?: string; accent?: number; spoiler?: boolean; children: any | any[] };
|
||||||
|
TextDisplay: { content: string };
|
||||||
|
}
|
||||||
30
packages/discord/src/jsx/runtime.ts
Normal file
30
packages/discord/src/jsx/runtime.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export function jsx(type: any, props: Record<string, any>) {
|
||||||
|
console.log('JSX', type, props);
|
||||||
|
if (typeof type === 'function') {
|
||||||
|
return type(props);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
props,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsxDEV(
|
||||||
|
type: any,
|
||||||
|
props: Record<string, any>,
|
||||||
|
key: string | number | symbol,
|
||||||
|
isStaticChildren: boolean,
|
||||||
|
source: any,
|
||||||
|
self: any,
|
||||||
|
) {
|
||||||
|
console.log('JSX DEV', type, props);
|
||||||
|
if (typeof type === 'function') {
|
||||||
|
return type(props);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
props: { ...props, key },
|
||||||
|
_source: source,
|
||||||
|
_self: self,
|
||||||
|
};
|
||||||
|
}
|
||||||
8
packages/discord/src/jsx/types.d.ts
vendored
Normal file
8
packages/discord/src/jsx/types.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { Component, IntrinsicElements as StarKittenIntrinsicElements } from './jsx';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
type Element = Component;
|
||||||
|
interface IntrinsicElements extends StarKittenIntrinsicElements {}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/discord/src/locales.ts
Normal file
26
packages/discord/src/locales.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export type Locales = 'en' | 'ru' | 'de' | 'fr' | 'ja' | 'es' | 'zh' | 'ko';
|
||||||
|
export const ALL_LOCALES: Locales[] = ['en', 'ru', 'de', 'fr', 'ja', 'es', 'zh', 'ko'];
|
||||||
|
export const DEFAULT_LOCALE: Locales = 'en';
|
||||||
|
export const LOCALE_NAMES: { [key in Locales]: string } = {
|
||||||
|
en: 'English',
|
||||||
|
ru: 'Русский',
|
||||||
|
de: 'Deutsch',
|
||||||
|
fr: 'Français',
|
||||||
|
ja: '日本語',
|
||||||
|
es: 'Español',
|
||||||
|
zh: '中文',
|
||||||
|
ko: '한국어',
|
||||||
|
};
|
||||||
|
export function toDiscordLocale(locale: Locales): string {
|
||||||
|
switch (locale) {
|
||||||
|
case 'en': return 'en-US';
|
||||||
|
case 'ru': return 'ru';
|
||||||
|
case 'de': return 'de';
|
||||||
|
case 'fr': return 'fr';
|
||||||
|
case 'ja': return 'ja';
|
||||||
|
case 'es': return 'es-ES';
|
||||||
|
case 'zh': return 'zh-CN';
|
||||||
|
case 'ko': return 'ko';
|
||||||
|
default: return 'en-US';
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/discord/src/pages/index.ts
Normal file
2
packages/discord/src/pages/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './pages';
|
||||||
|
export * from './subroutes';
|
||||||
166
packages/discord/src/pages/pages.ts
Normal file
166
packages/discord/src/pages/pages.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { isAutocomplete, isMessageComponent, isModalSubmit, isPing, type CommandContext } from '@commands';
|
||||||
|
import {
|
||||||
|
Constants,
|
||||||
|
type InteractionContentEdit,
|
||||||
|
type InteractionModalContent,
|
||||||
|
type CommandInteraction,
|
||||||
|
type ComponentInteraction,
|
||||||
|
type ModalSubmitInteraction,
|
||||||
|
Interaction,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
export type PagesInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction;
|
||||||
|
export enum PageType {
|
||||||
|
MODAL = 'modal',
|
||||||
|
MESSAGE = 'message',
|
||||||
|
FOLLOWUP = 'followup',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Page<T> {
|
||||||
|
key: string;
|
||||||
|
type?: PageType; // defaults to MESSAGE
|
||||||
|
followUpFlags?: number;
|
||||||
|
render: (
|
||||||
|
ctx: PageContext<T>,
|
||||||
|
) => (InteractionModalContent | InteractionContentEdit) | Promise<InteractionModalContent | InteractionContentEdit>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PagesOptions<T> {
|
||||||
|
pages: Record<string, Page<T>>;
|
||||||
|
initialPage?: string;
|
||||||
|
timeout?: number; // in seconds
|
||||||
|
ephemeral?: boolean; // whether the initial message should be ephemeral
|
||||||
|
useEmbeds?: boolean; // will not enable components v2
|
||||||
|
initialStateData?: T; // initial state to merge with default state
|
||||||
|
router?: (ctx: PageContext<T>) => string; // function to determine the next page key
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageState<T> {
|
||||||
|
currentPage: string;
|
||||||
|
timeoutAt: number; // timestamp in ms
|
||||||
|
lastInteractionAt?: number; // timestamp in ms
|
||||||
|
messageId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageContext<T> {
|
||||||
|
state: PageState<T>;
|
||||||
|
custom_id: string; // current interaction custom_id
|
||||||
|
interaction: PagesInteraction;
|
||||||
|
goToPage: (pageKey: string) => Promise<InteractionContentEdit>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPageContext<T>(interaction: PagesInteraction, options: PagesOptions<T>, state: PageState<T>): PageContext<T> {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
interaction,
|
||||||
|
custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : (options.initialPage ?? 'root'),
|
||||||
|
goToPage: (pageKey: string) => {
|
||||||
|
const page = options.pages[pageKey];
|
||||||
|
this.state.currentPage = pageKey;
|
||||||
|
if (!page) {
|
||||||
|
throw new Error(`Page with key "${pageKey}" not found`);
|
||||||
|
}
|
||||||
|
return page.render(createPageContext(interaction, options, { ...state, currentPage: pageKey })) as Promise<InteractionContentEdit>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultPageState<T>(options: PagesOptions<T>): PageState<T> {
|
||||||
|
const timeoutAt = options.timeout ? Date.now() + options.timeout * 1000 : Infinity;
|
||||||
|
return {
|
||||||
|
currentPage: options.initialPage ?? options.pages[0].key,
|
||||||
|
timeoutAt,
|
||||||
|
lastInteractionAt: Date.now(),
|
||||||
|
data: options.initialStateData ?? ({} as T),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageState<T>(options: PagesOptions<T>, cmdCtx: CommandContext & { state: { __pageState?: PageState<T> } }) {
|
||||||
|
const cmdState = cmdCtx.state;
|
||||||
|
if ('__pageState' in cmdState && cmdState.__pageState) {
|
||||||
|
return cmdState.__pageState as PageState<T>;
|
||||||
|
}
|
||||||
|
cmdState.__pageState = defaultPageState(options);
|
||||||
|
return cmdState.__pageState as PageState<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOptions<T>(options: PagesOptions<T>) {
|
||||||
|
const keys = Object.keys(options.pages);
|
||||||
|
const uniqueKeys = new Set(keys);
|
||||||
|
if (uniqueKeys.size !== keys.length) {
|
||||||
|
throw new Error('Duplicate page keys found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFlags(options: PagesOptions<any>) {
|
||||||
|
let flags = 0;
|
||||||
|
if (options.ephemeral) {
|
||||||
|
flags |= Constants.MessageFlags.EPHEMERAL;
|
||||||
|
}
|
||||||
|
if (!options.useEmbeds) {
|
||||||
|
flags |= Constants.MessageFlags.IS_COMPONENTS_V2;
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function usePages<T>(options: PagesOptions<T>, interaction: Interaction, cmdCtx: CommandContext) {
|
||||||
|
if (isAutocomplete(interaction) || isPing(interaction)) {
|
||||||
|
throw new Error('usePages cannot be used with autocomplete or ping interactions');
|
||||||
|
}
|
||||||
|
const pagesInteraction = interaction as PagesInteraction;
|
||||||
|
validateOptions(options);
|
||||||
|
const pageState = getPageState(options, cmdCtx);
|
||||||
|
const pageContext = createPageContext(pagesInteraction, options, pageState);
|
||||||
|
const pageKey =
|
||||||
|
options.router ? options.router(pageContext) : (pageContext.custom_id ?? options.initialPage ?? Object.keys(options.pages)[0]);
|
||||||
|
// if we have subroutes, we only want the main route from the page key
|
||||||
|
const page = options.pages[pageKey.split(':')[0]] ?? options.pages[0];
|
||||||
|
pageContext.state.currentPage = page.key;
|
||||||
|
|
||||||
|
if (page.type === PageType.MODAL && !isModalSubmit(pagesInteraction)) {
|
||||||
|
// we don't defer modals and can't respond to a modal with a modal.
|
||||||
|
const cnt = page.render(pageContext);
|
||||||
|
const content = isPromise(cnt) ? await cnt : cnt;
|
||||||
|
return await pagesInteraction.createModal(content as InteractionModalContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.type === PageType.FOLLOWUP) {
|
||||||
|
if (!pageState.messageId) {
|
||||||
|
throw new Error('Cannot send a followup message before an initial message has been sent');
|
||||||
|
}
|
||||||
|
const flags = page.type === PageType.FOLLOWUP ? (page.followUpFlags ?? getFlags(options)) : getFlags(options);
|
||||||
|
await pagesInteraction.defer(flags);
|
||||||
|
const cnt = page.render(pageContext);
|
||||||
|
const content = isPromise(cnt) ? await cnt : cnt;
|
||||||
|
return await pagesInteraction.createFollowup({
|
||||||
|
flags,
|
||||||
|
...(content as InteractionContentEdit),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageState.messageId && isMessageComponent(pagesInteraction)) {
|
||||||
|
await pagesInteraction.deferUpdate();
|
||||||
|
const cnt = page.render(pageContext);
|
||||||
|
const content = isPromise(cnt) ? await cnt : cnt;
|
||||||
|
return await pagesInteraction.editMessage(pageState.messageId, content as InteractionContentEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await pagesInteraction.defer(getFlags(options));
|
||||||
|
const cnt = page.render(pageContext);
|
||||||
|
const content = isPromise(cnt) ? await cnt : cnt;
|
||||||
|
const message = await pagesInteraction.createFollowup({
|
||||||
|
flags: getFlags(options),
|
||||||
|
...(content as InteractionContentEdit),
|
||||||
|
});
|
||||||
|
pageState.messageId = message.id;
|
||||||
|
pageState.channelId = message.channel?.id;
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
|
||||||
|
return typeof (value as Promise<T>)?.then === 'function';
|
||||||
|
}
|
||||||
99
packages/discord/src/pages/subroutes.ts
Normal file
99
packages/discord/src/pages/subroutes.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
|
||||||
|
import { createActionRow, createButton, createMediaGallery, type ButtonOptions, type ContainerItems } from '@components';
|
||||||
|
import type { PageContext } from './pages';
|
||||||
|
|
||||||
|
export function getSubrouteKey(prefix: string, subroutes: string[]) {
|
||||||
|
return `${prefix}:${subroutes.join(':')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSubrouteKey(key: string, expectedPrefix: string, expectedLength: number, defaults: string[] = []) {
|
||||||
|
const parts = key.split(':');
|
||||||
|
if (parts[0] !== expectedPrefix) {
|
||||||
|
throw new Error(`Unexpected prefix: ${parts[0]}`);
|
||||||
|
}
|
||||||
|
if (parts.length - 1 < expectedLength && defaults.length) {
|
||||||
|
// fill in defaults
|
||||||
|
parts.push(...defaults.slice(parts.length - 1));
|
||||||
|
}
|
||||||
|
if (parts.length !== expectedLength + 1) {
|
||||||
|
throw new Error(`Expected ${expectedLength} subroutes, but got ${parts.length - 1}`);
|
||||||
|
}
|
||||||
|
return parts.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSubrouteButtons(
|
||||||
|
currentSubroute: string,
|
||||||
|
subRoutes: string[],
|
||||||
|
subrouteIndex: number,
|
||||||
|
prefix: string,
|
||||||
|
subroutes: { label: string; value: string; emoji?: PartialEmoji }[],
|
||||||
|
options?: Partial<ButtonOptions>,
|
||||||
|
) {
|
||||||
|
return subroutes
|
||||||
|
.filter((sr) => sr !== undefined)
|
||||||
|
.map(({ label, value, emoji }) => {
|
||||||
|
const routes = [...subRoutes];
|
||||||
|
routes[subrouteIndex] = currentSubroute == value ? '_' : value;
|
||||||
|
return createButton(label, getSubrouteKey(prefix, routes), {
|
||||||
|
...options,
|
||||||
|
disabled: value === currentSubroute,
|
||||||
|
emoji,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubrouteOptions {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
emoji?: PartialEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSubroutes<T, CType = ContainerItems>(
|
||||||
|
context: PageContext<T>,
|
||||||
|
prefix: string,
|
||||||
|
subroutes: (SubrouteOptions & {
|
||||||
|
banner?: string;
|
||||||
|
actionRowPosition?: 'top' | 'bottom';
|
||||||
|
})[][],
|
||||||
|
render: (currentSubroute: string, ctx: PageContext<T>) => CType,
|
||||||
|
btnOptions?: Partial<ButtonOptions>,
|
||||||
|
defaultSubroutes?: string[], // if not provided, will use the first option of each subroute
|
||||||
|
): CType[] {
|
||||||
|
const currentSubroutes = parseSubrouteKey(
|
||||||
|
context.custom_id,
|
||||||
|
prefix,
|
||||||
|
subroutes.length,
|
||||||
|
defaultSubroutes || subroutes.map((s) => s[0].value),
|
||||||
|
);
|
||||||
|
|
||||||
|
const components = subroutes
|
||||||
|
.filter((sr) => sr.length > 0)
|
||||||
|
.map((srOpts, index) => {
|
||||||
|
const opts = srOpts.filter((sr) => sr !== undefined);
|
||||||
|
if (opts.length === 0) return undefined;
|
||||||
|
// find the current subroute, or default to the first
|
||||||
|
const sri = opts.findIndex((s) => s.value === currentSubroutes[index]);
|
||||||
|
const current = opts[sri] || opts[0];
|
||||||
|
const components = [];
|
||||||
|
|
||||||
|
const actionRow = createActionRow(...renderSubrouteButtons(current.value, currentSubroutes, index, prefix, opts, btnOptions));
|
||||||
|
|
||||||
|
if (current.banner) {
|
||||||
|
components.push(createMediaGallery({ url: current.banner }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current.actionRowPosition || current.actionRowPosition === 'top') {
|
||||||
|
components.push(actionRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
components.push(render(current.value, context));
|
||||||
|
|
||||||
|
if (current.actionRowPosition === 'bottom') {
|
||||||
|
components.push(actionRow);
|
||||||
|
}
|
||||||
|
return components;
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
.filter((c) => c !== undefined);
|
||||||
|
return components;
|
||||||
|
}
|
||||||
41
packages/discord/tsconfig.json
Normal file
41
packages/discord/tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "@star-kitten/discord",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
// Paths
|
||||||
|
"paths": {
|
||||||
|
"@*": ["./src/*"],
|
||||||
|
"@types": ["./types/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
|
||||||
|
"typeRoots": ["src/types", "./node_modules/@types"]
|
||||||
|
},
|
||||||
|
"include": ["src", "types", "src/jsx/types.d.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "build", "**/*.test.ts"]
|
||||||
|
}
|
||||||
17
packages/discord/tsdown.config.ts
Normal file
17
packages/discord/tsdown.config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'tsdown';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
entry: [
|
||||||
|
'./src/index.ts',
|
||||||
|
'./src/commands/index.ts',
|
||||||
|
'./src/components/index.ts',
|
||||||
|
'./src/pages/index.ts',
|
||||||
|
'./src/common/index.ts',
|
||||||
|
'./src/jsx/index.ts',
|
||||||
|
],
|
||||||
|
platform: 'node',
|
||||||
|
dts: true,
|
||||||
|
external: ['bun', 'bun:sqlite'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
65
packages/discord/types/index.d.ts
vendored
Normal file
65
packages/discord/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
type ActionRow,
|
||||||
|
type Button,
|
||||||
|
type ChannelSelectMenu,
|
||||||
|
type GuildChannelTypes,
|
||||||
|
type MentionableSelectMenu,
|
||||||
|
type PartialEmoji,
|
||||||
|
type RoleSelectMenu,
|
||||||
|
type StringSelectMenu,
|
||||||
|
type TextInput,
|
||||||
|
type UserSelectMenu,
|
||||||
|
type LabelComponent,
|
||||||
|
type ContainerComponent,
|
||||||
|
type TextDisplayComponent,
|
||||||
|
type SectionComponent,
|
||||||
|
type MediaGalleryComponent,
|
||||||
|
type SeparatorComponent,
|
||||||
|
type FileComponent,
|
||||||
|
type InteractionButton,
|
||||||
|
type URLButton,
|
||||||
|
type PremiumButton,
|
||||||
|
type ThumbnailComponent,
|
||||||
|
} from '@projectdysnomia/dysnomia';
|
||||||
|
|
||||||
|
declare namespace JSX {
|
||||||
|
type Component =
|
||||||
|
| Button
|
||||||
|
| StringSelectMenu
|
||||||
|
| UserSelectMenu
|
||||||
|
| RoleSelectMenu
|
||||||
|
| MentionableSelectMenu
|
||||||
|
| ChannelSelectMenu
|
||||||
|
| TextInput
|
||||||
|
| LabelComponent
|
||||||
|
| ContainerComponent
|
||||||
|
| TextDisplayComponent
|
||||||
|
| SectionComponent
|
||||||
|
| MediaGalleryComponent
|
||||||
|
| SeparatorComponent
|
||||||
|
| FileComponent
|
||||||
|
| InteractionButton
|
||||||
|
| URLButton
|
||||||
|
| PremiumButton
|
||||||
|
| ThumbnailComponent;
|
||||||
|
|
||||||
|
type Element = Component | Promise<Component>;
|
||||||
|
|
||||||
|
interface ElementClass {
|
||||||
|
render: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElementAttributesProperty {
|
||||||
|
props: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntrinsicElements {
|
||||||
|
// Allow any element, but prefer known elements
|
||||||
|
[elemName: string]: any;
|
||||||
|
// Known elements
|
||||||
|
ActionRow: { children: any | any[] };
|
||||||
|
Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean };
|
||||||
|
Container: { accent?: number; spoiler?: boolean; children: any | any[] };
|
||||||
|
TextDisplay: { content: string };
|
||||||
|
}
|
||||||
|
}
|
||||||
31
packages/eve-bot/.env.development
Normal file
31
packages/eve-bot/.env.development
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
|
||||||
|
#/ public-key encryption for .env files /
|
||||||
|
#/ [how it works](https://dotenvx.com/encryption) /
|
||||||
|
#/----------------------------------------------------------/
|
||||||
|
DOTENV_PUBLIC_KEY_DEVELOPMENT="02572da3d4f3a844588a944214c0e142a5a01deaa6551456af146d34b574024416"
|
||||||
|
|
||||||
|
# .env.development
|
||||||
|
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
|
||||||
|
#/ public-key encryption for .env files /
|
||||||
|
#/ [how it works](https://dotenvx.com/encryption) /
|
||||||
|
#/----------------------------------------------------------/
|
||||||
|
DOTENV_PUBLIC_KEY="02292a330aa041b5f7efc51504e0c208accba67a6877a217ab43cbb59c3c0c3e66"
|
||||||
|
|
||||||
|
# .env
|
||||||
|
DEBUG="true"
|
||||||
|
PORT="3001"
|
||||||
|
NODE_ENV="development"
|
||||||
|
LOG_LEVEL="debug"
|
||||||
|
BASE_URL="http://dev.starkitten.cafe"
|
||||||
|
EVE_CLIENT_ID="4cfee6b0c8fc40b29e0415f6309bd756"
|
||||||
|
EVE_CLIENT_SECRET="1Y5SBrdRMNuy9YWfwGMHsvUmTejqcQqDj4muuhOj"
|
||||||
|
EVE_CALLBACK_URL="http://dev.starkitten.cafe/api/auth/callback"
|
||||||
|
ESI_USER_AGENT="Star Kitten DEV/0.0.1 (jb@jb.codes; +https://github.com/roman-kaas/star-kitten)"
|
||||||
|
DISCORD_APP_ID="1292871047942504572"
|
||||||
|
DISCORD_APP_SECRET="Ioji-sKdo2hoCJe8A82M_c6AwRcuDphE"
|
||||||
|
DISCORD_PUBLIC_KEY="9ac29ee2d8170cb720caab3a382221aff577f82d7bfa03cd0dd737832053246c"
|
||||||
|
DISCORD_BOT_TOKEN="MTI5Mjg3MTA0Nzk0MjUwNDU3Mg.Gx8VvN.hrJMd94CZl0dHFhFS40lmB_ynNfJTC56QATHBg"
|
||||||
|
DISCORD_TEST_GUILD_ID="424296600773459998"
|
||||||
|
JANICE_KEY="DUyi5Q3Dod48IoswUBkEfNRs8Qf3cwNN"
|
||||||
|
PERPLEXITY_API_KEY="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"
|
||||||
|
STAR_KITTEN_KV_DB_PATH="data/dev-kv.db"
|
||||||
23
packages/eve-bot/.env.production
Normal file
23
packages/eve-bot/.env.production
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
|
||||||
|
#/ public-key encryption for .env files /
|
||||||
|
#/ [how it works](https://dotenvx.com/encryption) /
|
||||||
|
#/----------------------------------------------------------/
|
||||||
|
DOTENV_PUBLIC_KEY_PRODUCTION="02f0469506f6722d8fcc179c199ff159ca32f082000c8e7a1465891adb50a4c031"
|
||||||
|
|
||||||
|
# .env.production
|
||||||
|
DEBUG="encrypted:BPl4gO9RYUusva9u3Su7apXsjhWpaNHxRROXvRjxva6DA1/3o+meDrcFyoqvruR2PCNu3gOvI8TJ1xLUh16QaL2D88We/Q5HvGl7XsZrOadsK3TSY2jgECyKpKI8bz+f6HdIeCzJ"
|
||||||
|
PORT="encrypted:BO/LozOnJXfRaDydiiL3K+xUIFPWk9F9XKiVqEknmTR/dhOpW3nIKFp+0a64w2bxWY9wJ20Px4FEeceI3A5KZpUUbgHvTnjKXgNAh6eXYYtqJl2mToYSOWggv00fjhfcBeTP8mQ="
|
||||||
|
NODE_ENV="encrypted:BBWmfcjkSBbkCF0YxRSB6grqFNbsx3eQyieKJLnqrN9iIofkhXKBq5FQyiH5+xiQ7lfDhms2Vm6T9qu/D67oJBpsGY2qxB3r5uMvMzWPEwQ+8Sz0NVde5UYIfkOmN5n/ci77YH1ymiJpWPk="
|
||||||
|
LOG_LEVEL="encrypted:BIX0F+K4nCIdP0E2npM+gfrTGwBTOGPvwuDupRVfe77eZG2ix2GQFeTbPU8l1At1gWodqW179+8JvJslrRBuZUYK4bPuORvlgzQ9mrrLEqP1vCiCu5AzwXwHTTo/DfmHIm0z/dI="
|
||||||
|
BASE_URL="encrypted:BGbVLeMkYn+PXS0DJHAaPJKf64gQ8+WhlZOgevtMM9cw3oacEege6ic2DlPvpxP9F7h41cjy1KIvw3HHbR3yXuB2PlU/r6nZ7QL2pRBklYhkixAWVR7/ri9Ke3XR8OV68KXA9GsW2AS/r9ceG6Z6jd237JmMpLr+"
|
||||||
|
DISCORD_APP_ID="encrypted:BIsMPoPKxeByxEZpN3uBLUH/yar4YIFfxSHHwbqjs/TMmsz6oyNwGxhJyWyqoJOVeVGZ+ZsWKp6L7Hj75BqlmQynNPpKcOQNN55zAOJYkol3fWiNlEKmPLjgT7fKKlSnzFNfmdpqncOPUlT36WW8jvY+dd0="
|
||||||
|
DISCORD_APP_SECRET="encrypted:BJtSW4wyAbBSky5dqJqLI0Xb24JqDiRBcpnJAWVW+F0bVzCBGNaMMVn7XjCIgL9h1z09IBQquxRgo9kh5szSHHNO7J8a/5kYVMUrD6BPYWU5GcNRe6unInL0OlXzUFJ4PE9SxRBSErgU6PoO1jXfQYXNTa0B4SKTeEzvPfXFmOeu"
|
||||||
|
DISCORD_PUBLIC_KEY="encrypted:BJ/UXLOd1IikRwGNWC/PsAVhCPytbOP6XlC9S/8XJIacXz3GPukiCGEzTZoq4YTEwGwyP93yT5imIkiYS6xCAMSqPk9KzShNP8aDleZl2vSoCiW+QZ8WCVQBBmk5c/A/SuKPaLDWtjsfRlOnAkoIKYujxlmYL5UJV8MPA4Jz+ECsSPiZ8MOWbmRBeq9JAI+eERpDgjOyBgPw4V2JCLSOCvw="
|
||||||
|
DISCORD_BOT_TOKEN="encrypted:BOjPtcjzYCT47YeCYwnY6db53IGWHqUJvvolnxuR1D/008z5g1DEM21mRI2EMri1n0bNXqqmt2aQX4jNyUzEz2Tjmf4QTLim+L+XuGz1cm813ADYFzWNiCE+F45pu8xd2p/Ku0x8xGgkPrZSRm/jP3g8NXk80e1IfUd1WRCcSB6lDdYMl4GkYU+IKysPIf8FqaZMV6F6bjoYdsFkeAspZVJQECOqlLdWAg=="
|
||||||
|
EVE_CLIENT_ID="encrypted:BB+B4lgs4yMPla7a4PDtZfENuUQ2VmT6huvn9ySC6cDsxVM0HjXhziRK3/Yui23s129WIqksUi2Ywt9RIjLptWkOroP2CRA5hNLff+yzqTyZYrt35PVGdHHjkBmvcuxybrf2hzkIvkxOgvfi4WgR6Pl5N5ALTVUn3+6tJ5+p2aqJ"
|
||||||
|
EVE_CLIENT_SECRET="encrypted:BK/OQ/AA+DNvPhhIZQgXi7a4GIEo2Gc/aMfEiHGyNvLJn5WW5b2wCCKCwR6sNVUhEgAha6X73wT0rRxhGpSwPGe7IN0+JAb/1HYxZgFOCPegZHk1a8h2cadUcPDQYfOZeY2R+h/ldkz0KG5YteWD3A9iNNmTidYF3oszecESVTnMUuQ4NFKZsiQ="
|
||||||
|
EVE_CALLBACK_URL="encrypted:BNHPMxv+ozxFm7H9jOOECZyvpB+5JzA/8Y0EUtHq/v+r6V3uIKLPkrfDzqrccDvGqMcYrVNrexx9Iy/DbWa4lnxAhOyytcXhXRDKj4lGe5jOKj/VhDGvvBPmRHAdv7+kCXbohBz7fN5OE67ZIr8+x1L5oBojIBjSwmBKpnsXq9E3Q4+Q0qrudCJB"
|
||||||
|
ESI_USER_AGENT="encrypted:BEafFp9jiHx2EvlFmetxAwAcEkzc7qoN8vJc7I6wkuv2b+MB/eizjh24r33vyGrj92IJJV4numo8hLFnc2MfsUYOoVaE+f90Yh4OKkG9GUVqzNsI8psNJZ7nY0a7wURS5VM5bwbc6G98u0R4tc2HpGpMQPssu8lmRU5AsKnqSN5MTPp8R5ij3jEzMOpVZxxwnJgY+ViozHEQ2v1VK8dJ+C1BDFskAmb9iGSd6w=="
|
||||||
|
AUTH_DB_PATH="encrypted:BNZrtEfMB3JWWcXyIfaNme9cOXfMJULF+zqBxXK3sQW/aj5l6Q5s2lf1PFWlU88csMFjYqL57HbnjbBIAnR1VY0sNunGv1+gIOU6Dt2bOt0RBrqjVHaaPLst4IseDxBSsnYr+GHHEBlEP3R+EIbG7PWgDdQm73z9"
|
||||||
|
JANICE_KEY="encrypted:BIi+eFEI8t63MaSkW6ROvGS2zEuEbMosRJGVLvtLxAikPN7EWTa1KFwP1w44DV56Gx4HXBKBwLm4NlAxqjqpB0PJIPJyDy41evFZ0Nugua8dvOJ06VefMxNxvay33lvDybsfoeDuuu/OYi+zXur95B71f5bUUfuA296eAm/fT7X2"
|
||||||
|
PERPLEXITY_API_KEY="encrypted:BOXAcpkE+Qve39rIWzpIqBZBQAuLtIwUdyHguYmBOlrNdsJTRNC2vlqMLyA/B/SiMe1NfsDPEZNEB/vM0ue6SEugSh6trcZXqG06Gbl89bFxAiCVaz9j+qt+urcUivsr1sF44TNFZXIzSYMsO1O02PGqa7l/Ht153SKjaTOaJdCbtZMV6VqR6HX1TttPmqnXifugLZF4"
|
||||||
182
packages/eve-bot/.gitignore
vendored
Normal file
182
packages/eve-bot/.gitignore
vendored
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.env.keys
|
||||||
|
.flaskenv*
|
||||||
|
!.env.project
|
||||||
|
!.env.vault
|
||||||
|
|
||||||
|
data/
|
||||||
|
db/
|
||||||
|
litefs/
|
||||||
|
brainstorming/
|
||||||
|
|
||||||
|
# Sentry Config File
|
||||||
|
.sentryclirc
|
||||||
|
|
||||||
|
cloudflared.exe
|
||||||
5
packages/eve-bot/.prettierrc
Normal file
5
packages/eve-bot/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
8
packages/eve-bot/.prettierrc.yaml
Normal file
8
packages/eve-bot/.prettierrc.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
trailingComma: all
|
||||||
|
tabWidth: 2
|
||||||
|
useTabs: false
|
||||||
|
semi: true
|
||||||
|
singleQuote: true
|
||||||
|
printWidth: 140
|
||||||
|
experimentalTernaries: true
|
||||||
|
quoteProps: consistent
|
||||||
99
packages/eve-bot/README.md
Normal file
99
packages/eve-bot/README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Star Kitten Discord Bot
|
||||||
|
|
||||||
|
A Discord bot for [EVE Online](https://www.eveonline.com/).
|
||||||
|
|
||||||
|
# [Click this link to use this bot!](https://discord.com/oauth2/authorize?client_id=1288711114388930601)
|
||||||
|
|
||||||
|
## Running the Bot
|
||||||
|
|
||||||
|
This bot runs on [Bun](https://bun.sh/)! To install, run one of the following commands.
|
||||||
|
|
||||||
|
_Linux & MacOS_
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
_Windows_
|
||||||
|
|
||||||
|
```bash
|
||||||
|
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Install dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Link the Library & download static data
|
||||||
|
|
||||||
|
`star-kitten-lib` has not been published, so link to it locally before running this web project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd star-kitten-lib
|
||||||
|
bun link
|
||||||
|
cd ../web
|
||||||
|
bun link star-kitten-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download static eve reference data & Hoboleaks archive from [EVE Ref](https://everef.net/).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd star-kitten-lib
|
||||||
|
bun get-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initialize the sqlite database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd star-kitten-lib
|
||||||
|
bun generate-migrations
|
||||||
|
bun migrate
|
||||||
|
```
|
||||||
|
Drizzle's migrations seems to fail on the first try sometimes, so just grab the .sql from the generation and run those against the kitten.db file to create the tables & indexes.
|
||||||
|
|
||||||
|
### Run the bot
|
||||||
|
Run the bot locally.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Create a .env file in the root directory with the following values:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
#General
|
||||||
|
BASE_URL=http://localhost:3000
|
||||||
|
DEBUG=true
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
|
||||||
|
# EVE - https://developers.eveonline.com/applications
|
||||||
|
EVE_CLIENT_ID=YOUR_EVE_CLIENT_ID
|
||||||
|
EVE_CLIENT_SECRET=YOUR_EVE_SECRET
|
||||||
|
EVE_CALLBACK_URL=http://localhost:3000/auth/callback
|
||||||
|
ESI_USER_AGENT=ADD_YOUR_USER_AGENT_INFO_HERE
|
||||||
|
|
||||||
|
#Discord - https://discord.com/developers/applications
|
||||||
|
DISCORD_APP_ID=YOUR_APP_ID
|
||||||
|
DISCORD_CLIENT_SECRET=YOUR_CLIENT_SECRET
|
||||||
|
DISCORD_PUBLIC_KEY=YOUR_PUBLIC_KEY
|
||||||
|
DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN
|
||||||
|
|
||||||
|
# ID of a test server to have immediate command refreshes
|
||||||
|
DISCORD_TEST_GUILD_ID=YOUR_TEST_SERVER_ID
|
||||||
|
|
||||||
|
# For using Janice's Appraisal API
|
||||||
|
JANICE_KEY=XXX
|
||||||
|
|
||||||
|
# For using Perplexities AI API
|
||||||
|
PERPLEXITY_API_KEY=XXX
|
||||||
|
|
||||||
|
```
|
||||||
121
packages/eve-bot/bun.lock
Normal file
121
packages/eve-bot/bun.lock
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "star-kitten",
|
||||||
|
"dependencies": {
|
||||||
|
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dotenvx/dotenvx": "^1.49.0",
|
||||||
|
"@types/bun": "^1.2.21",
|
||||||
|
"@types/node": "^24.3.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.49.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-M1cyP6YstFQCjih54SAxCqHLMMi8QqV8tenpgGE48RTXWD7vfMYJiw/6xcCDpS2h28AcLpTsFCZA863Ge9yxzA=="],
|
||||||
|
|
||||||
|
"@ecies/ciphers": ["@ecies/ciphers@0.2.4", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
|
||||||
|
|
||||||
|
"@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||||
|
|
||||||
|
"@projectdysnomia/dysnomia": ["@projectdysnomia/dysnomia@github:projectdysnomia/dysnomia#5e3300e", { "dependencies": { "ws": "^8.18.0" }, "optionalDependencies": { "@stablelib/xchacha20poly1305": "~1.0.1", "opusscript": "^0.1.1" }, "peerDependencies": { "@discordjs/opus": "^0.9.0", "erlpack": "github:discord/erlpack", "eventemitter3": "^5.0.1", "pako": "^2.1.0", "sodium-native": "^4.1.1", "zlib-sync": "^0.1.9" }, "optionalPeers": ["@discordjs/opus", "erlpack", "eventemitter3", "pako", "sodium-native", "zlib-sync"] }, "projectdysnomia-dysnomia-5e3300e"],
|
||||||
|
|
||||||
|
"@stablelib/aead": ["@stablelib/aead@1.0.1", "", {}, "sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg=="],
|
||||||
|
|
||||||
|
"@stablelib/binary": ["@stablelib/binary@1.0.1", "", { "dependencies": { "@stablelib/int": "^1.0.1" } }, "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q=="],
|
||||||
|
|
||||||
|
"@stablelib/chacha": ["@stablelib/chacha@1.0.1", "", { "dependencies": { "@stablelib/binary": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg=="],
|
||||||
|
|
||||||
|
"@stablelib/chacha20poly1305": ["@stablelib/chacha20poly1305@1.0.1", "", { "dependencies": { "@stablelib/aead": "^1.0.1", "@stablelib/binary": "^1.0.1", "@stablelib/chacha": "^1.0.1", "@stablelib/constant-time": "^1.0.1", "@stablelib/poly1305": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA=="],
|
||||||
|
|
||||||
|
"@stablelib/constant-time": ["@stablelib/constant-time@1.0.1", "", {}, "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg=="],
|
||||||
|
|
||||||
|
"@stablelib/int": ["@stablelib/int@1.0.1", "", {}, "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w=="],
|
||||||
|
|
||||||
|
"@stablelib/poly1305": ["@stablelib/poly1305@1.0.1", "", { "dependencies": { "@stablelib/constant-time": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA=="],
|
||||||
|
|
||||||
|
"@stablelib/wipe": ["@stablelib/wipe@1.0.1", "", {}, "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg=="],
|
||||||
|
|
||||||
|
"@stablelib/xchacha20": ["@stablelib/xchacha20@1.0.1", "", { "dependencies": { "@stablelib/binary": "^1.0.1", "@stablelib/chacha": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-1YkiZnFF4veUwBVhDnDYwo6EHeKzQK4FnLiO7ezCl/zu64uG0bCCAUROJaBkaLH+5BEsO3W7BTXTguMbSLlWSw=="],
|
||||||
|
|
||||||
|
"@stablelib/xchacha20poly1305": ["@stablelib/xchacha20poly1305@1.0.1", "", { "dependencies": { "@stablelib/aead": "^1.0.1", "@stablelib/chacha20poly1305": "^1.0.1", "@stablelib/constant-time": "^1.0.1", "@stablelib/wipe": "^1.0.1", "@stablelib/xchacha20": "^1.0.1" } }, "sha512-B1Abj0sMJ8h3HNmGnJ7vHBrAvxuNka6cJJoZ1ILN7iuacXp7sUYcgOVEOTLWj+rtQMpspY9tXSCRLPmN1mQNWg=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||||
|
|
||||||
|
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
|
||||||
|
|
||||||
|
"eciesjs": ["eciesjs@0.4.15", "", { "dependencies": { "@ecies/ciphers": "^0.2.3", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0" } }, "sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA=="],
|
||||||
|
|
||||||
|
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||||
|
|
||||||
|
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||||
|
|
||||||
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
|
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||||
|
|
||||||
|
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||||
|
|
||||||
|
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||||
|
|
||||||
|
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||||
|
|
||||||
|
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
|
||||||
|
|
||||||
|
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||||
|
|
||||||
|
"opusscript": ["opusscript@0.1.1", "", {}, "sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||||
|
|
||||||
|
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
|
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/eve-bot/package.json
Normal file
33
packages/eve-bot/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "star-kitten",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "A Discord bot for Eve Online",
|
||||||
|
"author": "j-b-3",
|
||||||
|
"type": "module",
|
||||||
|
"module": "src/main.ts",
|
||||||
|
"devDependencies": {
|
||||||
|
"@dotenvx/dotenvx": "^1.49.0",
|
||||||
|
"@types/bun": "^1.2.21",
|
||||||
|
"@types/node": "^24.3.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
|
||||||
|
"@star-kitten/discord": "workspace:^0.0.0",
|
||||||
|
"@star-kitten/eve": "workspace:^0.0.0",
|
||||||
|
"@star-kitten/util": "workspace:^0.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bunx dotenvx run -f .env.development -- bun run --watch src/main.ts",
|
||||||
|
"start": "bunx @dotenvx/dotenvx run -f .env.production -- bun src/main.ts",
|
||||||
|
"build": "mkdirp ./db && bun build --minify --sourcemap src/main.ts --target bun --outdir ./dist",
|
||||||
|
"lint": "prettier --check .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"test": "bun test",
|
||||||
|
"get-data": "bun refresh:reference-data && bun refresh:hoboleaks && bun static-export",
|
||||||
|
"refresh:reference-data": "bun run ../util/dist/download-and-extract.js https://data.everef.net/reference-data/reference-data-latest.tar.xz ./data/reference-data",
|
||||||
|
"refresh:hoboleaks": "bun run ../util/dist/download-and-extract.js https://data.everef.net/hoboleaks-sde/hoboleaks-sde-latest.tar.xz ./data/hoboleaks",
|
||||||
|
"static-export": "bun run ../eve/scripts/export-solar-systems.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
90
packages/eve-bot/src/commands/appraise/appraise.command.ts
Normal file
90
packages/eve-bot/src/commands/appraise/appraise.command.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { Constants, type ChatInputApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||||
|
import { appraiseItems, type Appraisal } from '@star-kitten/eve/third-party/janice.js';
|
||||||
|
import { isModalSubmit } from '@star-kitten/discord/commands';
|
||||||
|
import { componentHasIdPrefix, isModalLabel, isModalSelect, isModalTextInput } from '@star-kitten/discord/components';
|
||||||
|
import type { CommandContext, ExecutableInteraction } from '@star-kitten/discord/commands';
|
||||||
|
import { PageType, usePages } from '@star-kitten/discord/pages';
|
||||||
|
import { renderAppraisal } from './renderAppraisal';
|
||||||
|
import { renderAppraisalModal } from './renderAppraisalModal';
|
||||||
|
|
||||||
|
const definition: ChatInputApplicationCommandStructure = {
|
||||||
|
type: Constants.ApplicationCommandTypes.CHAT_INPUT,
|
||||||
|
name: 'appraise',
|
||||||
|
nameLocalizations: {
|
||||||
|
de: 'bewerten',
|
||||||
|
'es-ES': 'tasar',
|
||||||
|
fr: 'estimer',
|
||||||
|
ja: '査定',
|
||||||
|
ko: '감정',
|
||||||
|
ru: 'оценить',
|
||||||
|
'zh-CN': '评估',
|
||||||
|
},
|
||||||
|
description: 'Evaluate the worth of your space junk',
|
||||||
|
descriptionLocalizations: {
|
||||||
|
de: 'Bewerten Sie den Wert Ihres Weltraumschrotts',
|
||||||
|
'es-ES': 'Evalúa el valor de tu chatarra espacial',
|
||||||
|
fr: 'Évaluez la valeur de vos déchets spatiaux',
|
||||||
|
ja: 'あなたの宇宙のガラクタの価値を評価します',
|
||||||
|
ko: '우주 쓰레기의 가치를 평가하십시오',
|
||||||
|
ru: 'Оцените стоимость вашего космического мусора',
|
||||||
|
'zh-CN': '评估您宇宙垃圾的价值',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AppraisalState {
|
||||||
|
appraisal?: Appraisal;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
|
||||||
|
return await usePages<AppraisalState>(
|
||||||
|
{
|
||||||
|
pages: {
|
||||||
|
appraiseModal: {
|
||||||
|
key: 'appraiseModal',
|
||||||
|
type: PageType.MODAL,
|
||||||
|
render: async () => renderAppraisalModal(interaction),
|
||||||
|
},
|
||||||
|
appraisalResult: {
|
||||||
|
key: 'appraisalResult',
|
||||||
|
render: async (pageCtx) => {
|
||||||
|
if (!isModalSubmit(interaction)) {
|
||||||
|
throw new Error('Expected a modal submit interaction for appraisalResult page');
|
||||||
|
}
|
||||||
|
let marketId = 2; // Default to Jita
|
||||||
|
let items = '';
|
||||||
|
|
||||||
|
interaction.data.components.forEach((comp) => {
|
||||||
|
if (isModalLabel(comp)) {
|
||||||
|
if (isModalSelect(comp.component) && componentHasIdPrefix(comp.component, `market`)) {
|
||||||
|
marketId = Number.parseInt(comp.component.values[0]) || marketId;
|
||||||
|
} else if (isModalTextInput(comp.component) && componentHasIdPrefix(comp.component, `input`)) {
|
||||||
|
items = comp.component.value || items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const appraisal = await appraiseItems(items, marketId);
|
||||||
|
pageCtx.state.data.appraisal = appraisal;
|
||||||
|
return renderAppraisal(appraisal, pageCtx, interaction);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
key: 'share',
|
||||||
|
type: PageType.FOLLOWUP,
|
||||||
|
followUpFlags: Constants.MessageFlags.IS_COMPONENTS_V2,
|
||||||
|
render: async (pageCtx) => renderAppraisal(pageCtx.state.data.appraisal!, pageCtx, interaction),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialPage: 'appraiseModal',
|
||||||
|
timeout: 300, // 5 minutes
|
||||||
|
ephemeral: true,
|
||||||
|
},
|
||||||
|
interaction,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
definition,
|
||||||
|
execute,
|
||||||
|
};
|
||||||
50
packages/eve-bot/src/commands/appraise/renderAppraisal.ts
Normal file
50
packages/eve-bot/src/commands/appraise/renderAppraisal.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { ExecutableInteraction } from '@star-kitten/discord';
|
||||||
|
import { createActionRow, createButton, createContainer, createTextDisplay } from '@star-kitten/discord/components';
|
||||||
|
import type { PageContext } from '@star-kitten/discord/pages';
|
||||||
|
import { type Appraisal } from '@star-kitten/eve/third-party/janice.js';
|
||||||
|
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
|
||||||
|
import type { AppraisalState } from './appraise.command';
|
||||||
|
|
||||||
|
export function renderAppraisal(
|
||||||
|
appraisal: Appraisal,
|
||||||
|
pageCtx: PageContext<AppraisalState>,
|
||||||
|
interaction: ExecutableInteraction,
|
||||||
|
) {
|
||||||
|
const formatter = new Intl.NumberFormat(interaction.locale || 'en-US', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = createContainer(
|
||||||
|
{
|
||||||
|
accent_color: 0x1da57a,
|
||||||
|
},
|
||||||
|
createTextDisplay(`
|
||||||
|
# [Appraisal ${appraisal.id} @ ${appraisal.market.name}](https://janice.e-351.com/a/${appraisal.id})
|
||||||
|
### Buy: \`${formatter.format(appraisal.effectivePrices.totalBuyPrice)}\` ISK
|
||||||
|
### Split: \`${formatter.format(appraisal.effectivePrices.totalSplitPrice)}\` ISK
|
||||||
|
### Sell: \`${formatter.format(appraisal.effectivePrices.totalSellPrice)}\` ISK
|
||||||
|
-# Volume: ${formatter.format(appraisal.totalPackagedVolume)} m³
|
||||||
|
\`\`\`
|
||||||
|
Buy: Sell: Qty: Item:
|
||||||
|
${appraisal.items.map((i) => `${formatNumberToShortForm(i.effectivePrices.buyPrice).padEnd(10)}${formatNumberToShortForm(i.effectivePrices.sellPrice).padEnd(10)}${formatNumberToShortForm(i.amount).padEnd(10)}${i.itemType.name}`).join('\n')}
|
||||||
|
\`\`\`
|
||||||
|
-# https://janice.e-351.com/a/${appraisal.id}\n\n
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pageCtx.state.currentPage !== 'share') {
|
||||||
|
container.components.push(
|
||||||
|
createActionRow(
|
||||||
|
createButton('Share in Channel', 'share', {
|
||||||
|
disabled: !interaction.channel?.id,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 1,
|
||||||
|
components: [container],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Interaction } from '@projectdysnomia/dysnomia';
|
||||||
|
import { createModalLabel, createStringSelect, createTextInput } from '@star-kitten/discord/components';
|
||||||
|
import { markets } from '@star-kitten/eve/third-party/janice.js';
|
||||||
|
|
||||||
|
export function renderAppraisalModal(interaction: Interaction) {
|
||||||
|
return {
|
||||||
|
// next page to render will be appraisalResult
|
||||||
|
custom_id: `appraisalResult`,
|
||||||
|
title: 'Appraise Items',
|
||||||
|
components: [
|
||||||
|
createModalLabel(
|
||||||
|
'Select your market (default: Jita)',
|
||||||
|
createStringSelect(
|
||||||
|
'market',
|
||||||
|
{
|
||||||
|
placeholder: 'Select a market',
|
||||||
|
},
|
||||||
|
...markets.map((m) => ({
|
||||||
|
label: m.name,
|
||||||
|
value: m.id.toString(),
|
||||||
|
default: m.id === 2, // Jita
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
createModalLabel(
|
||||||
|
'Enter items to appraise',
|
||||||
|
createTextInput('input', {
|
||||||
|
isParagraph: true,
|
||||||
|
placeholder: `Enter list of items to be appraised.
|
||||||
|
Tritanium 22222
|
||||||
|
Pyerite 8000
|
||||||
|
Mexallon 2444`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
98
packages/eve-bot/src/commands/search/_old_ItemLookup.ts
Normal file
98
packages/eve-bot/src/commands/search/_old_ItemLookup.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { ButtonStyle, ChatInputCommandInteraction, CommandInteraction, MessageFlags } from 'discord.js';
|
||||||
|
import { typeSearch } from './search';
|
||||||
|
import { createActionRow, useNavigation, type ResumeableInteraction } from '@lib/discord';
|
||||||
|
import { getTypeBlueprints, getTypeSchematics, getTypeSkills, getTypeVariants, typeHasAttributes, type Type } from 'star-kitten-lib/eve';
|
||||||
|
import { mainPage, attributesPage, fittingPage, skillsPage, industryPage } from './pages';
|
||||||
|
|
||||||
|
export enum PageKey {
|
||||||
|
MAIN = 'main',
|
||||||
|
ATTRIBUTES = 'attributes',
|
||||||
|
FITTING = 'fitting',
|
||||||
|
SKILLS = 'skills',
|
||||||
|
INDUSTRY = 'industry',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypeContext {
|
||||||
|
type: Type;
|
||||||
|
interaction: CommandInteraction;
|
||||||
|
disabled?: boolean;
|
||||||
|
buildButtonRow: (key: string, context: TypeContext) => any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemLookupOptions {
|
||||||
|
ephemeral: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function itemLookup(interaction: ChatInputCommandInteraction, options: ItemLookupOptions, saveResume: (messageId: string) => void) {
|
||||||
|
const deferred = await interaction.deferReply({ flags: options.ephemeral ? MessageFlags.Ephemeral : undefined });
|
||||||
|
const name = interaction.options.getString('name') ?? '';
|
||||||
|
|
||||||
|
await lookup(deferred.interaction as any, options, name, interaction.guild?.preferredLocale, saveResume);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resumeItemLookup(interaction: ResumeableInteraction, options: ItemLookupOptions, name: string, saveResume: (messageId: string) => void) {
|
||||||
|
await lookup(interaction, options, name, interaction.guild?.preferredLocale, saveResume);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function lookup(messageOrInteraction: ResumeableInteraction | ChatInputCommandInteraction, options: ItemLookupOptions, name: string, locale: string, saveResume: (messageId: string) => void) {
|
||||||
|
const type = await typeSearch(name);
|
||||||
|
if (!type) {
|
||||||
|
if (messageOrInteraction instanceof ChatInputCommandInteraction) {
|
||||||
|
messageOrInteraction.editReply({ content: `${options.type} ${name} not found` });
|
||||||
|
} else {
|
||||||
|
messageOrInteraction.message.edit({ content: `${options.type} ${name} not found` });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateContext = async (key: string, context: TypeContext) => {
|
||||||
|
return Promise.resolve(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildButtonRow = (key: string, context: TypeContext) => {
|
||||||
|
return createActionRow(
|
||||||
|
{ customId: PageKey.MAIN, label: 'Main', style: ButtonStyle.Primary, disabled: key === PageKey.MAIN },
|
||||||
|
typeHasAttributes(context.type) && {
|
||||||
|
customId: PageKey.ATTRIBUTES,
|
||||||
|
label: 'Attributes',
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
// disabled: key === PageKey.ATTRIBUTES,
|
||||||
|
},
|
||||||
|
typeHasAttributes(context.type) && {
|
||||||
|
customId: PageKey.FITTING,
|
||||||
|
label: `Fitting${getTypeVariants(context.type).length > 0 ? ' | Variants' : ''}`,
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
// disabled: key === PageKey.FITTING,
|
||||||
|
},
|
||||||
|
getTypeSkills(context.type)?.length > 0 && {
|
||||||
|
customId: PageKey.SKILLS,
|
||||||
|
label: 'Skills',
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
// disabled: key === PageKey.SKILLS,
|
||||||
|
},
|
||||||
|
(getTypeBlueprints(context.type)?.length > 0 || getTypeSchematics(context.type)?.length > 0) && {
|
||||||
|
customId: PageKey.INDUSTRY,
|
||||||
|
label: 'Industry',
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
// disabled: key === PageKey.INDUSTRY,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useNavigation({
|
||||||
|
interaction: messageOrInteraction,
|
||||||
|
key: 'main',
|
||||||
|
pages: [
|
||||||
|
mainPage(PageKey.MAIN, locale),
|
||||||
|
attributesPage(PageKey.ATTRIBUTES, locale),
|
||||||
|
fittingPage(PageKey.FITTING, locale),
|
||||||
|
skillsPage(PageKey.SKILLS, locale),
|
||||||
|
industryPage(PageKey.INDUSTRY, locale),
|
||||||
|
],
|
||||||
|
context: { type, buildButtonRow, interaction: messageOrInteraction },
|
||||||
|
updateContext,
|
||||||
|
saveResume,
|
||||||
|
});
|
||||||
|
}
|
||||||
189
packages/eve-bot/src/commands/search/pages/_old_attributes.ts
Normal file
189
packages/eve-bot/src/commands/search/pages/_old_attributes.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { renderThreeColumns, type Page } from '@lib/discord';
|
||||||
|
import { EmbedBuilder } from 'discord.js';
|
||||||
|
import {
|
||||||
|
attributeOrdering,
|
||||||
|
type Type,
|
||||||
|
CommonAttribute,
|
||||||
|
eveRefLink,
|
||||||
|
getTypeIconUrl,
|
||||||
|
typeHasAnyAttribute,
|
||||||
|
getGroup,
|
||||||
|
typeGetAttribute,
|
||||||
|
getUnit,
|
||||||
|
renderUnit,
|
||||||
|
} from 'star-kitten-lib/eve';
|
||||||
|
import type { PageKey, TypeContext } from '../_old_ItemLookup';
|
||||||
|
|
||||||
|
export function attributesPage(key: PageKey.ATTRIBUTES, locale: string = 'en'): Page<TypeContext> {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
content: async (context: TypeContext) => {
|
||||||
|
const type = context.type;
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(type.name[locale] ?? type.name.en)
|
||||||
|
.setThumbnail(getTypeIconUrl(type))
|
||||||
|
.setURL(eveRefLink(type.type_id))
|
||||||
|
.setFooter({ text: `id: ${type.type_id}` })
|
||||||
|
.setColor('Green');
|
||||||
|
|
||||||
|
const embeds = [embed];
|
||||||
|
const fields = [];
|
||||||
|
|
||||||
|
if (type.dogma_attributes) {
|
||||||
|
const useOrders =
|
||||||
|
getGroup(type.group_id).category_id === 11
|
||||||
|
? attributeOrdering['11']
|
||||||
|
: getGroup(type.group_id).category_id === 87
|
||||||
|
? attributeOrdering['87']
|
||||||
|
: attributeOrdering.default;
|
||||||
|
|
||||||
|
Object.entries(useOrders).map((pair) => {
|
||||||
|
const [attributePath, attrs] = pair;
|
||||||
|
const combined = attrs['groupedAttributes']
|
||||||
|
? attrs.normalAttributes.concat(...(attrs['groupedAttributes']?.map(([name, id]) => id) ?? []))
|
||||||
|
: attrs.normalAttributes;
|
||||||
|
if (!typeHasAnyAttribute(type, combined)) return;
|
||||||
|
const split = attributePath.split('/');
|
||||||
|
const name = split[split.length - 1];
|
||||||
|
fields.push(
|
||||||
|
...renderThreeColumns(
|
||||||
|
name,
|
||||||
|
getAttributeNames(type, combined, locale),
|
||||||
|
[],
|
||||||
|
getAttributeValues(type, combined, locale),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// for (const [name, attrs] of Object.entries(attrMap)) {
|
||||||
|
// if (!type.hasAnyAttribute(attrs)) continue;
|
||||||
|
// if (name === 'Cargo | Drones' && type.group.category.category_id === CommonCategory.MODULE) continue;
|
||||||
|
// fields.push(...renderThreeColumns(
|
||||||
|
// name,
|
||||||
|
// getAttributeNames(type, attrs, locale),
|
||||||
|
// [],
|
||||||
|
// getAttributeValues(type, attrs, locale)
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// there is a max number of 24 fields per embed
|
||||||
|
embed.addFields(fields.splice(0, 24));
|
||||||
|
while (fields.length > 0) {
|
||||||
|
embeds.push(new EmbedBuilder().addFields(fields.splice(0, 24)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'page',
|
||||||
|
embeds,
|
||||||
|
components: [context.buildButtonRow(key, context)],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const structureAttrs = [
|
||||||
|
CommonAttribute.StructureHitpoints,
|
||||||
|
CommonAttribute.Mass,
|
||||||
|
CommonAttribute.Volume,
|
||||||
|
CommonAttribute.InertiaModifier,
|
||||||
|
CommonAttribute.StructureEMResistance,
|
||||||
|
CommonAttribute.StructureThermalResistance,
|
||||||
|
CommonAttribute.StructureKineticResistance,
|
||||||
|
CommonAttribute.StructureExplosiveResistance,
|
||||||
|
];
|
||||||
|
|
||||||
|
const droneAttrs = [CommonAttribute.CargoCapacity, CommonAttribute.DroneBandwidth, CommonAttribute.DroneCapacity];
|
||||||
|
|
||||||
|
const armorAttrs = [
|
||||||
|
CommonAttribute.ArmorHitpoints,
|
||||||
|
CommonAttribute.ArmorEMResistance,
|
||||||
|
CommonAttribute.ArmorThermalResistance,
|
||||||
|
CommonAttribute.ArmorKineticResistance,
|
||||||
|
CommonAttribute.ArmorExplosiveResistance,
|
||||||
|
];
|
||||||
|
|
||||||
|
const shieldAttrs = [
|
||||||
|
CommonAttribute.ShieldCapacity,
|
||||||
|
CommonAttribute.ShieldRechargeTime,
|
||||||
|
CommonAttribute.ShieldEMResistance,
|
||||||
|
CommonAttribute.ShieldThermalResistance,
|
||||||
|
CommonAttribute.ShieldKineticResistance,
|
||||||
|
CommonAttribute.ShieldExplosiveResistance,
|
||||||
|
];
|
||||||
|
|
||||||
|
const elResAttrs = [
|
||||||
|
CommonAttribute.CapacitorWarfareResistance,
|
||||||
|
CommonAttribute.StasisWebifierResistance,
|
||||||
|
CommonAttribute.WeaponDisruptionResistance,
|
||||||
|
];
|
||||||
|
|
||||||
|
const capAttrs = [CommonAttribute.CapacitorCapacity, CommonAttribute.CapacitorRechargeTime];
|
||||||
|
|
||||||
|
const targetAttrs = [
|
||||||
|
CommonAttribute.MaxTargetRange,
|
||||||
|
CommonAttribute.MaxLockedTargets,
|
||||||
|
CommonAttribute.SignatureRadius,
|
||||||
|
CommonAttribute.ScanResolution,
|
||||||
|
CommonAttribute.RadarSensorStrength,
|
||||||
|
CommonAttribute.MagnetometricSensorStrength,
|
||||||
|
CommonAttribute.GravimetricSensorStrength,
|
||||||
|
CommonAttribute.LadarSensorStrength,
|
||||||
|
];
|
||||||
|
|
||||||
|
const jumpAttrs = [
|
||||||
|
CommonAttribute.JumpDriveCapacitorNeed,
|
||||||
|
CommonAttribute.MaxJumpRange,
|
||||||
|
CommonAttribute.JumpDriveFuelNeed,
|
||||||
|
CommonAttribute.JumpDriveConsumptionAmount,
|
||||||
|
CommonAttribute.FuelBayCapacity,
|
||||||
|
CommonAttribute.ConduitJumpConsumptionAmount,
|
||||||
|
CommonAttribute.COnduitJumpPassengerCapacity,
|
||||||
|
];
|
||||||
|
|
||||||
|
const propAttrs = [CommonAttribute.MaxVelocity, CommonAttribute.WarpSpeed];
|
||||||
|
|
||||||
|
const weaponAttrs = [
|
||||||
|
CommonAttribute.DamageMultiplier,
|
||||||
|
CommonAttribute.AccuracyFalloff,
|
||||||
|
CommonAttribute.OptimalRange,
|
||||||
|
CommonAttribute.RateOfFire,
|
||||||
|
CommonAttribute.TrackingSpeed,
|
||||||
|
CommonAttribute.ReloadTime,
|
||||||
|
CommonAttribute.ActivationTime,
|
||||||
|
CommonAttribute.ChargeSize,
|
||||||
|
CommonAttribute.UsedWithCharge1,
|
||||||
|
CommonAttribute.UsedWithCharge2,
|
||||||
|
];
|
||||||
|
|
||||||
|
const eWarAttrs = [CommonAttribute.MaxVelocityBonus];
|
||||||
|
|
||||||
|
const attrMap = {
|
||||||
|
Weapon: weaponAttrs,
|
||||||
|
Structure: structureAttrs,
|
||||||
|
Armor: armorAttrs,
|
||||||
|
Shield: shieldAttrs,
|
||||||
|
'Cargo | Drones': droneAttrs,
|
||||||
|
'Electronic Resistances': elResAttrs,
|
||||||
|
Capacitor: capAttrs,
|
||||||
|
Targeting: targetAttrs,
|
||||||
|
'Jump Drive Systems': jumpAttrs,
|
||||||
|
Propulsion: propAttrs,
|
||||||
|
'Electronic Warfare': eWarAttrs,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAttributeNames(type: Type, ids: number[], locale: string = 'en') {
|
||||||
|
return ids
|
||||||
|
.map((id) => typeGetAttribute(type, id))
|
||||||
|
.filter((attr) => !!attr)
|
||||||
|
.map((attr) => `> ${attr.attribute.display_name[locale] ?? attr.attribute.display_name.en}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAttributeValues(type: Type, ids: number[], locale: string = 'en') {
|
||||||
|
return ids
|
||||||
|
.map((id) => typeGetAttribute(type, id))
|
||||||
|
.filter((attr) => !!attr)
|
||||||
|
.map(
|
||||||
|
(attr) => `**${attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : attr.value}**`,
|
||||||
|
);
|
||||||
|
}
|
||||||
86
packages/eve-bot/src/commands/search/pages/_old_fitting.ts
Normal file
86
packages/eve-bot/src/commands/search/pages/_old_fitting.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { EmbedBuilder } from 'discord.js';
|
||||||
|
import { renderThreeColumns, type Page } from '@lib/discord';
|
||||||
|
import { getAttributeNames, getAttributeValues } from './_old_attributes';
|
||||||
|
import { PageKey, type TypeContext } from '../_old_ItemLookup';
|
||||||
|
import {
|
||||||
|
eveRefLink,
|
||||||
|
getTypeIconUrl,
|
||||||
|
getTypeVariants,
|
||||||
|
typeHasAnyAttribute,
|
||||||
|
CommonAttribute,
|
||||||
|
renderTypeEveRefLink,
|
||||||
|
} from 'star-kitten-lib/eve';
|
||||||
|
|
||||||
|
export function fittingPage(key: string = PageKey.FITTING, locale: string = 'en'): Page<TypeContext> {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
content: async (context: TypeContext) => {
|
||||||
|
const type = context.type;
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(type.name[locale] ?? type.name.en)
|
||||||
|
.setThumbnail(getTypeIconUrl(type))
|
||||||
|
.setURL(eveRefLink(type.type_id))
|
||||||
|
.setFooter({ text: `id: ${type.type_id}` })
|
||||||
|
.setColor('Green');
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
|
||||||
|
for (const [name, attrs] of Object.entries(attrMap)) {
|
||||||
|
if (!typeHasAnyAttribute(type, attrs)) continue;
|
||||||
|
fields.push(
|
||||||
|
...renderThreeColumns(
|
||||||
|
name,
|
||||||
|
getAttributeNames(type, attrs, locale),
|
||||||
|
[],
|
||||||
|
getAttributeValues(type, attrs, locale),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get variants
|
||||||
|
{
|
||||||
|
if (getTypeVariants(type).length > 0) {
|
||||||
|
getTypeVariants(type).map((v) => {
|
||||||
|
fields.push({
|
||||||
|
name: `${v.metaGroup.name[locale] ?? v.metaGroup.name.en} variants`,
|
||||||
|
value: v.types.map((t) => renderTypeEveRefLink(t, locale)).join('\n'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.length === 0) {
|
||||||
|
return {
|
||||||
|
type: 'page',
|
||||||
|
embeds: [embed.setDescription('This item does not have any fitting attributes.')],
|
||||||
|
components: [context.buildButtonRow(key, context)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.addFields(fields);
|
||||||
|
return {
|
||||||
|
type: 'page',
|
||||||
|
embeds: [embed],
|
||||||
|
components: [context.buildButtonRow(key, context)],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const shipOutputAttrs = [CommonAttribute.PowergridOutput, CommonAttribute.CPUOutput];
|
||||||
|
|
||||||
|
const hardpointAttrs = [CommonAttribute.TurretHardpoints, CommonAttribute.LauncherHardpoints];
|
||||||
|
|
||||||
|
const moduleAttrs = [CommonAttribute.HighSlots, CommonAttribute.MediumSlots, CommonAttribute.LowSlots];
|
||||||
|
|
||||||
|
const rigAttrs = [CommonAttribute.RigSlots, CommonAttribute.RigSize, CommonAttribute.Calibration];
|
||||||
|
|
||||||
|
const moduleFittingAttrs = [CommonAttribute.CPUUsage, CommonAttribute.PowergridUsage, CommonAttribute.ActivationCost];
|
||||||
|
|
||||||
|
const attrMap = {
|
||||||
|
'Ship Output': shipOutputAttrs,
|
||||||
|
Hardpoints: hardpointAttrs,
|
||||||
|
Modules: moduleAttrs,
|
||||||
|
Rigs: rigAttrs,
|
||||||
|
Fitting: moduleFittingAttrs,
|
||||||
|
};
|
||||||
115
packages/eve-bot/src/commands/search/pages/_old_industry.ts
Normal file
115
packages/eve-bot/src/commands/search/pages/_old_industry.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { renderThreeColumns, type Page } from '@lib/discord';
|
||||||
|
import { EmbedBuilder } from 'discord.js';
|
||||||
|
import {
|
||||||
|
getBlueprint,
|
||||||
|
type ManufacturingActivity,
|
||||||
|
eveRefLink,
|
||||||
|
getType,
|
||||||
|
getSchematic,
|
||||||
|
getTypeIconUrl,
|
||||||
|
getTypeBlueprints,
|
||||||
|
getTypeSchematics,
|
||||||
|
} from 'star-kitten-lib/eve';
|
||||||
|
import type { PageKey, TypeContext } from '../_old_ItemLookup';
|
||||||
|
|
||||||
|
export function industryPage(key: PageKey.INDUSTRY, locale: string = 'en'): Page<TypeContext> {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
content: async (context: TypeContext) => {
|
||||||
|
const type = context.type;
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(type.name[locale] ?? type.name.en)
|
||||||
|
.setThumbnail(getTypeIconUrl(type))
|
||||||
|
.setURL(eveRefLink(type.type_id))
|
||||||
|
.setFooter({ text: `id: ${type.type_id}` })
|
||||||
|
.setColor('Green');
|
||||||
|
|
||||||
|
let description = '';
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
const bps = getTypeBlueprints(type);
|
||||||
|
if (bps.length > 0) {
|
||||||
|
bps.map((bp) => {
|
||||||
|
const type = bp.blueprint;
|
||||||
|
const blueprint = getBlueprint(bp.blueprint.type_id);
|
||||||
|
const activity = blueprint.activities[bp.activity];
|
||||||
|
|
||||||
|
description += `### Blueprint\n`;
|
||||||
|
description += `[${type.name[locale] ?? type.name.en}](${eveRefLink(type.type_id)})\n`;
|
||||||
|
// fields.push({
|
||||||
|
// name: 'Blueprints',
|
||||||
|
// value: bps.map(bp => {
|
||||||
|
// const type = bp.blueprint;
|
||||||
|
// return `[${type.name[locale] ?? type.name.en}](${type.eveRefLink})`;
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (activity['materials']) {
|
||||||
|
const manufacturing = activity as ManufacturingActivity;
|
||||||
|
if (manufacturing.materials) {
|
||||||
|
description += '### Materials\n```';
|
||||||
|
description += Object.values(manufacturing.materials)
|
||||||
|
.map((m) => {
|
||||||
|
const t = getType(m.type_id);
|
||||||
|
return `${t.name[locale] ?? t.name.en} ${m.quantity}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
description += '```';
|
||||||
|
|
||||||
|
// fields.push(...renderThreeColumns(
|
||||||
|
// 'Materials',
|
||||||
|
// Object.values(manufacturing.materials).map(m => {
|
||||||
|
// const t = getType(m.type_id);
|
||||||
|
// return `[${t.name[locale] ?? t.name.en}](${t.eveRefLink})`;
|
||||||
|
// }),
|
||||||
|
// [],
|
||||||
|
// Object.values(manufacturing.materials).map(m => {
|
||||||
|
// const t = getType(m.type_id);
|
||||||
|
// return `x**${m.quantity}**`;
|
||||||
|
// }),
|
||||||
|
// ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const schematics = getTypeSchematics(type);
|
||||||
|
if (schematics.length > 0) {
|
||||||
|
schematics.map((type) => {
|
||||||
|
const schematic = getSchematic(type.type_id);
|
||||||
|
|
||||||
|
fields.push({
|
||||||
|
name: 'Schematic',
|
||||||
|
value: `[${type.name[locale] ?? type.name.en}](${eveRefLink(type.type_id)})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
fields.push(
|
||||||
|
...renderThreeColumns(
|
||||||
|
'Materials',
|
||||||
|
Object.values(schematic.materials).map((m) => {
|
||||||
|
const t = getType(m.type_id);
|
||||||
|
return `[${t.name[locale] ?? t.name.en}](${eveRefLink(t.type_id)})`;
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
Object.values(schematic.materials).map((m) => {
|
||||||
|
return `x**${m.quantity}**`;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description === '') {
|
||||||
|
description = 'No blueprints or schematics found';
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.addFields(fields);
|
||||||
|
embed.setDescription(description);
|
||||||
|
return {
|
||||||
|
type: 'page',
|
||||||
|
embeds: [embed],
|
||||||
|
components: [context.buildButtonRow(key, context)],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
134
packages/eve-bot/src/commands/search/pages/_old_skills.ts
Normal file
134
packages/eve-bot/src/commands/search/pages/_old_skills.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { coloredText, renderThreeColumns, WHITE_SPACE, type Page } from '@lib/discord';
|
||||||
|
import { EmbedBuilder } from 'discord.js';
|
||||||
|
import type { Type } from 'star-kitten-lib/eve';
|
||||||
|
import { eveRefLink, getCharacterSkills, getGroup, getTypeIconUrl, getTypeSkills } from 'star-kitten-lib/eve';
|
||||||
|
import { CommonCategory } from 'star-kitten-lib/eve';
|
||||||
|
import type { PageKey, TypeContext } from '../_old_ItemLookup';
|
||||||
|
import { CharacterHelper, UserHelper } from 'star-kitten-lib/db';
|
||||||
|
|
||||||
|
function canUseText(type: Type) {
|
||||||
|
const category = getGroup(type.group_id).category_id;
|
||||||
|
switch (category) {
|
||||||
|
case CommonCategory.SHIP:
|
||||||
|
return 'fly this ship';
|
||||||
|
case CommonCategory.DRONE:
|
||||||
|
return 'use this drone';
|
||||||
|
case CommonCategory.MODULE:
|
||||||
|
return 'use this module';
|
||||||
|
default:
|
||||||
|
return 'use this item';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skillsPage(key: PageKey.SKILLS, locale: string = 'en'): Page<TypeContext> {
|
||||||
|
return {
|
||||||
|
key: 'skills',
|
||||||
|
content: async (context: TypeContext) => {
|
||||||
|
const type = context.type;
|
||||||
|
|
||||||
|
if (!type.required_skills || type.required_skills.length === 0) {
|
||||||
|
return {
|
||||||
|
type: 'page',
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle(type.name[locale] ?? type.name.en)
|
||||||
|
.setDescription('This item does not require any skills to use.')
|
||||||
|
.setThumbnail(getTypeIconUrl(type))
|
||||||
|
.setURL(eveRefLink(type.type_id))
|
||||||
|
.setFooter({ text: `id: ${type.type_id}` })
|
||||||
|
.setColor('Green'),
|
||||||
|
],
|
||||||
|
components: [context.buildButtonRow(key, context)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = UserHelper.findByDiscordId(context.interaction.user.id);
|
||||||
|
const main = CharacterHelper.find(user.mainCharacterID);
|
||||||
|
const skills = main && (await getCharacterSkills(main));
|
||||||
|
const characterSkills: { [key: number]: number } =
|
||||||
|
skills && skills?.skills.reduce((acc, skill) => ({ ...acc, [skill.skill_id]: skill.trained_skill_level }), {});
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(type.name[locale] ?? type.name.en)
|
||||||
|
.setThumbnail(getTypeIconUrl(type))
|
||||||
|
.setURL(eveRefLink(type.type_id))
|
||||||
|
.setFooter({ text: `id: ${type.type_id} -- ◼ = trained | ☒ = required but not trained` });
|
||||||
|
|
||||||
|
let description = '';
|
||||||
|
|
||||||
|
description += '### Required Skills\n```\n';
|
||||||
|
description += getTypeSkills(type)
|
||||||
|
.map((skillLevel) => `${skillLevel.skill.name[locale] ?? skillLevel.skill.name.en} ${skillLevel.level}`)
|
||||||
|
.join('\n');
|
||||||
|
description += '```';
|
||||||
|
|
||||||
|
let canFly = true;
|
||||||
|
if (characterSkills) {
|
||||||
|
if (getTypeSkills(type).every((skillLevel) => characterSkills[skillLevel.skill.type_id] >= skillLevel.level)) {
|
||||||
|
description += coloredText(`${main.name} can ${canUseText(type)}`, 'green');
|
||||||
|
canFly = true;
|
||||||
|
} else {
|
||||||
|
description += coloredText(`${main.name} cannot ${canUseText(type)}`, 'red');
|
||||||
|
canFly = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
embed.setDescription(description);
|
||||||
|
embed.addFields(
|
||||||
|
renderThreeColumns('', getSkillNames(type, locale), [], getSkillLevels(type, characterSkills).map(renderLevel)),
|
||||||
|
);
|
||||||
|
embed.setColor(canFly ? 'Green' : 'Red');
|
||||||
|
return {
|
||||||
|
type: 'page',
|
||||||
|
embeds: [embed],
|
||||||
|
components: [context.buildButtonRow(key, context)],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSkillNames(type: Type, locale: string, depth: number = 0) {
|
||||||
|
let spacing = '';
|
||||||
|
for (let i = 0; i < depth; ++i) {
|
||||||
|
spacing += WHITE_SPACE;
|
||||||
|
}
|
||||||
|
let names: string[] = [];
|
||||||
|
getTypeSkills(type).forEach((skillLevel) => {
|
||||||
|
names.push(
|
||||||
|
`${spacing}[${skillLevel.skill.name[locale] ?? skillLevel.skill.name.en}](${skillLevel.skill.eveRefLink})`,
|
||||||
|
);
|
||||||
|
if (skillLevel.skill.skills.length > 0) {
|
||||||
|
names.push(...getSkillNames(skillLevel.skill, locale, depth + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequiredLevel {
|
||||||
|
required: number;
|
||||||
|
have: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skills is a map of skill_id to trained_skill_level
|
||||||
|
function getSkillLevels(type: Type, skills?: { [key: number]: number }): RequiredLevel[] {
|
||||||
|
let levels: RequiredLevel[] = [];
|
||||||
|
getTypeSkills(type).forEach((skillLevel) => {
|
||||||
|
levels.push({
|
||||||
|
required: skillLevel.level,
|
||||||
|
have: skills ? skills[skillLevel.skill.type_id] || 0 : 0,
|
||||||
|
});
|
||||||
|
if (skillLevel.skill.skills.length > 0) {
|
||||||
|
levels.push(...getSkillLevels(skillLevel.skill, skills));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLevel(level: RequiredLevel) {
|
||||||
|
let str = '';
|
||||||
|
for (let i = 1; i <= 5; ++i) {
|
||||||
|
str += i <= level.required ? (level.have >= i ? '◼' : '☒') : level.have >= i ? '◼' : '▢';
|
||||||
|
// shapes to test with:
|
||||||
|
// '■' '▰' '▱' '▨' '▧' '◼' '▦' '▩' '▥' '▤' '▣' '▢' '◪' '◫' '◩' '◨' '◧'
|
||||||
|
}
|
||||||
|
return str + `${WHITE_SPACE}${level.have}/${level.required}`;
|
||||||
|
}
|
||||||
140
packages/eve-bot/src/commands/search/pages/attributes.ts
Normal file
140
packages/eve-bot/src/commands/search/pages/attributes.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { renderSubroutes, type Page } from '@star-kitten/discord/pages';
|
||||||
|
import type { SearchState } from '../search.command';
|
||||||
|
import {
|
||||||
|
ButtonStyle,
|
||||||
|
createContainer,
|
||||||
|
createSection,
|
||||||
|
createSeparator,
|
||||||
|
createTextDisplay,
|
||||||
|
createThumbnail,
|
||||||
|
Padding,
|
||||||
|
} from '@star-kitten/discord/components';
|
||||||
|
import {
|
||||||
|
getGroup,
|
||||||
|
getType,
|
||||||
|
getUnit,
|
||||||
|
renderUnit,
|
||||||
|
typeGetAttribute,
|
||||||
|
typeHasAnyAttribute,
|
||||||
|
type Type,
|
||||||
|
} from '@star-kitten/eve/models';
|
||||||
|
import { attributeOrdering } from '@star-kitten/eve';
|
||||||
|
import { searchActionRow } from './helpers';
|
||||||
|
import { toTitleCase } from '@star-kitten/util/text.js';
|
||||||
|
|
||||||
|
enum Images {
|
||||||
|
ATTRIBUTES = 'https://iili.io/KTbaMR2.md.webp',
|
||||||
|
DEFENSES = 'https://iili.io/KTbSVoX.md.webp',
|
||||||
|
FITTING = 'https://iili.io/KufiFYG.md.webp',
|
||||||
|
FACILITIES = 'https://iili.io/KufikGt.md.webp',
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributeCategoryMap = {
|
||||||
|
structure: 'UI/Fitting/Structure',
|
||||||
|
armor: 'UI/Common/Armor',
|
||||||
|
shield: 'UI/Common/Shield',
|
||||||
|
ewar: 'UI/Common/EWarResistances',
|
||||||
|
capacitor: 'UI/Fitting/FittingWindow/Capacitor',
|
||||||
|
targeting: 'UI/Fitting/FittingWindow/Targeting',
|
||||||
|
facilities: 'UI/InfoWindow/SharedFacilities',
|
||||||
|
fighters: 'UI/InfoWindow/FighterFacilities',
|
||||||
|
on_death: 'UI/InfoWindow/OnDeath',
|
||||||
|
jump_drive: 'UI/InfoWindow/JumpDriveSystems',
|
||||||
|
propulsion: 'UI/Compare/Propulsion',
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupedCategories = [
|
||||||
|
// defenses
|
||||||
|
['shield', 'armor', 'structure', 'ewar'],
|
||||||
|
// fittings
|
||||||
|
['capacitor', 'targeting', 'propulsion'],
|
||||||
|
// facilities
|
||||||
|
['facilities', 'fighters', 'on_death', 'jump_drive'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function getAttributeOrdering(type: Type) {
|
||||||
|
const group = getGroup(type.group_id);
|
||||||
|
switch (group.category_id) {
|
||||||
|
case 11:
|
||||||
|
return attributeOrdering['11'];
|
||||||
|
case 87:
|
||||||
|
return attributeOrdering['87'];
|
||||||
|
default:
|
||||||
|
return attributeOrdering.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bannerMap = {
|
||||||
|
shield: Images.DEFENSES,
|
||||||
|
armor: Images.DEFENSES,
|
||||||
|
structure: Images.DEFENSES,
|
||||||
|
ewar: Images.DEFENSES,
|
||||||
|
|
||||||
|
capacitor: Images.FITTING,
|
||||||
|
targeting: Images.FITTING,
|
||||||
|
propulsion: Images.FITTING,
|
||||||
|
|
||||||
|
facilities: Images.FACILITIES,
|
||||||
|
fighters: Images.FACILITIES,
|
||||||
|
on_death: Images.FACILITIES,
|
||||||
|
jump_drive: Images.FACILITIES,
|
||||||
|
};
|
||||||
|
|
||||||
|
const page: Page<SearchState> = {
|
||||||
|
key: 'attributes',
|
||||||
|
render: (context) => {
|
||||||
|
const type = getType(context.state.data.type_id);
|
||||||
|
const ordering = getAttributeOrdering(type);
|
||||||
|
|
||||||
|
return {
|
||||||
|
components: [
|
||||||
|
createContainer(
|
||||||
|
{},
|
||||||
|
createSection(
|
||||||
|
createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
|
||||||
|
createTextDisplay(`# [${type.name.en}](https://everef.net/types/${type.type_id})\n## Attributes`),
|
||||||
|
),
|
||||||
|
...renderSubroutes(
|
||||||
|
context,
|
||||||
|
'attributes',
|
||||||
|
groupedCategories.map((group) =>
|
||||||
|
group.map((cat) => {
|
||||||
|
const attrCat = ordering[attributeCategoryMap[cat]];
|
||||||
|
const attrs = attrCat.groupedCategories
|
||||||
|
? attrCat.groupedCategories.map(([name, id]) => id).concat(attrCat.normalAttributes) || []
|
||||||
|
: attrCat.normalAttributes;
|
||||||
|
if (!typeHasAnyAttribute(type, attrs)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: toTitleCase(cat.replace('_', ' ')),
|
||||||
|
value: cat,
|
||||||
|
banner: bannerMap[cat],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(currentRoute) => {
|
||||||
|
const lines: string[] = [];
|
||||||
|
const attrCat = ordering[attributeCategoryMap[currentRoute]];
|
||||||
|
const attrs = attrCat.groupedCategories
|
||||||
|
? attrCat.groupedCategories.map(([name, id]) => id).concat(attrCat.normalAttributes) || []
|
||||||
|
: attrCat.normalAttributes;
|
||||||
|
attrs.map((attrId) => {
|
||||||
|
const attr = typeGetAttribute(type, attrId);
|
||||||
|
if (!attr) return;
|
||||||
|
const unit = attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : '';
|
||||||
|
lines.push(`${attr.attribute.display_name.en.padEnd(24)} ${unit}`);
|
||||||
|
});
|
||||||
|
return createTextDisplay('```\n' + lines.join('\n') + '\n```');
|
||||||
|
},
|
||||||
|
{ style: ButtonStyle.SECONDARY },
|
||||||
|
),
|
||||||
|
createSeparator(Padding.LARGE),
|
||||||
|
searchActionRow('attributes'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
11
packages/eve-bot/src/commands/search/pages/helpers.ts
Normal file
11
packages/eve-bot/src/commands/search/pages/helpers.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createActionRow, createButton } from '@star-kitten/discord/components';
|
||||||
|
|
||||||
|
export function searchActionRow(pageKey: string) {
|
||||||
|
return createActionRow(
|
||||||
|
createButton('Main', 'main', { disabled: pageKey === 'main' }),
|
||||||
|
createButton('Attributes', 'attributes', { disabled: pageKey === 'attributes' }),
|
||||||
|
createButton('Fittings', 'fittings', { disabled: pageKey === 'fittings' }),
|
||||||
|
createButton('Skills', 'skills', { disabled: pageKey === 'skills' }),
|
||||||
|
createButton('Industry', 'industry', { disabled: pageKey === 'industry' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
3
packages/eve-bot/src/commands/search/pages/index.ts
Normal file
3
packages/eve-bot/src/commands/search/pages/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './_old_industry';
|
||||||
|
export * from './main';
|
||||||
|
export * from './attributes';
|
||||||
89
packages/eve-bot/src/commands/search/pages/main.ts
Normal file
89
packages/eve-bot/src/commands/search/pages/main.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Page } from '@star-kitten/discord/pages';
|
||||||
|
import type { SearchState } from '../search.command';
|
||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
createMediaGallery,
|
||||||
|
createSection,
|
||||||
|
createTextDisplay,
|
||||||
|
createThumbnail,
|
||||||
|
createURLButton,
|
||||||
|
} from '@star-kitten/discord/components';
|
||||||
|
import { getRoleBonuses, getSkillBonuses, getType } from '@star-kitten/eve/models/type.js';
|
||||||
|
import { cleanText } from '@star-kitten/eve/utils/markdown.js';
|
||||||
|
import { typeSearch } from '@star-kitten/eve/utils/typeSearch.js';
|
||||||
|
import { isApplicationCommand } from '@star-kitten/discord';
|
||||||
|
import { fetchPrice } from '@star-kitten/eve/third-party/evetycoon.js';
|
||||||
|
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
|
||||||
|
import { searchActionRow } from './helpers';
|
||||||
|
|
||||||
|
const page: Page<SearchState> = {
|
||||||
|
key: 'main',
|
||||||
|
render: async (context) => {
|
||||||
|
if (!context.state.data.type_id && isApplicationCommand(context.interaction)) {
|
||||||
|
const typeName = context.interaction.data.options?.find((opt) => opt.name === 'name')?.value;
|
||||||
|
const found = await typeSearch(typeName as string);
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return {
|
||||||
|
components: [createTextDisplay(`No item found for: ${typeName}`)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
context.state.data.type_id = found.type_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = getType(context.state.data.type_id);
|
||||||
|
|
||||||
|
const skillBonuses = getSkillBonuses(type);
|
||||||
|
const roleBonuses = getRoleBonuses(type);
|
||||||
|
const price = await fetchPrice(type.type_id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
components: [
|
||||||
|
createContainer(
|
||||||
|
{},
|
||||||
|
createSection(
|
||||||
|
createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
|
||||||
|
createTextDisplay(`
|
||||||
|
# [${type.name.en}](https://everef.net/types/${type.type_id})
|
||||||
|
|
||||||
|
${skillBonuses
|
||||||
|
.map((bonus) => {
|
||||||
|
return `## Bonus per level of ${bonus.skill.name.en}
|
||||||
|
${bonus.bonuses
|
||||||
|
.sort((a, b) => a.importance - b.importance)
|
||||||
|
.map((b) => `${b.bonus}${b.unit?.display_name ?? '-'} ${cleanText(b.bonus_text.en)}`)
|
||||||
|
.join('\n')}`;
|
||||||
|
})
|
||||||
|
.join('\n')}
|
||||||
|
${
|
||||||
|
roleBonuses.length > 0
|
||||||
|
? `\n## Role Bonuses
|
||||||
|
${roleBonuses
|
||||||
|
.sort((a, b) => a.importance - b.importance)
|
||||||
|
.map((b) => `${b.bonus ?? ''}${b.unit?.display_name ?? '-'} ${cleanText(b.bonus_text.en)}`)
|
||||||
|
.join('\n')}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
),
|
||||||
|
createMediaGallery({
|
||||||
|
url: 'https://iili.io/KTPCFRt.md.webp',
|
||||||
|
}),
|
||||||
|
// createSeparator(Padding.LARGE),
|
||||||
|
createSection(
|
||||||
|
createURLButton('View on EVE Tycoon', `https://evetycoon.com/market/${type.type_id}`),
|
||||||
|
createTextDisplay(
|
||||||
|
`## Buy: ${price ? formatNumberToShortForm(price.buyAvgFivePercent) : '--'} ISK
|
||||||
|
## Sell: ${price ? formatNumberToShortForm(price.sellAvgFivePercent) : '--'} ISK`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
createTextDisplay(`-# Type Id: ${type.type_id}`),
|
||||||
|
searchActionRow('main'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
69
packages/eve-bot/src/commands/search/search.command.ts
Normal file
69
packages/eve-bot/src/commands/search/search.command.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
createChatCommand,
|
||||||
|
isAutocomplete,
|
||||||
|
stringOption,
|
||||||
|
type CommandContext,
|
||||||
|
type ExecutableInteraction,
|
||||||
|
} from '@star-kitten/discord';
|
||||||
|
import { usePages } from '@star-kitten/discord/pages';
|
||||||
|
import { initializeTypeSearch, typeSearchAutoComplete } from '@star-kitten/eve/utils/typeSearch.js';
|
||||||
|
|
||||||
|
import main from './pages/main';
|
||||||
|
import attributes from './pages/attributes';
|
||||||
|
|
||||||
|
let now = Date.now();
|
||||||
|
console.debug('Initializing type search...');
|
||||||
|
await initializeTypeSearch().catch((e) => {
|
||||||
|
console.error('Failed to initialize type search', e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
console.debug(`Type search initialized. Took ${Date.now() - now}ms`);
|
||||||
|
|
||||||
|
export interface SearchState {
|
||||||
|
type_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createChatCommand(
|
||||||
|
{
|
||||||
|
name: 'search',
|
||||||
|
description: 'Search for a type',
|
||||||
|
options: [
|
||||||
|
stringOption({
|
||||||
|
name: 'name',
|
||||||
|
description: 'The type name to search for',
|
||||||
|
autocomplete: true,
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
execute,
|
||||||
|
);
|
||||||
|
|
||||||
|
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
|
||||||
|
if (isAutocomplete(interaction)) {
|
||||||
|
const focusedOption = interaction.data.options?.find((opt) => opt.focused);
|
||||||
|
if (focusedOption?.name === 'name') {
|
||||||
|
const value = focusedOption.value as string;
|
||||||
|
const results = await typeSearchAutoComplete(value);
|
||||||
|
if (results) {
|
||||||
|
await interaction.result(results);
|
||||||
|
} else {
|
||||||
|
await interaction.result([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usePages<SearchState>(
|
||||||
|
{
|
||||||
|
pages: {
|
||||||
|
main,
|
||||||
|
attributes,
|
||||||
|
},
|
||||||
|
initialPage: 'main',
|
||||||
|
ephemeral: false,
|
||||||
|
},
|
||||||
|
interaction,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
3
packages/eve-bot/src/main.ts
Normal file
3
packages/eve-bot/src/main.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { startDiscordBot } from '@star-kitten/discord';
|
||||||
|
|
||||||
|
startDiscordBot();
|
||||||
40
packages/eve-bot/tsconfig.json
Normal file
40
packages/eve-bot/tsconfig.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "@star-kitten/discord",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
// Paths
|
||||||
|
"paths": {
|
||||||
|
"@*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
|
||||||
|
"typeRoots": ["src/types", "./node_modules/@types"]
|
||||||
|
},
|
||||||
|
"include": ["src", "types"],
|
||||||
|
"exclude": ["node_modules", "dist", "build", "**/*.test.ts"]
|
||||||
|
}
|
||||||
30
packages/eve-web/.env.development
Normal file
30
packages/eve-web/.env.development
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
|
||||||
|
#/ public-key encryption for .env files /
|
||||||
|
#/ [how it works](https://dotenvx.com/encryption) /
|
||||||
|
#/----------------------------------------------------------/
|
||||||
|
DOTENV_PUBLIC_KEY_DEVELOPMENT="02572da3d4f3a844588a944214c0e142a5a01deaa6551456af146d34b574024416"
|
||||||
|
|
||||||
|
# .env.development
|
||||||
|
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
|
||||||
|
#/ public-key encryption for .env files /
|
||||||
|
#/ [how it works](https://dotenvx.com/encryption) /
|
||||||
|
#/----------------------------------------------------------/
|
||||||
|
DOTENV_PUBLIC_KEY="02292a330aa041b5f7efc51504e0c208accba67a6877a217ab43cbb59c3c0c3e66"
|
||||||
|
|
||||||
|
# .env
|
||||||
|
DEBUG="encrypted:BC7p62nrs3NV7XdxnBbO1WsHGm8IgDEbOS1RmgORHSqh05vGIv+hmqwau61FamrU/puT4btAsG+iLcSeypCQV5e7bBpr0qu0HQoVyMzunBvrN5ivzNY0Af800lNynsBXzq0cXTY="
|
||||||
|
PORT="encrypted:BJpY7J2J+0z4LUNnKRr7HzcpETcdnWFuRAOC3hVl2cZyiCBl706vJqv+iY3BgA0mus73t9fwYjGRrPSXSQbcSEBzr+Jquj8Gkvy7loXKkKp4Gz1tqX554txfY0XjrgMO3oHATO8="
|
||||||
|
NODE_ENV="encrypted:BJun6Kdf/kBSrIYUgw7pfnMwlrTjvUUq/w2yjqn+X5UgaxUxzLsI0JabYlxQCxoDMSEagQYWI5HkRaZvYuXHzyN2aXm6drC2bahg9aWZTVyWYu00FFwIah7l/tuMA/caeO7s5dwkuOgCvlOQ"
|
||||||
|
LOG_LEVEL="encrypted:BKBvZDS7xkzgg5IqiTc9izmt4om4CjX7t4LA8gMY+0ru0NtVBpkSkchil4PvaRNhktcNjtIzfE6sduRoFw5T1tt88PjtKWIhORyACZk2ZtR3vuO/xecq2q1rIIp5kD1gcp4ujltK"
|
||||||
|
BASE_URL="encrypted:BMueek9QzKR3k+Foe0xUruZUxwwIXBcrL7B6ksdBNFaF92nHHy2HLLOhOpKNebGfSwb9mBR5kOXb095+hsDEcqAiv5nc5BjWGfk/wkFpRfFcmyEGYlmrxLclbFdQFXIqYrzmN7ae/VV1VlRzNLBipO0smJj4LZY649oV2A=="
|
||||||
|
EVE_CLIENT_ID="encrypted:BPOAohc2MmP7VvvLybwvcl7XgzYRqWC6IzBkOf+T73kN8YlHYKt611uYNjU27G6hkVRK3DSfgCuxTPzrQBPxvAY6pDbqFG8pVU9cKDmoeHYtiKh7KkHuBN/cEzll6x8hpIwwrY32nzQjxYMvyVO5UgG7OHK1T/jkeya2TW2DSgGn"
|
||||||
|
EVE_CLIENT_SECRET="encrypted:BE0VYniO3JEFNt1R4iUrGA7W7cSp7gQtG7Y86VQeWnte+idjnqSFmv2lmz83rc7Idvi/VU+ipuY6RL2+49jAb/oUaXGUiwguBAnlFU+ypOVy2Ed29o7yggqiB2+dUuu4xDAsLAfSXErnw4gsDsEPAMqaKhCYz0LHEvJX5ZdwfAcfrpWoLeI/Vm0="
|
||||||
|
EVE_CALLBACK_URL="encrypted:BIjACAnGtL06X0vkvmydydup9HZDcPA+DAYUAhlH3lsq8GJPD5XxlRwVx02VzZQuATfqm1JwGDyYbw8ceaWD2RLlcjSXPF7MWDpYzG1FExE7FZbFRBBO8XqGH9X8kfxYsuca/Td8KuIPjyS5BNkyM4GQcTlojKa15Hk4GXmpNP6Gyb45XNPCegRJL5aARQ=="
|
||||||
|
ESI_USER_AGENT="encrypted:BEUaqMbwPNvJeF/d5q5LJ5Owd5wcQ8Jg/BTGn6qns4cwlX/e6QqLLmfp8E8SxQ8Z6h+qDLpZj0HROJIOK9Y2Xb6qjB+hCnjceRMTx1QWpNS6jXQ85TQiZfYzee57QFleau621B77KIuM5DjPUZZ02efAL+2Yk83amrh3vnzvwrnvM5mMGTzc2TbeQwB6MEdjAdvAz65VGX9DnCwbtP3bMKE7og4+sKMUrTYZpCsILug="
|
||||||
|
DISCORD_APP_ID="encrypted:BM3yHCf9kTxcIQzzmNseT0/xol6ZLYTjZ3m3NKybW1oD2joZ/gTUIg2+mgfaeCqY1CPaSGppxguDPFgthMbWihAdeGuxiITiwLDulCTLcgjBsyT6IlsKUSsE5ZiEZl1A+ikNG1/8rxrF0MsIjVqfw+U+ev8="
|
||||||
|
DISCORD_APP_SECRET="encrypted:BNVLmKb2ZJq1+iIYKwGaCtcM+hak5NPXLNgJnPzTlpd/5zUTKc8OtYZLhg2oqtOYv8rxf8sbjpXNWB6lZL5J5NUuPcQSfVOMr3U1BN6b9WsfWZ/2Pr4cm8kqqqjJVTF49/DasRQU5VlIXodvCI2XfWrcPFV20NvE8HzsJQz/g5cF"
|
||||||
|
DISCORD_PUBLIC_KEY="encrypted:BEmxCcAqODfgukOza3EGzsXdrnXv3qVbzlLFYsD4Iba9mQFCaOwChIQNkN/+Ve5NV5sZeefU5sEXZRYbb/hRjJjMRX4NrhEZn2Pg3mI5/FG/+uCD0cWWs4JGzTRwUcBcG8FZ2Mw/kP1ymUqMRkJCYC+XdyYqcP8zNrQ6/aca3HLcqPma7j2/1lsbX4UJ3QcKDg3bsY+107MLLJJ4+TYiwcE="
|
||||||
|
DISCORD_BOT_TOKEN="encrypted:BCLvoICUVOYz7pkV3f+dufxEZiWcKxQ7E56cCUXtLQjjorW5WsCftgoCdP5NXKSbhHYZBqrTCAHqPp/kLgaVjDXxXu2+61DiCNjB3t/kNIudkfsuzJ0vusKcVzgnQOmTmYnOZ+SYo7hKPrjLi2/Vk8r6K26TATp9t+iuSBMsEs7t9nnnySSYIXyHfXRXbRBg4NUFOmeDRUqBhhix9mgr+MI5SPmQgsXCvA=="
|
||||||
|
DISCORD_TEST_GUILD_ID="encrypted:BKRJRRvQw2aoBrVoVZpPPYSzCy+2VXLLzBb1zNzUR5510qobPNDbUoIlJdm41moQS0ALG94l8miVLtkOKW6MGZyepq+gE1zSEu7eIJWHB8/eSUnGuNeqSghI4Kxr8kn04Bl4eDb1UgP1Fu+8XSXjVel85Q=="
|
||||||
|
JANICE_KEY="encrypted:BPq+CJycBbmmoDHNmHHYiQ00PeISDQTqp2IdlLZD52V2wWPOmXnRnqgvoXff64ebUayySaW1sQtvoaMJE3Gt9E/FUquYiRTPUMT5+oW7Xo60IRgAhRW9n2m/YUDIORxR0J0qxKc9cdE75VWJNqLELKKwsriBdpqj0+4yM/Mn6IaU"
|
||||||
|
PERPLEXITY_API_KEY="encrypted:BHxscbb6WwhFgt5Cp/WzrYGJ6nJvIFeoS3JrTaibPbQ7kiu3C/Zx8klgRhlBF25+HAA8chZHT5MeE99FSD59RqIQTcnKAyfUnxNq+ovoXMV7Nk2Fb/rrdLnvUf8VoeH7L4ZyOVF1wv0r4xm/7Qqc1JN50fME6Qa47KnnQekq/n6o2e5HVI153yJnlCQu/SvcyXCY6VW2"
|
||||||
23
packages/eve-web/.env.production
Normal file
23
packages/eve-web/.env.production
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
|
||||||
|
#/ public-key encryption for .env files /
|
||||||
|
#/ [how it works](https://dotenvx.com/encryption) /
|
||||||
|
#/----------------------------------------------------------/
|
||||||
|
DOTENV_PUBLIC_KEY_PRODUCTION="02f0469506f6722d8fcc179c199ff159ca32f082000c8e7a1465891adb50a4c031"
|
||||||
|
|
||||||
|
# .env.production
|
||||||
|
DEBUG="encrypted:BKnKyDjoQANl7bGi3568JTnA/7sUBdUVlc8nNznTwxs5Lu/4iMDu1cY0x0iE7b0z+RXEuA/w6bERB5uTmWkCdglEI5S6LhnXZzcV55iLBY8rHO/MIDBF39vn/PBsMiA86gBmtaIn"
|
||||||
|
PORT="encrypted:BKaGvooBBBWB13491yjfYwYT2zG1MZiYi3+Y6wW8ZhuvMOEsfPuW94rv3cE2LoguALsBFXH9rn3lQJysZLJcYd9AxJoLqxmWHkEpbQ35PYDSvkJ0GsEGlrd74hVbnh57A7Dmqqc="
|
||||||
|
NODE_ENV="encrypted:BIiY7XZ9vy9stbiNC2u+o4ibruGTtMuRXUJPs3lMxkHKlK6gksg0ddTPbia/qZkZudGjnEhmqYDUPfSWQDdmf8gFsuBgSgYGhR2GCNw7mCCmsDO/wE3ojNnlvuetnVfLeJa7ugvOGjX5QTs="
|
||||||
|
LOG_LEVEL="encrypted:BEAJviH6nTAR4AdFEoiud4ZHV+dwvURoZys4M9KsYAn5MD+nlNEnzS+9vtE3NPwqzfkpK0Z/46xB+SUIXwhwaJ9Yzgz2WqLK1UXEB6fhQqHXdIqvW2ug/+hUxW9k0ueMu5I9btE="
|
||||||
|
BASE_URL="encrypted:BJ6YYd2cX31HuTvGnNxLK33KQgzWxU9yRtlwAc79hhbuioHP5lMu7LxCu8NnXfcaEvevWsEt3cG4I7PNqzlBmU1WhTwsdJ4Kqi2cvs6hKwMeVWvtgdI+ymojG/GoglkbHjSc9737dsth2+erI4qbjkafqYuOC9S9"
|
||||||
|
DISCORD_APP_ID="encrypted:BMqY+6wep/q7bWO9Yc/tJukPHH5H6vvItZpOFK5zaLP92Fx4S+qyZvH/LZKfUBSxZ1d1vbAqo4V+HPxNPyvwXaZyu/qlRf5ZTP/hkt1k9RoNb5UNOMSoD6GSZFS24/JEKuDPzQnvEOb0+prPHEJknvTMXmI="
|
||||||
|
DISCORD_APP_SECRET="encrypted:BBBWkXA3zi8rCWEiMH66v1hFb6Jqw0Bn4H/6b5qWbVakm5UDckOmgQSjXosJLA1VX0vY+38s5fT1ICPIBv2b0rzpaQ09GpRIwryyTT+VLvMcZfrt1CP4ISC0uzlE2p9qceK9EG+6I7ge4pRkzpxotwnWAg9SCqVcNb04kRwPhUvj"
|
||||||
|
DISCORD_PUBLIC_KEY="encrypted:BPqsvWPJ7/IBYynqFMHmEcpMBS+T5CXyfnzZU3flouPwcCaKkIc3xkLhtVco7nWdv2v/hw9ulysmVJhT6CiW9r1k0XRqRnv4q1PDDSTLrP0c1cXPg6pFLcOEv7e2CU+Gkj7UEFZz3xgrb6aWkkLRO0yATcA20xbdnOv3rbigOrJCfJwbbAWPQk7yun8oF6O/mGvWW9Blv8if54fp8vBOeUs="
|
||||||
|
DISCORD_BOT_TOKEN="encrypted:BMfVvYAKvIsMW+dS1X5SZcXfUIxrsunb3q4iXM2ifTlSP8ZlFpA2jxvgYj16jKcexiyVUBhWMSyUC4eS2AuIY/6fKCuA/5JtvqMltzcxPYAi3VEYIz3AgURiMRNy2QsNnqEXxNiakNaPq5Tv4dqVB4z7YeQ16QdvpDWxD4XUQFtG8Q5jZQK/ISj4xJT9cmaxLUJB9XQcbAa8Oseghpk/A1i5JXO05Rx4vQ=="
|
||||||
|
EVE_CLIENT_ID="encrypted:BCBtASu/2DN+EvLPM//WQjBdfwRscwSC5zKBHLTTTcXMfVur8GtDB6ZcBWstmC4YdiimjPobi1RJ+qdYndu1SM300g6UwOmmO2sNhOpG5nyP0mT2HNgwcJzl+Z7Ad1Vr/iByzYyqkc+uYr9NwhvJDPud+HP11dTjKvw+9Ht9/abA"
|
||||||
|
EVE_CLIENT_SECRET="encrypted:BMccZ43R9rT33amzo0zfIgLM8hKDCMXLrj+5h0TNLH1RhwrsUxcKgl17MAVqV+8uPBbB171kRRnjKLaQDjjJM27Jv1SV5bn316qrIx35Tkl2Ocd5wjEs7TSAjf8HwzUhiH9F68+IrQ36Vm8w27+RmsaRtvTtiWWVmYvBXw4PFMprTE7SG0bFq3M="
|
||||||
|
EVE_CALLBACK_URL="encrypted:BDsEZqRGXFzRigkBq50UYj14UvNjRM4Ao3PLSxVTeyc+2Fad1DQa9mfFE9yBnp3l1H5KMQcPJdWf/MxyAa9J0RvXm9l01lbmkgXu+C+HJXWHKJ7/b91NrQqngm2l76jp80WjtmdJ2D5WOUGrIxZatQaqgh8TexQAjVwjkTeQO97PJnbF0FyNQOlu"
|
||||||
|
ESI_USER_AGENT="encrypted:BJ9Pib5a8/qxfROzBfjlAKr/fEvgepN8o6NCI0l3aiYvFuk5hczaA57TKPMP6P2Ct/Juj47YuU8bqF147y8C556NMiY2HDPbrnenXKdAh4xCerjXhkFqowPvEMVxoeuiyhRM7mPmUSAw7AbYi7AxDtCTw80/6S2/b9/32XBk4eCnSdJmM9kFxwHVFQNK83V0Sr5XEymT4S1kntvqlFsBel/5KxMMfNieqTiT+b5mVyM="
|
||||||
|
AUTH_DB_PATH="encrypted:BAqssA/4tJHhxv+pQuSXln5reiqtIdaJzIakctW9fs3omlsZr8j7pXHvZEPQAyYnH2u396tXQxZLXSfdj68q5odUEXUDt6kxN70h3ikL/4gbkfpPkW24wd4NlVPA21GZR+rBpvfpZN1u57Lvp8Lm/QvUfTlka4H5"
|
||||||
|
JANICE_KEY="encrypted:BFEsjnnZNfYFIXvGXKVtko5c8zh5sZze7hjFORfAb4QsHqHh/SqXVKClMCyEa8OMCjNtd8Zmz8LOckaOUYAh09Xi57KM6Eh33CirHipys0rdeURcwSkI9RSXPZOvmOfKZ9yDmhd3iov3AF6b+wwDQ9/rhYZrqh/NWETeHV98Xgv1"
|
||||||
|
PERPLEXITY_API_KEY="encrypted:BIRn8UX4BgL/4QOCaz2cNZVfiJY3zR/Qclr2UI8FnDsUR8mu+hWi8SVWeaauzLSRwiWU1Ihc3/sWUxi8Jz/Ma1dGcDdPwPO7kiZuN2a1Tl3NKiSMmDlNYszLekhpESoUzVOa/605lcKsTemqC8SvVfq7rOPuC2QP7/7bIGSPGDShIfBjU7dUpvfpDX0/Vf++kLHkNiV1"
|
||||||
8
packages/eve-web/.prettierrc.yaml
Normal file
8
packages/eve-web/.prettierrc.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
trailingComma: all
|
||||||
|
tabWidth: 2
|
||||||
|
useTabs: false
|
||||||
|
semi: true
|
||||||
|
singleQuote: true
|
||||||
|
printWidth: 140
|
||||||
|
experimentalTernaries: true
|
||||||
|
quoteProps: consistent
|
||||||
83
packages/eve-web/README.md
Normal file
83
packages/eve-web/README.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Star Kitten Web
|
||||||
|
|
||||||
|
Project created with [Brisa](https://github.com/brisa-build/brisa).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Link the Library
|
||||||
|
|
||||||
|
`star-kitten-lib` has not been published, so link to it locally before running this web project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd star-kitten-lib
|
||||||
|
bun link
|
||||||
|
cd ../web
|
||||||
|
bun link star-kitten-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download static eve reference data & Hoboleaks archive from [EVE Ref](https://everef.net/).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd star-kitten-lib
|
||||||
|
bun get-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initialize the sqlite database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd star-kitten-lib
|
||||||
|
bun generate-migrations
|
||||||
|
bun migrate
|
||||||
|
```
|
||||||
|
Drizzle's migrations seems to fail on the first try sometimes, so just grab the .sql from the generation and run those against the kitten.db file to create the tables & indexes.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Create a .env file in the root directory with the following values:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
#General
|
||||||
|
BASE_URL=http://localhost:3000
|
||||||
|
DEBUG=true
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
|
||||||
|
# EVE - https://developers.eveonline.com/applications
|
||||||
|
EVE_CLIENT_ID=YOUR_EVE_CLIENT_ID
|
||||||
|
EVE_CLIENT_SECRET=YOUR_EVE_SECRET
|
||||||
|
EVE_CALLBACK_URL=http://localhost:3000/auth/callback
|
||||||
|
ESI_USER_AGENT=ADD_YOUR_USER_AGENT_INFO_HERE
|
||||||
|
|
||||||
|
# For using Janice's Appraisal API
|
||||||
|
JANICE_KEY=XXX
|
||||||
|
|
||||||
|
# For using Perplexities AI API
|
||||||
|
PERPLEXITY_API_KEY=XXX
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun start
|
||||||
|
```
|
||||||
|
|
||||||
7
packages/eve-web/brisa.config.ts
Normal file
7
packages/eve-web/brisa.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Configuration } from "brisa";
|
||||||
|
import tailwindcss from 'brisa-tailwindcss';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
integrations: [tailwindcss()],
|
||||||
|
} as Configuration;
|
||||||
|
|
||||||
4
packages/eve-web/build/_brisa/types.ts
Normal file
4
packages/eve-web/build/_brisa/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IntrinsicCustomElements {
|
||||||
|
'counter-client': JSX.WebComponentAttributes<typeof import("D:\dev\@star-kitten\packages\eve-web\src\web-components\counter-client.tsx").default>;
|
||||||
|
}
|
||||||
|
export type PageRoute = "/" | "/about" | "/auth/error" | "/auth/success";
|
||||||
18258
packages/eve-web/build/api/auth/callback.js
Normal file
18258
packages/eve-web/build/api/auth/callback.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user