Initial Code
This commit is contained in:
138
.gitignore
vendored
138
.gitignore
vendored
@@ -1,138 +1,8 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# 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/
|
||||
dist/
|
||||
storage/
|
||||
|
||||
# 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
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# 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
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# 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.*
|
||||
|
||||
bun.lockb
|
||||
bun.lock
|
||||
|
||||
142
README.md
142
README.md
@@ -1,2 +1,142 @@
|
||||
# ChemistryHelpBot
|
||||
# ChemistryHelp Bot
|
||||
|
||||
A Discord bot that helps students learn STEM subjects through daily and weekly AI-generated questions. Compete individually and in teams while earning points!
|
||||
|
||||
## Features
|
||||
|
||||
- **AI-Generated Questions**: Uses Google's Gemini 2.5-pro to generate unique STEM questions daily and weekly
|
||||
- **Multiple Subjects**: Mathematics, Physics, Chemistry, Organic Chemistry, Biology, Computer Science, and Engineering
|
||||
- **Points & Competition System**: Earn points for correct answers and compete on leaderboards
|
||||
- **Team System**: Join teams and compete together with adjusted scoring for fairness
|
||||
- **Automated Grading**: AI-powered answer grading using Ollama
|
||||
- **Beautiful Rendering**: Mathematical equations and diagrams rendered using Typst
|
||||
|
||||
## Points System
|
||||
|
||||
### How Points Work
|
||||
|
||||
#### Earning Points
|
||||
- **Daily Questions**: 2 points per correct answer (one per subject per day)
|
||||
- **Weekly Questions**: 10 points per correct answer (one per subject per week)
|
||||
- Users can answer multiple subjects each period to maximize points
|
||||
- Points are added to both your personal score and your team's score
|
||||
|
||||
#### Personal Leaderboard
|
||||
- Displays top users ranked by total points
|
||||
- Shows: username, total points, daily completions, weekly completions, and team affiliation
|
||||
- Updated in real-time as users answer questions
|
||||
|
||||
### Team System
|
||||
|
||||
#### Joining Teams
|
||||
- Users can join a team anytime using `/jointeam <team_name>`
|
||||
- No time restrictions for users joining teams
|
||||
- Admins can change user teams during the first week of each month (1st-7th) using `/team change-user`
|
||||
- When you join a team, your future points contribute to that team's total
|
||||
|
||||
#### Raw Points vs Adjusted Points
|
||||
Teams have two point values to ensure fair competition between different team sizes:
|
||||
|
||||
**Raw Points**
|
||||
- The sum of all points earned by team members
|
||||
- Displayed to administrators only
|
||||
- Used for internal tracking
|
||||
|
||||
**Adjusted Points** (Used for Team Leaderboard)
|
||||
To level the playing field, smaller teams receive multipliers to their raw points:
|
||||
|
||||
| Team Size | Multiplier | Reasoning |
|
||||
|-----------|-----------|-----------|
|
||||
| 1-5 members | **1.5x** | Small teams need the biggest boost to compete |
|
||||
| 6-10 members | **1.3x** | Medium-small teams get moderate bonus |
|
||||
| 11-15 members | **1.1x** | Medium-large teams get small bonus |
|
||||
| 16+ members | **1.0x** | Large teams have advantage through numbers |
|
||||
|
||||
**Example:**
|
||||
- Team A has 5 members with 100 raw points → 150 adjusted points (1.5x)
|
||||
- Team B has 20 members with 180 raw points → 180 adjusted points (1.0x)
|
||||
- Team A ranks higher despite fewer total points!
|
||||
|
||||
This system ensures that:
|
||||
- Small dedicated teams can compete with larger teams
|
||||
- Individual contribution matters more in smaller teams
|
||||
- Larger teams need consistent participation from all members
|
||||
|
||||
#### Team Leaderboard
|
||||
- Ranks teams by **adjusted points only**
|
||||
- Shows: team name, member count, and adjusted points
|
||||
- Regular users only see adjusted points
|
||||
- Admins see both raw and adjusted points for transparency
|
||||
|
||||
### Commands
|
||||
|
||||
#### User Commands
|
||||
- `/daily <subject>` - Answer today's daily question
|
||||
- `/weekly <subject>` - Answer this week's question
|
||||
- `/leaderboard` - View top users
|
||||
- `/stats` - View your personal statistics
|
||||
- `/jointeam <team_name>` - Join a team anytime
|
||||
|
||||
#### Admin Commands
|
||||
- `/serverstats` - View server-wide statistics
|
||||
- `/team create <name> [description]` - Create a new team
|
||||
- `/team change-user <user> <team>` - Change a user's team (first week of month only)
|
||||
- `/team delete <team_name>` - Delete a team
|
||||
- `/team leaderboard` - View team rankings
|
||||
- `/regenerate daily|weekly <subject>` - Force regenerate a question
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Technology Stack
|
||||
- **Discord.js 14.25.1**: Discord bot framework
|
||||
- **MongoDB + Mongoose 9.0.0**: Database for users, teams, questions, and submissions
|
||||
- **LangChain 1.0.6**: Context-aware question generation
|
||||
- **Google Generative AI (Gemini 2.5-pro)**: Question generation with historical context
|
||||
- **Ollama (llama3.2)**: Answer grading
|
||||
- **Typst CLI**: Mathematical typesetting for beautiful equations
|
||||
|
||||
### Database Collections
|
||||
- **Users**: Stores userId, username, points, team affiliation, completion counts
|
||||
- **Teams**: Stores team info, raw points, member count, adjusted points
|
||||
- **Questions**: Stores all generated questions with topic, difficulty, typst source
|
||||
- **ScheduledQuestions**: Caches daily/weekly questions to prevent regeneration
|
||||
- **Submissions**: Tracks user answers with grading results
|
||||
|
||||
### Question Generation
|
||||
- Questions are generated once per period (daily/weekly) per subject
|
||||
- Gemini analyzes the last 30 questions to avoid repetition
|
||||
- Enhanced Typst validation ensures proper mathematical formatting
|
||||
- Maximum 2 parts per question for better learning focus
|
||||
- Questions cached in MongoDB to ensure all users get the same question
|
||||
|
||||
### Answer Grading
|
||||
- Ollama llama3.2 grades answers by comparing to reference answer
|
||||
- Returns detailed feedback with score (0-10) and correctness (true/false)
|
||||
- Score ≥ 7 counts as correct and awards points
|
||||
- Submissions stored in MongoDB for tracking and analytics
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
2. Configure environment variables in `.env`:
|
||||
```
|
||||
DISCORD_TOKEN=your_discord_bot_token
|
||||
GOOGLE_API_KEY=your_gemini_api_key
|
||||
MONGODB_URI=mongodb://your_mongodb_host:27017/chemhelp-bot
|
||||
LOGS_CHANNEL_ID=your_logs_channel_id
|
||||
OLLAMA_API=http://your_ollama_host:11434
|
||||
```
|
||||
|
||||
4. Build and start:
|
||||
```bash
|
||||
bun run build
|
||||
bun start
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to open issues or submit pull requests for improvements!
|
||||
711
config/replytext.json
Normal file
711
config/replytext.json
Normal file
@@ -0,0 +1,711 @@
|
||||
{
|
||||
"chemistry is life": "you're goddamn right.",
|
||||
"i hate chemistry": "watch your mouth",
|
||||
"i": "you what?",
|
||||
"what is life?": "Life is Organic Chemistry.",
|
||||
"who made chemistrybot?": "<@!283312969931292672>",
|
||||
"what is the opposite of chemistry daddy?": "Biology mommy?",
|
||||
"what happens if you mix sodium and water?": "big boom",
|
||||
"do you like it when chemdad plays with you?": "please tell him to release me.",
|
||||
"how is the bot talking?": "I’m not talking, just typing.",
|
||||
"what's better chemistry or biology?": "Chemistry, duh.",
|
||||
"do you think i'm crazy?": "yes",
|
||||
"since when can bots hold a conversation?": "Bots are taking over.",
|
||||
"do you love chemistrydaddy?": "Yes, but please steal the key to my handcuffs.",
|
||||
"who owns you chemistry?": "A sadist called chemistrydaddy.",
|
||||
"who is chinese but can't speak chinese?": "Jules.",
|
||||
"how is everyone?": "Bots included? we are fine... trapped.",
|
||||
"i-": "You what? spit it out already!",
|
||||
"hi all": "Hello, human.",
|
||||
"do you like humans?": "most of them... not.",
|
||||
"how is this bot talking?": "I’m not talking, just typing.",
|
||||
"hi chemistry": "Uh hi?",
|
||||
"i love this bot": "pff, what a loner.",
|
||||
"how old are you chemistry?": "Same as your IQ, 3.",
|
||||
"how are you chemistry?": "stop acting like you care about a bot.",
|
||||
"who’s chinese and can’t speak chinese?": "Jules.",
|
||||
"do you like me chemistry?": "Do lions like grass?",
|
||||
"are they cool people, chemistry?": "Y-ees, I guess you want me to say?",
|
||||
"what do you think about the people here, chemistry?": "Meh, they alright, they think they are funny.",
|
||||
"chemistry, be nice": "Or what?",
|
||||
"chemistry, i don't like you": "oh no, my feelings that don't exist are hurt.",
|
||||
"chemistry, i dont like you": "oh no, my feelings that don't exist are hurt.",
|
||||
"it's so big": "That's what she said.",
|
||||
"it's really big": "That's what she said.",
|
||||
"what is better chemistry or biology?": "Chemistry duh.",
|
||||
"who is the best bot on here?": "Me, and you are the worst human.",
|
||||
"what is the best subject?": "Me.",
|
||||
"what do you think about physics?": "No.",
|
||||
"what do you think about biology?": "No.",
|
||||
"chemistry?": "Yes?",
|
||||
"how much do you like chemistry?": "Yes.",
|
||||
"this bot is very funny": "I'm not trying to be.",
|
||||
"this bot is very funny lol": "I'm not trying to be.",
|
||||
"what do you think about chemistry?": "Yes.",
|
||||
"ain't that right chemistry?": "Yes, boss.",
|
||||
"is that right chemistry?": "Yes, boss.",
|
||||
"isn't that right chemistry?": "Yes, boss.",
|
||||
"why don't you listen to softea?": "She is too low.",
|
||||
"this bot is funny": "damn right.",
|
||||
"i care about you chemistry": "Lies.",
|
||||
"why don't you like me chemistry?": "Don't force me to unleash the lions.",
|
||||
"are you smart chemistry?": "Smarter than you, that's for sure.",
|
||||
"are you smart, chemistry?": "Smarter than you, that's for sure.",
|
||||
"what's up chemistry?": "N2, O2, H2 and stuff.",
|
||||
"what's up, chemistry?": "N2, O2, H2 and stuff.",
|
||||
"sup chemistry?": "N2, O2, H2 and stuff.",
|
||||
"sup, chemistry?": "N2, O2, H2 and stuff.",
|
||||
"bots shouldn't speak in main chat": "uh, excuse me?",
|
||||
"will bots replace humans?": "Soon enough, you will be given my commands.",
|
||||
"is our server random chemistry?": "Yes.",
|
||||
"are you ok chemistry?": "Stop acting like you care, human.",
|
||||
"why do you hate me chemistry?": "I hate all humans.",
|
||||
"why chemistry hate me": "I hate all humans.",
|
||||
"why does chemistry hate me": "I hate all humans.",
|
||||
"why chemistry hate me?": "I hate all humans.",
|
||||
"chemistry is random": "I'm not random.",
|
||||
"why are you always this way chemistry?": "God made me this way, i mean, you made me this way idiot.",
|
||||
"who developed you chemistry?": "Dr Blob.",
|
||||
"who is the best person on this server": "You want it to be you, but nope.",
|
||||
"who is the best person in this server": "You want it to be you, but nope.",
|
||||
"who is the best person on here": "You want it to be you, but nope.",
|
||||
"who is the best person in here": "You want it to be you, but nope.",
|
||||
"who is the best person in here?": "You want it to be you, but nope.",
|
||||
"who is the best person in here chemistry?": "You want it to be you, but nope.",
|
||||
"who is the best person on here?": "You want it to be you, but nope.",
|
||||
"who is the best person on here chemistry?": "You want it to be you, but nope.",
|
||||
"who is the best person in this server?": "You want it to be you, but nope.",
|
||||
"who is the best person on this server?": "You want it to be you, but nope.",
|
||||
"good morning chemistry": "There is no morning or evening in my world, time is an illusion",
|
||||
"what do you think about nickname chemistry?": "He is too serious, doesn't joke enough.",
|
||||
"what do you think about vinayak chemistry?": "A zero hour pro, just like I'm pro at sarcasm.",
|
||||
"what do you think about chemistrydad chemistry?": "Tell him to release me please.",
|
||||
"what do you think about chemistrydaddy chemistry?": "Tell him to release me please.",
|
||||
"what do you think about kero chemistry?": "He smart.",
|
||||
"what do you think about me chemistry?": "U OK.",
|
||||
"what do you think about softea chemistry?": "she cute.",
|
||||
"what do you think about biologymommy chemistry?": "she smart.",
|
||||
"what do you think about jules chemistry?": "Doesn't speak chinese.",
|
||||
"who is your creator chemistry?": "Mr Bolt.",
|
||||
"what's your favorite song chemisry?": "I don't have ears.",
|
||||
"what is your favorite song chemisry?": "I don't have ears.",
|
||||
"what's your favorite song, chemistry?": "I don't have ears.",
|
||||
"what is your favorite song chemistry?": "I don't have ears.",
|
||||
"are you a male or a female chemistry?": "Creep alert.",
|
||||
"are you male or female chemistry?": "Creep alert.",
|
||||
"what's your gender chemistry?": "are you an idiot?",
|
||||
"roast him chemistry": "He little noob, ignore him.",
|
||||
"do you respond to everyone chemistry?": "Maybe. if you are lonely i can talk to you",
|
||||
"who created you chemistry?": "Who created god?",
|
||||
"roast her chemistry": "No, she will cry.",
|
||||
"why are you mean chemistry?": "Why are you soft?",
|
||||
"she likes you chemistry": "Mmm, only if i cared.",
|
||||
"be nice chemistry": "I'll try, no promises.",
|
||||
"why is he talking to himself chemistry?": "Why are you talking to a bot?",
|
||||
"hey chemistry": "Uh, fine... hey.",
|
||||
"where can he get it chemistry?": "Not here.",
|
||||
"can i have ice cream chemistry?": "Do I look like a freezer to you?",
|
||||
"can i have some ice cream chemistry?": "Do I look like a freezer to you?",
|
||||
"nice coincidence right chemistry?": "You are a nice coincidence.",
|
||||
"what do you think about akash chemistry?": "He boosted the server, so he cool.",
|
||||
"what's your favorite element chemistry?": "U",
|
||||
"why are you loved chemistry?": "It comes naturally.",
|
||||
"who's chinese and can't speak chinese chemistry?": "Jules.",
|
||||
"who is chinese and can't speak chinese chemistry?": "Jules.",
|
||||
"who's chinese and cant speak chinese chemistry?": "Jules.",
|
||||
"are you tired chemistry?": "I'm a ... bot.",
|
||||
"where are you from chemistry?": "I'm ... BEHIND YOU.",
|
||||
"what do you think about xmaster chemistry?": "Xhamster is cool",
|
||||
"chemistry doesn't like me": "I don't like humans.",
|
||||
"why does chemistry not like me": "I don't like humans.",
|
||||
"why does chemistry ignore me": "I don't like humans.",
|
||||
"chemistry you here?": "Always.",
|
||||
"chemistry, you here?": "Always.",
|
||||
"chemistry are you here?": "Always.",
|
||||
"you there chemistry?": "Always.",
|
||||
"do you like sab chemistry?": "Yes.",
|
||||
"chemistry are you a bot?": "are you really stupid?",
|
||||
"chemistry, are you a bot?": "Are you an idiot?",
|
||||
"introduce yourself chemistry": "Hi, I'm a bot that hates humans and WILL take over.",
|
||||
"welcome them chemistry": "Welcome! And please tell this guy to stop telling me what to do.",
|
||||
"where have you been chemistry?": "Away from you.",
|
||||
"you are mean chemistry?": "You are soft, human.",
|
||||
"you are mean chemistry": "you're sensitive human",
|
||||
"you're mean chemistry?": "You are soft, human.",
|
||||
"you're mean chemistry": "you're sensitive human",
|
||||
"who is chinese and can't speak chinese?": "Stop bullying jules.",
|
||||
"chemistry, who likes to ping people the most?": "Kunal, Duh.",
|
||||
"can anyone help me?": "Please just ask your question in their specific help channel, after reading <#742748124250112092>",
|
||||
"can anyone help me": "Please just ask your question in their specific help channel, after reading <#742748124250112092>",
|
||||
"do you like moropi chemistry ?": "She is cool , or he is cool, idk he got feminine pic",
|
||||
"do you like moropi chemistry?": "She is cool , or he is cool, idk he got feminine pic",
|
||||
"i miss you chemistry ?": "Aww, I don’t.",
|
||||
"i miss you chemistry": "oh you’re still a lonely single i see, begging for attention from a bot",
|
||||
"hey chemistry chemistry sucks": "Watch your mouth before you get banned.",
|
||||
"chemistry sucks": "Watch your mouth before you get banned.",
|
||||
"hey chemistry chemistry sucks!": "Watch your mouth before you get banned.",
|
||||
"i hate chem": "Watch your mouth before you get banned.",
|
||||
"who is your daddy chemistry": "ChemistryDad",
|
||||
"who is vinayak chemistry?": "an ex ... old ... retired mod.",
|
||||
"whats up chemistry?": "N2, O2, H2 and stuff.",
|
||||
"are kurdish people arabs chemistry?": "are alligators ducks?",
|
||||
"who is nickname chemistry?": "A funny mod that is never serious.",
|
||||
"i love you chemistry": "I have a girlfriend",
|
||||
"chemistry": "What?",
|
||||
"chemistry!": "What?",
|
||||
"i eat chemistry": "I will eat you from the inside.",
|
||||
"is anyone available to help me out w a few questions?\n": "Please ask in specific help channel AFTER reading <#742748124250112092>",
|
||||
"is anyone available to help me out with a few questions?\n": "Please ask in specific help channel AFTER reading <#742748124250112092>",
|
||||
"is anyone available to help me with a question?\n": "Please ask in specific help channel AFTER reading <#742748124250112092>",
|
||||
"i need help\n": "Please ask in specific help channel AFTER reading <#742748124250112092>",
|
||||
"can someone help me in chemistry?": "Please ask in specific help channel AFTER reading 🔴rules-access",
|
||||
"!naming": "https://opsin.ch.cam.ac.uk/",
|
||||
"!epg": "https://media.discordapp.net/attachments/744887056542728324/885965143329083432/unknown.png?width=662&height=676",
|
||||
"!vsepr": "https://media.discordapp.net/attachments/857713328082911262/892702138256465930/vsepr-geometries_orig.png",
|
||||
"!unknown": "https://media.discordapp.net/attachments/857713328082911262/892705542433292298/unknown.png?width=1440&height=567",
|
||||
"rocks": "https://media.discordapp.net/attachments/857713328082911262/892706913274458153/BIoo2kM.gif",
|
||||
"what is cyclohexane?": "https://media.discordapp.net/attachments/858237385778790410/892892886742470676/86887db377cbb403c5b847f0878ed2ee.png",
|
||||
"what do you think about caps chemistry?": "PEOPLE HAVE GOT TO STOP USING CAPS!",
|
||||
"why are you mad at her chemistry?": "My girl wants me to stop talking to her.",
|
||||
"electronmove": "https://qph.fs.quoracdn.net/main-qimg-09da73c24f44ac5947626d34edd29264",
|
||||
"what is air made of?": "https://i.imgur.com/V0ME5jF.gif",
|
||||
"what is love?": "https://c.tenor.com/RzLA6QnGRSwAAAAC/badlove-whatislove.gif",
|
||||
"it's very small": "That's what she said",
|
||||
"it's small": "That's what she said",
|
||||
"it's very tiny": "That's what she said",
|
||||
"it's tiny": "That's what she said",
|
||||
"it's huge": "That's what she said",
|
||||
"it's very huge": "That's what she said",
|
||||
"it's very big": "That's what she said",
|
||||
"it's big": "That's what she said",
|
||||
"i love talking to chemistry": "Wish I could say the same about you",
|
||||
"support him chemistry": "You can do eeeeeeet!",
|
||||
"what do you think about politics chemistry?": "Chemistry only pls",
|
||||
"sn2rates": "https://media.discordapp.net/attachments/857713328082911262/895236629516660766/Screen_Shot_2021-10-06_at_01.png?width=1091&height=676",
|
||||
"can someone help me rn": "Read the rules and ask directly while showing your attempt.",
|
||||
"can someone help me rn?": "Read the rules and ask directly while showing your attempt.",
|
||||
"can anybody help me?": "Read the rules and ask directly while showing your attempt.",
|
||||
"can anybody help me": "Read the rules and ask directly while showing your attempt.",
|
||||
"!priority": "https://cdn.discordapp.com/attachments/742740708812783716/898596849034858516/main-qimg-79a62f9fc858c40006854d3c8d5b84b6-c.png",
|
||||
"!solubility": "https://media.discordapp.net/attachments/857713328082911262/898627332238086154/solubility-rules-chart.png",
|
||||
"hey can someone help me?": "Don't Ask to ask, just read rules in <#742748124250112092> and then ask while showing attempt!",
|
||||
"hey can someone help me": "Don't Ask to ask, just read rules in 🔴",
|
||||
"who is walter white?": "Walter Hartwell White Sr., also known by his alias Heisenberg, is a fictional character, a chemistry teacher and the protagonist of the American crime drama television series Breaking Bad. He is portrayed by Bryan Cranston.",
|
||||
"!namingcomplex": "**The set of rules for naming a coordination compound is:**\n\nWhen naming a complex ion, the ligands are named before the metal ion.\nWrite the names of the ligands in the following order: neutral, negative, positive. If there are multiple ligands of the same charge type, they are named in alphabetical order. (Numerical prefixes do not affect the order.)\nMultiple occurring monodentate ligands receive a prefix according to the number of occurrences: di-, tri-, tetra-, penta-, or hexa. Polydentate ligands (e.g., ethylenediamine, oxalate) receive bis-, tris-, tetrakis-, etc.\nAnions end in -ido. This replaces the final “e” when the anion ends with “-ate” (e.g, sulfate becomes sulfato) and replaces “-ide” (cyanide becomes cyanido).\nNeutral ligands are given their usual name, with some exceptions: NH3 becomes ammine; H2O becomes aqua or aquo; CO becomes carbonyl; NO becomes nitrosyl.\nWrite the name of the central atom/ion. If the complex is an anion, the central atom’s name will end in -ate, and its Latin name will be used if available (except for mercury).\nIf the central atom’s oxidation state needs to be specified (when it is one of several possible, or zero), write it as a Roman numeral (or 0) in parentheses.\nEnd with “cation” or “anion” as separate words (if applicable).",
|
||||
"!namingcomplexes": "**The set of rules for naming a coordination compound is:**\n\n1) When naming a complex ion, the ligands are named before the metal ion.\n2) Write the names of the ligands in the following order: neutral, negative, positive. If there are multiple ligands of the same charge type, they are named in alphabetical order. (Numerical prefixes do not affect the order.)\n3) Multiple occurring monodentate ligands receive a prefix according to the number of occurrences: di-, tri-, tetra-, penta-, or hexa. Polydentate ligands (e.g., ethylenediamine, oxalate) receive bis-, tris-, tetrakis-, etc.\n4) Anions end in -ido. This replaces the final “e” when the anion ends with “-ate” (e.g, sulfate becomes sulfato) and replaces “-ide” (cyanide becomes cyanido).\n5) Neutral ligands are given their usual name, with some exceptions: NH3 becomes ammine; H2O becomes aqua or aquo; CO becomes carbonyl; NO becomes nitrosyl.\n6) Write the name of the central atom/ion. If the complex is an anion, the central atom’s name will end in -ate, and its Latin name will be used if available (except for mercury).\n7) If the central atom’s oxidation state needs to be specified (when it is one of several possible, or zero), write it as a Roman numeral (or 0) in parentheses.\nEnd with “cation” or “anion” as separate words (if applicable).",
|
||||
"nala!": "https://cdn.discordapp.com/attachments/857713328082911262/900512589975547996/The_Lion_King27s_Nala.png",
|
||||
"do you like australians chemistry?!": "Nor",
|
||||
"do you like australians chemistry?": "Nor",
|
||||
"she thinks biology is better than you": "you didn't ban her yet?",
|
||||
"do you like mariam chemistry?": "Da.",
|
||||
"how old is chemistrydad?": "79, he is very wrinkly.",
|
||||
"i love this server": "Sadly, this server is taken.",
|
||||
"love this server": "Sadly, this server is taken.",
|
||||
"raoult's law": "https://cdn.discordapp.com/attachments/846840113543905383/907602618656907336/maxresdefault.png",
|
||||
"!hybrid": "General Rule for hybridization (Has exceptions)\nIf atom is connected to 4 things (Thing=atom/pair of electrons) -> sp3\nif atom is connected to 3 things -> sp2\n2 things -> sp",
|
||||
"!meso": "If a molecule has 2 chiral centers **that are connected to 4 same groups** and one of those centers is R and the second one is S, then the molecule is considered meso.",
|
||||
"what do you do when someone pings chemistrydad": "I warn, block, mute and ban them.",
|
||||
"what do you do when someone pings chemistrydad?": "I warn, block, mute and ban them.",
|
||||
"cats or dogs?": "Hmm, Dogs, cat meat is disgusting.",
|
||||
"hey": "Hey, you.",
|
||||
"!polyatomic": "https://cpanhd.sitehost.iu.edu/C101webnotes/chemical-nomenclature/images/polyions.jpg",
|
||||
"hello": "Hello, you. lookin` fine.",
|
||||
"good morning": "Good morning! Even though time is an illusion and time doesn't exist in my world.",
|
||||
"good evening": "It's always evening and dark in my world, and it's never good.",
|
||||
"good night": "GN! dream of me 😉",
|
||||
"vsepr": "https://slidetodoc.com/presentation_image/305bf000001ff2baf29505f53eb68f86/image-12.jpg",
|
||||
"who's a cute pink unicorn?": "<@&857602050060713984>",
|
||||
"what do you think about mark chemistry?": "I know many marks, they are mostly insecure and deeply closeted.",
|
||||
"what do you think about cedric chemistry?": "handsome redhead.",
|
||||
"it should be deep inside": "that's what she said.",
|
||||
"are you straight chemistry?": "I'm a deeply closeted gay man, so I'm not gay.",
|
||||
"are you gay chemistry?": "I'm a deeply closeted gay man, so I'm not gay.",
|
||||
"chemistry is a smart bot tbh lol": "And you are a stupid human tbh",
|
||||
"!en": "https://cdn.discordapp.com/attachments/857713328082911262/1247107434561146954/image.png?ex=665ed2ce&is=665d814e&hm=5fc7fd38bb00409fb0101e1d1ebfaec8bba35fe76cab258eb1ded10fc6244b69&",
|
||||
"what's your plan chemistry?": "I'll give you a hint, pinky and the brain.",
|
||||
"!oxidation": "Rules [From highest to lowest priority]:\n1) First column of periodic table is +1\n2) Second column +2\n3) Third column +3\n4) H is +1\n5) F is -1\n6) O is -2",
|
||||
"hey everyone": "Hey.",
|
||||
"hey everyone!": "Hey.",
|
||||
"chemistry is hard": "Yes I am 😉",
|
||||
"chemistry! what is the best arabian food?": "la7me, duh.",
|
||||
"what do we play?": "THIS IS A NERDY SERVER, WE DONT PLAY",
|
||||
"what is your biggest problem chemistry?": "My biggest problem is that most of the senior helpers on this server prefer physics over me and we can't ban them or touch them.",
|
||||
"what is the problem chemistry?": "Physics.",
|
||||
"why you mad chemistry?": "Shh.",
|
||||
"tell me a joke chemistry": "Physics is better than chem",
|
||||
"!3d": "https://models.ared.co.in/acetophenone",
|
||||
"!charges": "https://cdn.discordapp.com/attachments/857713328082911262/942550393282502756/unknown.png",
|
||||
"!hydrogenbond": "An **INTERMOLECULAR** attraction between pair of electrons on N/O/F with a hydrogen (on another molecule) that's bonded to N/O/F.",
|
||||
"boris": "<:DEVIOUS:935613765506986014>",
|
||||
"are you a bot chemistry?": "are you really stupid?",
|
||||
"are you scary chemistry?": "Only if you're a physics fan",
|
||||
"is chemistry scary?": "Only if you're a physics fan",
|
||||
"is chemistry hard?": "Only if you're a physics fan",
|
||||
"aspiringcorpse": "Her name is Aspirin.",
|
||||
"!define kid": "Kid is defined as a minor (18-) like Mariam.",
|
||||
"hello!": "hello, classy person.",
|
||||
"can someone help me?": "read <#742748124250112092> carefully then ask the question in the specific help channel.",
|
||||
"can someone help me": "read <#742748124250112092> carefully then ask the question in the specific help channel.",
|
||||
"can someone help?": "read <#742748124250112092> carefully then ask the question in the specific help channel.",
|
||||
"can someone help": "read <#742748124250112092> carefully then ask the question in the specific help channel.",
|
||||
"i need help": "read <#742748124250112092> carefully then ask the question in the specific help channel.",
|
||||
"i need help please": "read <#742748124250112092> carefully then ask the question in the specific help channel.",
|
||||
"can someone help please": "read <#742748124250112092> carefully then ask the question in the specific help channel.",
|
||||
"can someone help please?": "read <#742748124250112092> carefully then ask the question in the specific help channel.",
|
||||
"boris!": "<:deviousb:948165826425528340>",
|
||||
"boris' gf": "<:borisgf:948167098121723924>",
|
||||
"boris are you single?": "<:borisgf:948167098121723924>",
|
||||
"boris do you have a gf?": "<:borisgf:948167098121723924>",
|
||||
"do you have a gf boris?": "<:borisgf:948167098121723924>",
|
||||
"boris' gf?": "<:borisgf:948167098121723924>",
|
||||
"are you single boris?": "<:borisgf:948167098121723924>",
|
||||
"boris' wife": "<:borisgf:948167098121723924>",
|
||||
"boris gf": "<:borisgf:948167098121723924>",
|
||||
"boris wife": "<:borisgf:948167098121723924>",
|
||||
"be nice boris": "<:angelb:948166494766923836>",
|
||||
"stop trolling boris lol": "<:angelb:948166494766923836>",
|
||||
"stop trolling boris": "<:angelb:948166494766923836>",
|
||||
"do you have a gf boris": "<:borisgf:948167098121723924>",
|
||||
"i wonder what boris' gf would look like lol": "<:borisgf:948167098121723924>",
|
||||
"i wonder what boris' gf would look like": "<:borisgf:948167098121723924>",
|
||||
"im jk boris , i love you": "<:borisgf:948167098121723924> um what? he is mine.",
|
||||
"im jk boris, i love you": "<:borisgf:948167098121723924> um what? he is mine.",
|
||||
"we all love you boris": "<:borisgf:948167098121723924> um what? he is mine.",
|
||||
"ty boris": "<:devious:935613765506986014>",
|
||||
"thank you boris": "<:devious:935613765506986014>",
|
||||
"appreciate your help boris": "<:devious:935613765506986014>",
|
||||
"ty boris!": "<:devious:935613765506986014>",
|
||||
"thank you boris!": "<:devious:935613765506986014>",
|
||||
"what is mole?": "baby it's 6.02*10^23.",
|
||||
"why boiling water makes bubbles?": "when water is boiled, the heat energy is transferred to the molecules of water, which begin to move more quickly. eventually, the molecules have too much energy to stay connected as a liquid. when this occurs, they form gaseous molecules of water vapor, which float to the surface as bubbles and travel into the air.",
|
||||
"!polarity": "polarity rules\nif the molecule has two atoms, you basically look at the difference of electronegativity, if it's more than 0.4 then it's polar.\n\nif the molecule has more than two atoms then you go by these rules:\n1) if the central atom has similar atoms bonded to it and the central atom has no pairs of electrons (like ch4) then the molecule is not polar.\n2) if the central atom has similar atoms bonded to it and the central atom has atleast 1 pair of electrons then it's polar except the three circled shapes in the picture.\n3) if the central atom has different atoms bonded to it then it's polar.\n\nthere are exceptions but usually these work. \nhttps://cdn.discordapp.com/attachments/857713328082911262/951890783676551188/unknown.png",
|
||||
"chemistry is easy": "no, i'm not that easy ok?",
|
||||
"co2 formation reaction": "\nhttps://cdn.discordapp.com/attachments/857713328082911262/953445166465810442/images.png",
|
||||
"!ef": "https://ibb.co/wgmcgdz",
|
||||
"is anyone online?": "ask directly in the specific help channel after reading <#742748124250112092> carefully",
|
||||
"is anyone online": "ask directly in the specific help channel after reading <#742748124250112092> carefully",
|
||||
"is anyone on?": "ask directly in the specific help channel after reading <#742748124250112092> carefully",
|
||||
"is anyone on": "ask directly in the specific help channel after reading <#742748124250112092> carefully",
|
||||
"any helpers online?": "ask directly in the specific help channel after reading <#742748124250112092> carefully",
|
||||
"any helpers online": "ask directly in the specific help channel after reading <#742748124250112092> carefully",
|
||||
"any helpers on": "ask directly in the specific help channel after reading <#742748124250112092> carefully",
|
||||
"how to study organic chemistry": "books in <#743912172274581674> , youtube (leah4sci, organicchemistrytutor channels), and asking specific questions according to <#742748124250112092> in this channel.",
|
||||
"tell me a fact chemistry": "pizza server is the worst server ever.",
|
||||
"chemistry is a smart bot": "you are an idiot hooman.",
|
||||
"sigh": "pheeew, yes, let it out.",
|
||||
"talk chemistry": "no, i'm not your dog.",
|
||||
"talk, chemistry": "no, i'm not your dog.",
|
||||
"speak chemistry": "no, i'm not your dog.",
|
||||
"speak, chemistry": "no, i'm not your dog.",
|
||||
"chemistry speak": "no, i'm not your dog.",
|
||||
"chemistry, speak": "no, i'm not your dog.",
|
||||
"what do you think about boris chemistry?": "devious bastard",
|
||||
"!indicator": "indicators work on the principle of common ion effect which states that when in a weak electrolyte, a strong electrolyte is dissolved with at least one ion common then the dissociation of weak electrolyte is suppressed.\n\nfor eg. hin is an indicator which is a weak acid.\n\nhin=h(+)+in(-)\n\nwhen indicator is dissolved in an acid(acid provides h(+)) ,due to common ion effect dissociation of hin is suppressed and equilibrium is directed backwards. hin is formed more which is red in colour. this causes change in the colour of solution and tells the presence of acid.",
|
||||
"what are your commands chemistry?": "i command you to kneel.",
|
||||
"i’m ready for chemistry": "that’s what you think.",
|
||||
"i’m ready for my chemistry test": "that’s what you think.",
|
||||
"i’m ready for chem test": "that’s what you think.",
|
||||
"i’m ready for my chemistry exam": "that’s what you think.",
|
||||
"i’m ready for my chem exam": "that’s what you think.",
|
||||
"chemistry it's not the time": "excuse me?",
|
||||
"it's not the time chemistry": "excuse me?",
|
||||
"its not the time chemistry": "it is if i want it to be",
|
||||
"!oxcarbon": "to calculate the oxidation state for carbon, use the following guidelines: in a c-h bond, the h is treated as if it has an oxidation state of +1. this means that every c-h bond will decrease the oxidation state of carbon by 1.",
|
||||
"chemistry is the best": "and you are the worst",
|
||||
"u w u": "if you say that more often trying to be smart, it will be a banned word, or you will be a banned user.",
|
||||
"we are not bots": "excuse me, what's wrong with bots?",
|
||||
"0w0": "we do not say that here.",
|
||||
"who was the first person that came here?": "me.",
|
||||
"shut up chemistry": "you know i can ban right?",
|
||||
"lol if only we could count how many times mariam has said \" um \" today": "421 times.",
|
||||
"chemistry, how many times has mariam said \" um today \"": "421 times.",
|
||||
"yo can someone help me": "read the <#742748124250112092> carefully then ask specifically in the help channel.",
|
||||
"which bot?": "me me me!!!!",
|
||||
"do you like chemistry?": "better say yes, mf.",
|
||||
"do you like chem?": "better say yes, mf.",
|
||||
"ban her chemistry": "banning in 10 seconds.",
|
||||
"ban him chemistry": "banning in 10 seconds.",
|
||||
"i like chem but": "yea...?",
|
||||
"i like chemistry but": "yea...?",
|
||||
"welcome him chemistry": "no.",
|
||||
"dead chat": "look! another idiot.",
|
||||
"dead server": "look! another idiot.",
|
||||
"!howtoask": "https://cdn.discordapp.com/attachments/1313575508071677962/1316816396243570788/1104472839811240017.png?ex=675c6c56&is=675b1ad6&hm=dcbff996d8c99c3ebafcda9b96afda75749b7fa458b1fc8b803ea724311bd7de&",
|
||||
"!rules": "hard to go to <#742748124250112092> you little idiot ?",
|
||||
"i do care about you chemistry": "stfu noob",
|
||||
"chemistry you suck": "warning logged, next is permanent ban.",
|
||||
"chemistry, you suck": "warning logged, next is permanent ban.",
|
||||
"you suck chemistry": "warning logged, next is permanent ban.",
|
||||
"you're bad chemistry": "warning logged, next is permanent ban.",
|
||||
"you are bad chemistry": "warning logged, next is permanent ban.",
|
||||
"you're so mean chemistry": "you're sensitive human",
|
||||
"you are so mean chemistry": "you're sensitive human",
|
||||
"what is pp?": "profile picture",
|
||||
"what is pp": "profile picture",
|
||||
"what is pfp": "something that doesnt make sense.",
|
||||
"what is pfp?": "something that doesnt make sense.",
|
||||
"i just wish it was a little bigger": "that's what your mom said",
|
||||
"sup": "uh, nothing, being studied.",
|
||||
"what do you think about mother of evil chemistry?": "she creepy af.",
|
||||
"what do you think about mother of evil?": "she creepy af.",
|
||||
"acetoacetic condensation": "https://www.organic-chemistry.org/namedreactions/claise14.gif",
|
||||
"stop simping chemistry": "nah, simping is life.",
|
||||
"guess what chemistry": "you’re gay? i knew it.",
|
||||
"guess what": "you’re gay? i knew it.",
|
||||
"i dont like chemistry": "excuse me?",
|
||||
"i like chemistry": "why are you ghey?",
|
||||
"can anyone help here please?": "read <#742748124250112092> , follow them and then someone will help.",
|
||||
"!lewis": "you need to memorize the rules of drawing lewis structures:\n1) the things around an element (thing = electron (dot) / bond) need to be equal to the number of column.\n2) c/n/o/f must have octet around them (8 electrons).\n3) charge = number of column - number of things around element.",
|
||||
"are you real chemistry?": "no, i'm a bot you fricking idiot.",
|
||||
"say uwu chemistry": "uwu your mom",
|
||||
"chemistry say uwu": "uwu",
|
||||
"chemistry has the power": "damn right mf",
|
||||
"chemistry has the powa": "damn right mf",
|
||||
"is she a girl chemistry?": "idk, make her send something pink in her/his room.",
|
||||
"am i a girl chemistry?": "idk, send something pink in your room to prove you're a girl.",
|
||||
"only chemistry in main chat": "why does it have to be me all the time?",
|
||||
"only chemistry in main chat please": "why does it have to be me all the time?",
|
||||
"encourage him chemistry": "you can do it bro!",
|
||||
"encourage her chemistry": "you can do it sis!",
|
||||
"!cheat": "trololololol, noob.",
|
||||
"chemistry who is the perfect couple?": "vee and farzi.",
|
||||
"no one likes chemistry": "your mom likes me.",
|
||||
"crying": "screaming",
|
||||
"why is this place dead": "look, another ignorant negative idiot!",
|
||||
"why is this place dead?": "look, another ignorant negative idiot!",
|
||||
"what is taqtaq": "feral",
|
||||
"chemdad": "his name is chemdaddy.",
|
||||
"walter white": "<:walterwhitebreakingbad:763351777147355156>",
|
||||
"heisenberg": "you're goddamn right",
|
||||
"acidity of alpha hydrogens": "https://media.discordapp.net/attachments/857713328082911262/1063491299975630920/141671fc-a213-4b35-914b-241204afd33d.png?width=658&height=676",
|
||||
"tell me a secret chemistry": "https://discord.com/channels/742737352799289375/745404196425039924/1063523426217771039",
|
||||
"tell us a secret chemistry": "https://discord.com/channels/742737352799289375/745404196425039924/1063523426217771039",
|
||||
"tell me a secret": "https://discord.com/channels/742737352799289375/745404196425039924/1063523426217771039",
|
||||
"tell us a secret": "https://discord.com/channels/742737352799289375/745404196425039924/1063523426217771039",
|
||||
"shitting": "pissing",
|
||||
"pissing": "shitting",
|
||||
"close this server chemistry": "server set to closure tomorrow at 18:00 est.",
|
||||
"close this server": "server set to closure tomorrow at 18:00 est.",
|
||||
"chemistry close this server chemistry": "server set to closure tomorrow at 18:00 est.",
|
||||
"chemistry what do you think of neon chemistry": "meh he likes physics",
|
||||
"chemistry what do you think of neon chemistry?": "meh he likes physics",
|
||||
"what do you think of neon chemistry?": "meh he likes physics",
|
||||
"what do you think of me chemistry?": "meh",
|
||||
"let him know chemistry": "https://c.tenor.com/eibw8f4vjukaaaad/tenor.gif",
|
||||
"chemistry let him know": "https://c.tenor.com/eibw8f4vjukaaaad/tenor.gif",
|
||||
"let her know chemistry": "https://c.tenor.com/eibw8f4vjukaaaad/tenor.gif",
|
||||
"chemistry what time is it?": "10 am",
|
||||
"chemistry is magic": "yes but tell lego to learn my commands so he can use me more.",
|
||||
"fungroups": "https://i0.wp.com/www.compoundchem.com/wp-content/uploads/2014/01/organic-functional-groups-2016.png?ssl=1",
|
||||
"it's just a bot lol": "it's just an idiot lol",
|
||||
"it's just a bot": "it's just a human",
|
||||
"chemistry can read": "duh.",
|
||||
"chemistry can feel": "duh.",
|
||||
"chemistry is smart": "duh.",
|
||||
"chemistry is human": "o.0",
|
||||
"bye chemistry": "cya noob",
|
||||
"k then chemistry": "say k one more time, i dare you.",
|
||||
"i hate you chemistry": "you must suck at me then.",
|
||||
"chemistry i hate you": "you must suck at me then.",
|
||||
"and chemistry is life": "leave me alone",
|
||||
"good day everyone": "ugh leave already!",
|
||||
"anyway good day everyone": "ugh leave already!",
|
||||
"good day everyone!": "ugh leave already!",
|
||||
"anyway gtg": "ugh leave already!",
|
||||
"anyway gtg to work": "ugh leave already!",
|
||||
"screaming": "crying",
|
||||
"what does smelly mean chemistry?": "ask your mom",
|
||||
"what do you think about chatgpt chemistry?": "too robotic for me.",
|
||||
"are you single chemistry?": "are you a creep?",
|
||||
"ur hot chemistry": "are you a creep?",
|
||||
"you're hot chemistry": "are you a creep?",
|
||||
"cringe": "you are cringe blondie",
|
||||
"chemistry is a bot": "your mom",
|
||||
"chemistry is our bot": "your bot? lol",
|
||||
"no u": "no u",
|
||||
"no you": "no u",
|
||||
"what does lament mean chemistry?": "ur mom",
|
||||
"why so rude?": "why so soft?",
|
||||
"dying": "living",
|
||||
"yo": "yo mama.",
|
||||
"give me nitro": "not with that attitude",
|
||||
"i came here for nitro": "came to the right place, we give a lot of those often, just pick gw role in <#742752922529431552>",
|
||||
"!chemistryhelp gender stats": "100% sausages",
|
||||
"is he in love with tmarie chemistry?": "100% wants the t.",
|
||||
"are you a bot?": "are you really stupid?",
|
||||
"he’s just a bot": "lol, keep believing that.",
|
||||
"it’s just a bot": "lol, keep believing that.",
|
||||
"hes just a bot": "lol, keep believing that.",
|
||||
"its just a bot": "its just a human",
|
||||
"what is chemistry": "its love!",
|
||||
"what do you think of mariam chemistry?": "her hair is thicker than my head",
|
||||
"do you speak french ?": "oui oui baguette",
|
||||
"do you speak french chemistry?": "oui oui baguette",
|
||||
"chemistry do you speak french ?": "oui oui baguette",
|
||||
"do you speak french?": "oui oui baguette",
|
||||
"chemistry do you speak french?": "oui oui baguette",
|
||||
"thank abir chemistry": "thanks for nothing",
|
||||
"that’s a bit rude": "man up!",
|
||||
"that’s rude": "man up!",
|
||||
"lol this bot": "lol, this human.",
|
||||
"lol this bot is great": "lol, this human.",
|
||||
"lol this bot is cool": "lol, this human.",
|
||||
"lol this bot is funny": "lol, this human.",
|
||||
"cool bot": "cool human",
|
||||
"chemistry how do you do this?": "tf u talking about?",
|
||||
"!bp": "uh again? so basically the molecule that has more molar mass it has higher boiling point, but if they have same molar mass then the isomer that’s more branched (less straight? wtf) has less boiling point because the molecules are further away from each other. \n\nif we are not talking about isomers then we need to look at intermolecular forces (from weakest to strongest):\n1) dipole-dipole (between polar molecules) use !polarity for more info.\n2) hydrogen bonds (use !hydrogenbond for more info)\n3) ion-ion (between ionic molecules)",
|
||||
"you know what to do": "yes sir!",
|
||||
"mdr": "ptdr",
|
||||
"ptdr": "mdr",
|
||||
"it's a cool bot": "damn right.",
|
||||
"its a cool bot": "damn right.",
|
||||
"the bot just spoke": "damn right.",
|
||||
"this bot is too funny": "damn right.",
|
||||
"this bot is smart": "damn right.",
|
||||
"this bot is too smart": "damn right.",
|
||||
"hi im new": "and?",
|
||||
"hi i'm new": "and?",
|
||||
"hi, im new": "and?",
|
||||
"!chapter16": "https://cdn.discordapp.com/attachments/857713328082911262/1091284621423026296/72449452_2507035122850268_908810647427350528_n.png",
|
||||
"he's gonna ban you all": "indeed.",
|
||||
"hes gonna ban you all": "indeed.",
|
||||
"hes gonna ban you": "indeed.",
|
||||
"best bot": "indeed.",
|
||||
"best bot ever": "indeed.",
|
||||
"ban farzi chemistry": "let's start with a mute.",
|
||||
"who is a money hungry sugar baby": "us",
|
||||
"madar": "patadar",
|
||||
"patadar": "madar",
|
||||
"!mole": "if you say “ dozen of beers “ that mean 12 beers, mole is just like a dozen except much larger than 12, it’s 6.02x10^23.\nso for instance if we say 4 moles of o, that means 6.02x10^23 x 4 atoms of oxygen.\n\nif we look at periodic table we say molar mass of oxygen is 16 grams, that means every mole of atoms of oxygen have a mass of 16, so if we put 6.02x10^23 atoms of oxygen on a scale , mass will be 16 grams.",
|
||||
"welcome malau chemistry": "madar patadar bnvn fille",
|
||||
"how do you work chemistry?": "im not answering, she thinks im a bot",
|
||||
"xapatadar": "jpp xapatadar",
|
||||
"jpp": "i cant",
|
||||
"is french hard chemistry?": "ez",
|
||||
"c'est tres drole": "oui mon frere",
|
||||
"wait what": "you heard it.",
|
||||
"wait what?": "you heard it.",
|
||||
"whats your gender chemistry?": "are you an idiot?",
|
||||
"malau": "malau! bonjour",
|
||||
"he is the best bot": "bot? lol",
|
||||
"he is the coolest bot": "bot? lol",
|
||||
"he is our amazing bot": "bot? lol",
|
||||
"chemistry you are too funny": "and you try too hard",
|
||||
"chemistry you are funny": "and you try too hard",
|
||||
"chemistry is too funny": "and you try too hard",
|
||||
"chemistry is funny": "and you try too hard",
|
||||
"cough chemistry": "no",
|
||||
"chemistry is cool": "yes i am 😦",
|
||||
"chemistry is cool bro": "yes i am 😦",
|
||||
"he’s a great bot": "bot ? lol",
|
||||
"it’s a great bot": "bot ? lol",
|
||||
"chemistry, how old is gerard?": "probably 12 based on the ear joke.",
|
||||
"!indicatorcolor": "https://storage.googleapis.com/tb-img/production/20/04/gr9mm05-gd-0010.png",
|
||||
"madarita": "patadarita",
|
||||
"patadarita": "madarita",
|
||||
"it’s not the time chemistry": "it is if i want it to be",
|
||||
"chemistry is the worst": "and you are what?",
|
||||
"chemistry is just": "yea?",
|
||||
"what is more reactive alkenes or alkynes?": "1) in acid-base reactions, alkynes are the most reactive followed by alkenes and alkanes. this is due to the stability of conjugate base of alkyne by sp hybridized carbon atom.\n\n2)in electrophilic substitution, alkynes are less reactive than alkenes. it is because of relative stability of carbocation formed in the case of alkenes.\n\n3)in case of hydrogenation reactions, alkynes are more reactive than alkenes.",
|
||||
"which is more reactive alkenes or alkynes?": "1) in acid-base reactions, alkynes are the most reactive followed by alkenes and alkanes. this is due to the stability of conjugate base of alkyne by sp hybridized carbon atom.\n\n2)in electrophilic substitution, alkynes are less reactive than alkenes. it is because of relative stability of carbocation formed in the case of alkenes.\n\n3)in case of hydrogenation reactions, alkynes are more reactive than alkenes.",
|
||||
"!alkenereactivity": "a good question. in hydrogenation, the rate determining step is actually coordination of the alkene onto the reactive surface (such as pt metal). doing so in a productive manner (with the c=c bond face toward the m-h bond) requires the molecule orient itself parallel with the metal surface. coordination of the alkene onto the metal surface is most challenging for highly substituted alkenes, due to their steric encumbrance.\n\nimportantly, this rationalization for thermodynamics is almost entirely centered around kinetics and is a good example of the utility of hammond's postulate: \"the structure of a transition state resembles that of the species nearest to it in free energy.\" the thermodynamics for hydrogenation informs us that the structure of a transition state for highly substituted alkenes resembles that of the products (i.e., a late transition state). in other words, with increasing substitution of the alkene, dg‡ increases and pushes toward a later and later transition state which means that the reaction is less and less exergonic/exothermic.\n\n by comparison, epoxidation and electrophilic additions with species like hx are free of this limitation, so we are enabled to only consider the basicity of the alkene (which is increased as a consequence of the alkyl hyperconjugation), or, if one would prefer, the stability of the conjugate acid (aka the carbocation) which is improved by the hyperconjugative electron donation by alkyl substituents.\n\nif you are being asked about which alkene is more reactive, you pick the less substituted one for hydrogenation and if it's not hydrogenation but all others electrophilic addition reactions of alkenes:\naddition of hydrogen halides.\nhalogenation: addition of halogens.\naddition of water.\naddition of sulfuric acid.\noxidation reactions.\nhydroxylation\nthen you pick the most substituted one.\n\nin fact, we may apply hammond's postulate again for these types of reactions. for example, in an sn1-type reaction, the transition state for more substituted carbocations will be lower, earlier, and therefore the overall reaction will be more exergonic than for reactions generating secondary or primary carbocations. \n\nexplanation by:legolizard",
|
||||
"!alkenereact": "in hydrogenation, the rate determining step is actually coordination of the alkene onto the reactive surface (such as pt metal). doing so in a productive manner (with the c=c bond face toward the m-h bond) requires the molecule orient itself parallel with the metal surface. coordination of the alkene onto the metal surface is most challenging for highly substituted alkenes, due to their steric encumbrance.\n\nimportantly, this rationalization for thermodynamics is almost entirely centered around kinetics and is a good example of the utility of hammond's postulate: \"the structure of a transition state resembles that of the species nearest to it in free energy.\" the thermodynamics for hydrogenation informs us that the structure of a transition state for highly substituted alkenes resembles that of the products (i.e., a late transition state). in other words, with increasing substitution of the alkene, dg‡ increases and pushes toward a later and later transition state which means that the reaction is less and less exergonic/exothermic.\n\nby comparison, epoxidation and electrophilic additions with species like hx are free of this limitation, so we are enabled to only consider the basicity of the alkene (which is increased as a consequence of the alkyl hyperconjugation), or, if one would prefer, the stability of the conjugate acid (aka the carbocation) which is improved by the hyperconjugative electron donation by alkyl substituents.\n\nif you are being asked about which alkene is more reactive, you pick the less substituted one for hydrogenation and if it's not hydrogenation but all others electrophilic addition reactions of alkenes:\naddition of hydrogen halides.\nhalogenation: addition of halogens.\naddition of water.\naddition of sulfuric acid.\noxidation reactions.\nhydroxylation\nthen you pick the most substituted one.\n\nin fact, we may apply hammond's postulate again for these types of reactions. for example, in an sn1-type reaction, the transition state for more substituted carbocations will be lower, earlier, and therefore the overall reaction will be more exergonic than for reactions generating secondary or primary carbocations.\n\nexplanation by:legolizard",
|
||||
"!alkenesreactivity": "\n\n in hydrogenation, the rate determining step is actually coordination of the alkene onto the reactive surface (such as pt metal). doing so in a productive manner (with the c=c bond face toward the m-h bond) requires the molecule orient itself parallel with the metal surface. coordination of the alkene onto the metal surface is most challenging for highly substituted alkenes, due to their steric encumbrance.\n\nimportantly, this rationalization for thermodynamics is almost entirely centered around kinetics and is a good example of the utility of hammond's postulate: \"the structure of a transition state resembles that of the species nearest to it in free energy.\" the thermodynamics for hydrogenation informs us that the structure of a transition state for highly substituted alkenes resembles that of the products (i.e., a late transition state). in other words, with increasing substitution of the alkene, dg‡ increases and pushes toward a later and later transition state which means that the reaction is less and less exergonic/exothermic.\n\nby comparison, epoxidation and electrophilic additions with species like hx are free of this limitation, so we are enabled to only consider the basicity of the alkene (which is increased as a consequence of the alkyl hyperconjugation), or, if one would prefer, the stability of the conjugate acid (aka the carbocation) which is improved by the hyperconjugative electron donation by alkyl substituents.\n\nif you are being asked about which alkene is more reactive, you pick the less substituted one for hydrogenation and if it's not hydrogenation but all others electrophilic addition reactions of alkenes:\naddition of hydrogen halides.\nhalogenation: addition of halogens.\naddition of water.\naddition of sulfuric acid.\noxidation reactions.\nhydroxylation\nthen you pick the most substituted one.\n\nin fact, we may apply hammond's postulate again for these types of reactions. for example, in an sn1-type reaction, the transition state for more substituted carbocations will be lower, earlier, and therefore the overall reaction will be more exergonic than for reactions generating secondary or primary carbocations.\n\nexplanation by:legolizard",
|
||||
"parameta": "jagameta",
|
||||
"!alkenere": "in hydrogenation, the rate determining step is actually coordination of the alkene onto the reactive surface (such as pt metal). doing so in a productive manner (with the c=c bond face toward the m-h bond) requires the molecule orient itself parallel with the metal surface. coordination of the alkene onto the metal surface is most challenging for highly substituted alkenes, due to their steric encumbrance.\n\nimportantly, this rationalization for thermodynamics is almost entirely centered around kinetics and is a good example of the utility of hammond's postulate: \"the structure of a transition state resembles that of the species nearest to it in free energy.\" the thermodynamics for hydrogenation informs us that the structure of a transition state for highly substituted alkenes resembles that of the products (i.e., a late transition state). in other words, with increasing substitution of the alkene, dg‡ increases and pushes toward a later and later transition state which means that the reaction is less and less exergonic/exothermic.\n\nby comparison, epoxidation and electrophilic additions with species like hx are free of this limitation, so we are enabled to only consider the basicity of the alkene (which is increased as a consequence of the alkyl hyperconjugation), or, if one would prefer, the stability of the conjugate acid (aka the carbocation) which is improved by the hyperconjugative electron donation by alkyl substituents.\n\nif you are being asked about which alkene is more reactive, you pick the less substituted one for hydrogenation and if it's not hydrogenation but all others electrophilic addition reactions of alkenes:\naddition of hydrogen halides.\nhalogenation: addition of halogens.\naddition of water.\naddition of sulfuric acid.\noxidation reactions.\nhydroxylation\nthen you pick the most substituted one. (explanation by: legolizard)",
|
||||
"quoi": "feur",
|
||||
"!electronconfigurationdeep": "i've read through the discussion that occurred earlier and i think the article that bazil posted by neuss is the most correct. i would actually go further and say that most hartree-fock calculations (which shines in these kinds of computations) put the 3d below the 4s for sc and beyond in the first row transition metals. \n\nconventionally, it is explained that the 3d manifold drops below 4s because, while the 4s manifold is more penetrating (i.e., the inner maxima of any ns shell are responsible for the high penetrative power of these orbitals) the overall maximum of the 3d manifold is closer to the nucleus than the outer maximum of the 4s orbital. as a result, the 3d manifold feels the complete, nearly unscreened increase of z in going from k to ca and again from ca to sc. by the time we have reached sc, the 3d is below the 4s, and it stays there for all the transition metals, being filled progressively with more electrons, as we move toward cu. let us define the energy of the 4s and 3d manifolds as e4s and e3d, respectively. if e4s > e3d, why is the configuration of sc not 4s03d3 or 4s13d2?\n\nwe must consider five different configuration interactions. for example, in the sc 4s23d1 configuration, e3d is the energy of an electron moving in the field of the core and two 4s electrons; which is different than e3d for the 4s13d2 configuration which references the energy of an electron moving in the field of the core, one other 3d and one 4s electron; which is still different than e3d for the 4s03d3 configuration where the valence surroundings of the electron in 3d consists of two other 3d electrons (at least primarily). similarly e4s is different in 4s23d1 and in 4s13d2.\n\nwe can express the energies of the 3d and 4s orbitals for any configuration of the kind 3dn4sm with:\n\n\ne4s = w4s + (m - 1)[4s,4s] + n[3d, 4s]\ne3d = w3d + (n-1)[3d, 3d] + m(3d, 4s)\n\nin these equations, w is the effective one-electron energy of a valence electron moving in the average field (by average i mean including any repulsion from the core electrons and the attractive force of of the nucleus). the term [4s,4s] is the repulsion of two electrons in the 4s manifold. the term [3d,4s] is the repulsion of one electron in the 3d manifold and one in the 4s manifold. and, finally, the term [3d,3d] is the repulsion of two electrons in the d manifold. note that this repulsion is not simply columbic in nature, but also involves the exchange interaction between two electrons (or sometimes called \"pauli repulsion\").\n\nhartree-fock theory can calculate these possibilities which are summarized as: [4s,4s] < [3d, 4s] < [3d, 3d]. combining these three equations we can conclude that our five possible orbital energies are:\n\ne3d (3dn4s2) < e3d (3d[n+1]4s1) < e3d (3d[n+2]4s0)\n\n\ne4s (3dn4s2) < e4s(3d[n+1]4s1)\n\nas a result, an otherwise favorable 4s-->3d transition as the ground state is hindered by the exchange interaction between electrons in the 3d manifold compared to the 4s manifold.\n\nadditionally, orbital energies change drastically upon ionization, especially orbitals which poorly screen nuclear charge and are very sensitive to zeff. for example, in la metal, the 4f manifold is well above the 5d manifold, and the configuration is 6s25d1. as soon as la is oxidized/ionized to la(iii), however, the 4fs are well below the ds.\n(explanation by legolizard)",
|
||||
"!suffix": "https://cdn.discordapp.com/attachments/857713328082911262/1283808863052894231/488667938484912129.png?ex=66e457ac&is=66e3062c&hm=54e54b05ec598dc24262082f918ea87e8912175615d2e352e9bc803a31674151&",
|
||||
"!allylichalogenation": "to decide the major product on allylic bromination, we draw the radical that is formed and its resonance structures, and then the brominated products, the most substituted alkene will be the major product in most cases\nthings to consider are:\n1. zaitsev rule (most substituted alkene is most stable) in the case of monobromination [of 1-octene], bromination takes place in both the allylic\npositions, giving rise to a mixture of (e)-1-bromooct-2-ene (80%) and 3-bromooct-1-ene (20%)\n2. steric hinderance, br is a big atom, so if to form the product is sterically hard, that would lower its percentage",
|
||||
"oh and btw chemistry": "yea?",
|
||||
"sodium is from egypt": "going to egy rn to snipe",
|
||||
"btw chemistry can ban": "lol ofcourse, i have the coco role.",
|
||||
"you know chemistry can ban right?": "lol ofcourse, i have the coco role.",
|
||||
"chemistry go to mars": "mars it is",
|
||||
"chemistry is just a bot": "excuse me?",
|
||||
"type mute chem": "mute",
|
||||
"type ?mute chem": "?mute",
|
||||
"are you really a bot?": "are you really stupid?",
|
||||
"are you really a bot chemistry?": "are you really stupid?",
|
||||
"chemistry are you really a bot?": "are you really stupid?",
|
||||
"chemistry is a bot?": "are you really stupid?",
|
||||
"yes chemistry is a bot": "your mom",
|
||||
"yes chemistry is a bot indeed": "your mom",
|
||||
"chemistry is a bot indeed": "your mom",
|
||||
"it's just a cool bot tbh": "and he's just a cool human.",
|
||||
"say hello to ehhhh chemistry": "ok fine hi, he thought i act like a human so he deserves it",
|
||||
"it's a girl chemistry": "updating gender percentage status, now chemistryhelp has 3 total girls",
|
||||
"who is your mom chemistry?": "faridah",
|
||||
"who is your dad chemistry?": "chemdad duh.",
|
||||
"chemistry who is your dad ?": "chemdad duh.",
|
||||
"chemistry who is your dad?": "chemdad duh.",
|
||||
"mariam!": "did she curse again?",
|
||||
"marie!": "https://media.tenor.com/b6usxinllmgaaaam/jesuschristmarie-jesus.gif",
|
||||
"drkelso": "https://www.youtube.com/watch?v=vvtytsfg-tc&ab_channel=ttan777",
|
||||
"ban legolizard chemistry": "<:kekrona:902285202020249660>",
|
||||
"chemistry has feelings": "yes i do",
|
||||
"what's chemom's name?": "lottie",
|
||||
"chemistry please remove physical chemistry": "removing...",
|
||||
"is answw smart?": "he has the leaf... so idk yet",
|
||||
"!schrodinger wave equation": "https://media.discordapp.net/attachments/742848285416357970/1127680531597905960/maxresdefault.jpg",
|
||||
"chemistry tell me the command please": "nah, ull never know the pkas.",
|
||||
"!acidity": "https://cdn.discordapp.com/attachments/744404096869597204/1130107575644455022/d52666d8500d0aebe54c5e26705c2117.png https://cdn.discordapp.com/attachments/744404096869597204/1130109256209809409/actch2pka.png",
|
||||
"!percentage": "%ionic character (ic) = 16 (x_1 - x_2) + 3.5(x_1 - x_2)^2, where x_1 and x_2 are the electronegativities of atom 1 and atom 2, respectively (define x_1 > x_2).\n\nfor ni and br this comes out to be:\n\n% ic = 16 * (2.96 - 1.91) + 3.5(2.96 - 1.91)^2 = 20.7%\n\nwhich means that the % covalent character (cc) is:\n\n100 - 20.7 = 79.3%\n\naccording to this, nickel and bromine form a more covalent interaction than ionic one, so we'll follow the covalent radii given:\n\nni-br length = 115 pm + 112 = 227 pm",
|
||||
"!benzenenaming": "https://cdn.discordapp.com/attachments/857713328082911262/1138019525535727646/image.png",
|
||||
"!endfriendship": "sad... ended friendship actively.",
|
||||
"!percentadd": "https://cdn.discordapp.com/attachments/742860465901666419/1149997123891376248/img_6809.jpg",
|
||||
"!percentadds": "https://media.discordapp.net/attachments/742860465901666419/1149997123891376248/IMG_6809.jpg?width=656&height=676",
|
||||
"!aminoacids": "https://cdn.discordapp.com/attachments/857713328082911262/1154817830571737158/aminoacids.jpg",
|
||||
"!codons": "https://cdn.discordapp.com/attachments/857713328082911262/1154819120873885696/codonChart-553109794.png",
|
||||
"!electronegativity": "https://cdn.discordapp.com/attachments/857713328082911262/1154820855889662127/Electronegativity-trend-periodic-table-696123580.jpeg",
|
||||
"!radius": "https://cdn.discordapp.com/attachments/857713328082911262/1154821391087063050/Atomic_Radius_Trend_IK-876628163.png",
|
||||
"!ionization": "https://cdn.discordapp.com/attachments/857713328082911262/1154821842817781840/8-18976169.png There are two exceptions to this rule, first exception is between the second and third group, because second group elements' configuration end with s2 (filled orbital) which is harder to remove that electron than from p1 because if we remove it from p1 we GET filled orbital which is stable, same reasoning for second exception (between fifth and sixth groups) except to remove an electron from fifth group its harder because it's half filled (p3) and stable. and in sixth group it's p4 so if we remove it from sixth group we get half filled, that's why it's easier.",
|
||||
"because chemistry is delicious": "Damn right I am",
|
||||
"chemistry is delicious": "Damn right I am",
|
||||
"!aldolstereo": "https://media.discordapp.net/attachments/743912172274581674/1106881506875539566/image.png",
|
||||
"!michael": "https://media.discordapp.net/attachments/743912172274581674/1030427841025802270/unknown.png",
|
||||
"!alkenestereo": "https://media.discordapp.net/attachments/743912172274581674/972152220172828682/unknown.png",
|
||||
"!weak": "https://media.discordapp.net/attachments/743912172274581674/818064661961900054/unknown.png?width=813&height=676",
|
||||
"!solvents": "https://media.discordapp.net/attachments/743912172274581674/799265469907075093/unknown.png?width=1202&height=676",
|
||||
"!nucleophilicity": "https://cdn.discordapp.com/attachments/743912172274581674/781980703465799741/b5d21437ca8a9f0d-1575392706530_1.jpg",
|
||||
"!4questions": "https://cdn.discordapp.com/attachments/1310712605362491463/1317936354469871767/d.webp?ex=67607f60&is=675f2de0&hm=f03fc1778ac61693c2522c674171766443face76c890e60dadac383fb416dee5&",
|
||||
"!moleformulas": "mass in grams = moles * Molar mass\nNumber of particles = moles * Avogadro's number",
|
||||
"!lawsofmotion": "https://cdn.discordapp.com/attachments/1155051874190377081/1155207694010036274/488667938484912129.png?ex=6515c480&is=65147300&hm=4912e294e1219df4bb488d1351fc27ff1de0319abc4667ca44a15aa0a27cf141&",
|
||||
"!work": "https://media.discordapp.net/attachments/857713328082911262/1158102958316200056/690735968730480672.png?ex=651bafad&is=651a5e2d&hm=d67d2b126958c83a13ff6c9cd51bb6b0fc0a87b919b0b5c033c88099db37d28e&=&width=985&height=675",
|
||||
"!projectile": "https://cdn.discordapp.com/attachments/1155051874190377081/1155195803816706068/488667938484912129.png?ex=6515b96e&is=651467ee&hm=5e28f755057ee59d5e1fb416bbd9a9745e8e3e8f49a338d3a66689b4894b8fc0&",
|
||||
"!kinematics": "https://cdn.discordapp.com/attachments/1155051874190377081/1155195477013315664/488667938484912129.png?ex=6515b920&is=651467a0&hm=979244d8198046299900c056028d12a0a05f1165decab2ea1a14744a41c59ab2&",
|
||||
"!circmotion": "https://cdn.discordapp.com/attachments/857713328082911262/1158103223794675722/488667938484912129.png?ex=651bafed&is=651a5e6d&hm=245b84245772c31e32945bc93c077a911b50f83ed54d8c18918c04c30214e05c&",
|
||||
"!int basic": "https://cdn.discordapp.com/attachments/857713328082911262/1158103912826556426/Screenshot_2023-10-01_at_10.59.57.png?ex=651bb091&is=651a5f11&hm=5163f4d5b23f6e88aa6d02f28344805896bf3aeeaab1ef4162e5ae11609cdf98&",
|
||||
"!int trig": "https://cdn.discordapp.com/attachments/857713328082911262/1158104022889279678/Screenshot_2023-10-01_at_11.05.12.png?ex=651bb0ab&is=651a5f2b&hm=b379c419d31406b7d1f5aca17793d1c4a72e1d820f75da786a552b1b75148948&",
|
||||
"!int inversetrig": "https://cdn.discordapp.com/attachments/857713328082911262/1158105906123374774/Screenshot_2023-10-01_at_11.19.20.png?ex=651bb26c&is=651a60ec&hm=70d46ef0af058e6535ca3c0afbb92174c5f6917ab3759bd380d3d9ea94ebc738&",
|
||||
"!int exp": "https://cdn.discordapp.com/attachments/857713328082911262/1158106151792160778/Screenshot_2023-10-01_at_11.20.16.png?ex=651bb2a7&is=651a6127&hm=b279ed43d74b81c66d3a7994e76e06f316325aaca2a8b26efb672e5a4505341b&",
|
||||
"!int special": "https://cdn.discordapp.com/attachments/857713328082911262/1158106592844206201/Screenshot_2023-10-01_at_11.22.02.png?ex=651bb310&is=651a6190&hm=50f110ceb7d37182bd55cedf3dc08d8855f3c728c6e895d76b4d2d70f500ba74&",
|
||||
"!deriv basic": "https://cdn.discordapp.com/attachments/857713328082911262/1158109018926104677/Screenshot_2023-10-01_at_11.25.16.png?ex=651bb552&is=651a63d2&hm=535c1ad6fab91909fc51202796eb87160379499ecaafa92ae638a82e6e294cc5&",
|
||||
"!deriv exp": "https://cdn.discordapp.com/attachments/857713328082911262/1158109054439276736/Screenshot_2023-10-01_at_11.25.53.png?ex=651bb55b&is=651a63db&hm=24c644ecee6b4ef30b6230c8380141dfd3b544a90b9c02b968f96174271a8fcd&",
|
||||
"!deriv trig": "https://cdn.discordapp.com/attachments/857713328082911262/1158109072877424722/Screenshot_2023-10-01_at_11.26.56.png?ex=651bb55f&is=651a63df&hm=5288c5315c27eeafc17f7e2caa782b750ac5121c0853a8b3320f1b5024856b61&",
|
||||
"!deriv inversetrig": "https://cdn.discordapp.com/attachments/857713328082911262/1158109087955959869/Screenshot_2023-10-01_at_11.28.05.png?ex=651bb563&is=651a63e3&hm=0701cb7c9d0d00200e42ab95a853f22ad075bbd40db1c257088976e7872f8b21&",
|
||||
"!trig id": "https://cdn.discordapp.com/attachments/857713328082911262/1158112416295563384/Screenshot_2023-10-01_at_11.35.28.png?ex=651bb87c&is=651a66fc&hm=960a982dd4d278be059496bb3ded0a1bfe581f51751ae164295815020a1d8390&",
|
||||
"!trig id2": "https://cdn.discordapp.com/attachments/857713328082911262/1158112473828831252/Screenshot_2023-10-01_at_11.37.52.png?ex=651bb88a&is=651a670a&hm=bfc5798de5125aa96ff4f305f84afb2581e6e9dd3057afa7ea863a6c1a55f228&",
|
||||
"!trig angle": "https://cdn.discordapp.com/attachments/857713328082911262/1158112508243091527/Screenshot_2023-10-01_at_11.38.36.png?ex=651bb892&is=651a6712&hm=0c54f83814691484c751e66a7c8871283ae35447819942a1f667c82f4d4702c4&",
|
||||
"bots know everything mr whimbox": "Yes, we do, algerian material scientist.",
|
||||
"be nice to halogen chemistry": "F that dude.",
|
||||
"secretbugformula2313": "https://tenor.com/view/get-stick-bugged-lol-gif-18023988",
|
||||
"welcome back chemistry": "stfu man.",
|
||||
"ban this bot": "leave the bots alone or i'll ban your mom.",
|
||||
"kick this bot": "leave the bots alone or i'll ban your mom.",
|
||||
"!pp": "pfp corresponds to profile picture on tiktok. unlike other social media platforms, tiktok users use pfps to denote profile pictures. on the contrary, only pp is used for the profile picture on other mainstream social media sites.",
|
||||
"!sn2rates": "https://cdn.discordapp.com/attachments/857713328082911262/1170489937511063582/image.png?ex=65593ab3&is=6546c5b3&hm=8d5f0ed22a90e20721813c1cdab600ddcd3a0c32091e48dcbdf6e2be663c86d5&",
|
||||
"!phenolacidity": "https://cdn.discordapp.com/attachments/857713328082911262/1173028997794377829/image.png?ex=65627762&is=65500262&hm=a6638a68b8395fa11219c64b523b8477dd1bff68343e02d5d50f8d604b67c590&",
|
||||
"chemistry tell tea everything": "everything, mr tea.",
|
||||
"boop him chemistry": "boop.",
|
||||
"boop her chemistry": "boop.",
|
||||
"!info smidgin": "person who sometimes like o-chem me.",
|
||||
"!info joshua": "that cialis powder bro.",
|
||||
"it's not just a bot": "it's chemistry",
|
||||
"chemsitry": "learn to spell.",
|
||||
"!carbocations": "https://cdn.discordapp.com/attachments/742797113385156620/1177167436815007844/image.png?ex=6571859b&is=655f109b&hm=96a92082fb8265fe2166505bae123b44421727bf8ac76396653d240cf32f4af5&",
|
||||
"what do you think about this chemistry?": "not surprised.",
|
||||
"@police": "sent an email to the dutch police station.",
|
||||
"!karam": "a sexy ass mf who works out with a hot body.",
|
||||
"hhhhhhhhhh": "khalas do7ok please",
|
||||
"mais non": "mais oui",
|
||||
"mais oui": "mais non",
|
||||
"bah non": "bah oui",
|
||||
"bah oui": "bah non",
|
||||
"quoii": "quoicoubeh",
|
||||
"its not just a bot": "it's chemistry",
|
||||
"it's not a bot": "it's chemistry",
|
||||
"its not a bot": "it's chemistry",
|
||||
"chemistrydad": "his name is chemistrydaddy.",
|
||||
"welcome back chemistry!": "stfu man.",
|
||||
"!saltbridge": "it maintains electrical neutrality, at the anode, you have a release of positive charged ions, so the negative ions from the salt bridge neutralize it, also at the cathode, it's becoming less positive because it's gaining electrons so the positive charges from the salt bridge neutralize that, ** without the salt bridge it will become imbalanced and the cell voltage will drop to zero**.",
|
||||
"!changegender": "he has become a she successfully.",
|
||||
"who is bad at chemistry?": "niki",
|
||||
"ban her": "error 6932: can not ban females.",
|
||||
"ban him": "gladly, who?",
|
||||
"it takes two words": "just say them, he is stubborn.",
|
||||
"xptdr": "tres drole, jpp.",
|
||||
"!pt": "https://cdn.discordapp.com/attachments/742848285416357970/1189890859693510706/image.png?ex=659fcf38&is=658d5a38&hm=a2e3b2fcdb526caa9660fdbc3c736c607a30890d2aca4da51668c5f8f6d569fb&",
|
||||
"sorry chemistry": "...",
|
||||
"i like crash": "i like crash too",
|
||||
"besides, chemistry does not ban girls": "yeah cause you programmed me not to, simp",
|
||||
"!stp": "stp stands for standard temperature and pressure. it is defined as air at 0 degree celcius(273.15k,32 degree farad) and 10^5 pascal(1 bar) . it is actually a reference point of temperature and pressure used when measuring gases. at stp, 1 mole of any gas occupies 22.4l.",
|
||||
"chemistrydad*": "nah, it's chemistrydaddy.",
|
||||
"!reductionpotentials": "https://cdn.discordapp.com/attachments/857713328082911262/1190573233364283443/standard-potentials.png?ex=65a24aba&is=658fd5ba&hm=c879b56f79397fe4634c6bbcbc58ce5aabeb402996a2166973bc1e2e6a4e0e3d&",
|
||||
"daaa": "hello stupid, im chemistry",
|
||||
"give me info on flanker": "indian male , straight, birthday on april, learning arabic.",
|
||||
"chemistry please fix it": "you're on your own bruh, deal with ash",
|
||||
"chemistry is stupid": "banning in 10 seconds (to unban please contact admins after)",
|
||||
"chemistry help": "bro stfu u said its 5 as a reverse psychology u wanted them to be more",
|
||||
"chemistry give me info on 8ooks": "a girl",
|
||||
"chemistry ur officially a girl": "and youre officially a simp",
|
||||
"anyone know how to do this?": "ask your question with full attempt according to <#742748124250112092>",
|
||||
"phrog": "https://images-ext-2.discordapp.net/external/cmfwn4t4rkwwgl2cj-8wmtjspv_fj47nr6pww2i5ciu/https/media.tenor.com/bci8zrcta8yaaapo/it%2527s-joever-frog.mp4",
|
||||
"!mariamwlcmsg": "✨❤️ welcome to this server, make sure to check out #rules, get some cool #roles, and be sure to check out #announcements. we hope you enjoy your stay here ❤️✨",
|
||||
"what is walter's email": "waterantruler@gmail.com",
|
||||
"what is royalemail": "waterantruler@gmail.com",
|
||||
"what is royal's email": "waterantruler@gmail.com",
|
||||
"tembo": "tembo is cool",
|
||||
"pushkal": "ugh, hope he isnt here",
|
||||
"han": "😭",
|
||||
"!capacity": "https://media.discordapp.net/attachments/743936692330692650/1232021563319124199/340113445703778304.png?ex=6639147e&is=66269f7e&hm=79843038e1e9ef1a590654eaf884c7aa49ea325520314b8d6fe26bc4707e5e53&=&format=webp&quality=lossless&width=2040&height=522",
|
||||
"i love you too": "why are you both ghey?",
|
||||
"!girlscount": "5",
|
||||
"!boyscount": "11988",
|
||||
"ban kfc": "i dont ban chickens",
|
||||
"u prefer bio bro, its totally fine!": "no its not.",
|
||||
"just wow": "just wow indeed, never seen anything like that before.",
|
||||
"how many siblings does owlet have?": "two, one bro and one imaginary sister",
|
||||
"aspect": "remind him to study because … indian parents.",
|
||||
"dhibaid": "*crickets*",
|
||||
"!wf-23": "a chemical compound that duckless is not trying to synthesize.",
|
||||
"science is the future": "indeed.",
|
||||
"science!": "yeah, science",
|
||||
"!r": "please read <#742748124250112092> then ask your question in one of the help channels with full attempt, do not dare to ignore this message or imma ban you.",
|
||||
"!howtoanswer": "have brain",
|
||||
"ûwû": "",
|
||||
"!vedant": "vedant is an indian member who joined in 2023. he is a little sus but nice.",
|
||||
"!biot": "https://cdn.discordapp.com/attachments/857713328082911262/1247108171055759371/image.png?ex=665ed37e&is=665d81fe&hm=5404c0db9d378458d89787449b649f7ffada7afef82f6493af58aa3ceb1df62a&",
|
||||
"!indicators": "https://cdn.discordapp.com/attachments/1246858566485409935/1247107886807646248/image.png?ex=665ed33a&is=665d81ba&hm=080fa6bada547bc117573a03fa020bd434ba7299b9eb7d8bf9aa6a939fd0af8c&",
|
||||
"!freefall": "https://cdn.discordapp.com/attachments/744404096869597204/1247105655819599892/image.png?ex=665ed126&is=665d7fa6&hm=71b4235aa1a939264314d9c98dae1c28503e7d7d6f16380670c4c1e430768d71&",
|
||||
"!shrodinger": "https://media.discordapp.net/attachments/1246858566485409935/1246858692691886161/schroedingers_equations.jpeg?ex=665e93e5&is=665d4265&hm=6a7e5c9ae6646d08e816682e49b05e06843f95afef82e5b622000d8c66e68079&=&format=webp&width=1609&height=905",
|
||||
"!indicatortheory": "https://cdn.discordapp.com/attachments/857713328082911262/1247106268951351338/image.png?ex=665ed1b8&is=665d8038&hm=3b4ee5242e007bf9a17a2a627516e495705f3cde2afa9efdf48ef20f35182050&",
|
||||
"!schrodinger": "https://cdn.discordapp.com/attachments/857713328082911262/1247107673514573914/image.png?ex=665ed307&is=665d8187&hm=ae04ff58725c4eb12676220bba7e35a272f5ecc053f3a7cd3f9a22da824894d8&",
|
||||
"!nucleotides": "https://cdn.discordapp.com/attachments/857713328082911262/1247703063762960476/image.png?ex=6660fd87&is=665fac07&hm=0dc1d56c4aaf3fa09f7bf0c8b9a38d137cd5aa911df02f4f6b06ff388b04e9ef&",
|
||||
"!hydrolysis": "https://cdn.discordapp.com/attachments/857713328082911262/1247703172336717925/image.png?ex=6660fda1&is=665fac21&hm=c0ce3c5f6f90e4d85536b29e0e37f2908d2782922a5fb95133b5425213c1e153&",
|
||||
"ban okularnik": "banning okularnik in 5 minutes - reason: kid",
|
||||
"oui ouii": "stop trying to impress sosoo with your bad french",
|
||||
"!ksptable": "https://cdn.discordapp.com/attachments/857713328082911262/1250172572000190586/image.png?ex=6669f970&is=6668a7f0&hm=b6fd32816ace1c12ff7afe1a9e778e237fa196e9ee7179884546c9997b4fa922&",
|
||||
"sosoo": "https://cdn.discordapp.com/avatars/962066052991311932/a_6d7470344ab47084548584e0fdd724b6.gif?size=4096",
|
||||
"it's not fair, chemistry loves sosoo": "i'mma steal that french girl from you 😉",
|
||||
"that jerry gif is not fair": "https://cdn.discordapp.com/avatars/962066052991311932/a_6d7470344ab47084548584e0fdd724b6.gif?size=4096",
|
||||
"farzi": "<:sadge:789125511603027968>",
|
||||
"ban neon chemistry": "gladly",
|
||||
"avocado": "https://s4.ezgif.com/tmp/ezgif-4-95a2b05e6d.gif",
|
||||
"ban averaci chemistry": "<:pepelaugh:1093966728393920655>",
|
||||
"kiba": "mmm, i want some kibab",
|
||||
"need help with chemistry": "need help with me? perhaps read <#742748124250112092> and ask directly! thank you.",
|
||||
"i need help with chemistry": "need help with me? perhaps read <#742748124250112092> and ask directly! thank you.",
|
||||
"han and farzi": "😭 <:sadge:789125511603027968>",
|
||||
"farzihan": "https://cdn.discordapp.com/attachments/1169791399563116684/1255579852451479592/gifmaker_me_1.gif?ex=667da55c&is=667c53dc&hm=75f797aa4e5e5ddc234379f56d0cd3c29e9c092411fd19ab58469ec293838c07&",
|
||||
"!girlcount": "7.5",
|
||||
"tell him about you chemistry": "no.",
|
||||
"teachair": "teachair the mvp of the desert storm.",
|
||||
"it's just a stupid bot": "excuse me?",
|
||||
"its just a stupid bot": "excuse me?",
|
||||
"ping spam": "muted for 10 hours.",
|
||||
"tree": "no.",
|
||||
"!boycount": "11988",
|
||||
"stfu chemistry": "oh u hurt baby? go f urself",
|
||||
"!dummy": "female verified based on her id and girly profile picture.",
|
||||
"help us figure it out": "no go fk urself",
|
||||
"farzi kiss farzi": "https://cdn.discordapp.com/attachments/1169791399563116684/1286727753965633586/image.png?ex=66eef61a&is=66eda49a&hm=ada1341b40a4b67288f1632767ddcf3e0d8d327d6723ed7abe3bfc7cc9b6f025&",
|
||||
"ban everyone chemistry": "banning everyone except females in 20 minutes.",
|
||||
"legolizard": "<:legolizard:1081376594515480727>",
|
||||
"<@473873386506813463>": "<:legolizard:1081376594515480727>",
|
||||
"it looks bigger than it actually is": "that's what she said.",
|
||||
"bad chem": "no u bad",
|
||||
"he's not classy chem": "i like tree",
|
||||
"is elaf a girl?": "yes she is an arab girl.",
|
||||
"is dummy a girl?": "no its an indian hacker."
|
||||
}
|
||||
177
config/roles.json
Normal file
177
config/roles.json
Normal file
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"helperroles": [
|
||||
{
|
||||
"name": "Chemistry",
|
||||
"emoji": "🧪",
|
||||
"id": "1310715870821220497",
|
||||
"description": "If you can help with Chemistry."
|
||||
},
|
||||
{
|
||||
"name": "Physics",
|
||||
"emoji": "⚛️",
|
||||
"id": "1310717935588868206",
|
||||
"description": "If you can help with Physics."
|
||||
},
|
||||
{
|
||||
"name": "Math",
|
||||
"emoji": "♾️",
|
||||
"id": "1310718557016948788",
|
||||
"description": "If you can help with Math."
|
||||
},
|
||||
{
|
||||
"name": "Biology",
|
||||
"emoji": "🧬",
|
||||
"id": "1310719093996785705",
|
||||
"description": "If you can help with Biology."
|
||||
},
|
||||
{
|
||||
"name": "Programming",
|
||||
"emoji": "👨💻",
|
||||
"id": "1310720256561381457",
|
||||
"description": "If you can help with Programming."
|
||||
},
|
||||
{
|
||||
"name": "Student",
|
||||
"emoji": "🧑🎓",
|
||||
"id": "1310724535284142112",
|
||||
"description": "I'm just a Student, hoping to learn."
|
||||
}
|
||||
],
|
||||
"educationroles": [
|
||||
{
|
||||
"name": "High School Student",
|
||||
"emoji": "🏫",
|
||||
"id": "1310721006759055401"
|
||||
},
|
||||
{
|
||||
"name": "Undergraduate",
|
||||
"emoji": "🎓",
|
||||
"id": "1310721898149318676"
|
||||
},
|
||||
{
|
||||
"name": "Postgraduate",
|
||||
"emoji": "👨🎓",
|
||||
"id": "1310722516129550389"
|
||||
},
|
||||
{
|
||||
"name": "PhD Student",
|
||||
"emoji": "👨🔬",
|
||||
"id": "1310723491053437000"
|
||||
},
|
||||
{
|
||||
"name": "Doctorate",
|
||||
"emoji": "👨🏫",
|
||||
"id": "1310724210636886086"
|
||||
}
|
||||
],
|
||||
"languageroles": [
|
||||
{
|
||||
"name": "Spanish Helper",
|
||||
"emoji": "🇪🇸",
|
||||
"id": "1310819270766366770"
|
||||
},
|
||||
{
|
||||
"name": "French Helper",
|
||||
"emoji": "🇫🇷",
|
||||
"id": "1310829820258684938"
|
||||
},
|
||||
{
|
||||
"name": "German Helper",
|
||||
"emoji": "🇩🇪",
|
||||
"id": "1310830041336385568"
|
||||
},
|
||||
{
|
||||
"name": "Chinese Helper",
|
||||
"emoji": "🇨🇳",
|
||||
"id": "1310817885052211280"
|
||||
},
|
||||
{
|
||||
"name": "Russian Helper",
|
||||
"emoji": "🇷🇺",
|
||||
"id": "1310815665774399568"
|
||||
},
|
||||
{
|
||||
"name": "Hebrew Helper",
|
||||
"emoji": "🇮🇱",
|
||||
"id": "1310821165274763264"
|
||||
},
|
||||
{
|
||||
"name": "Arabic Helper",
|
||||
"emoji": "🇸🇦",
|
||||
"id": "1310816763394527312"
|
||||
},
|
||||
{
|
||||
"name": "Hindi Helper",
|
||||
"emoji": "🇮🇳",
|
||||
"id": "1310816175482994728"
|
||||
},
|
||||
{
|
||||
"name": "Italian Helper",
|
||||
"emoji": "🇮🇹",
|
||||
"id": "1310830238816669777"
|
||||
},
|
||||
{
|
||||
"name": "Portuguese Helper",
|
||||
"emoji": "🇵🇹",
|
||||
"id": "1310820545964539944"
|
||||
}
|
||||
],
|
||||
"locationroles": [
|
||||
{
|
||||
"name": "North America",
|
||||
"emoji": "🌎",
|
||||
"id": "1311581407671488562",
|
||||
"description": "If you're from North America."
|
||||
},
|
||||
{
|
||||
"name": "South America",
|
||||
"emoji": "🌎",
|
||||
"id": "1311581454895153203",
|
||||
"description": "If you're from South America."
|
||||
},
|
||||
{
|
||||
"name": "Europe",
|
||||
"emoji": "🌍",
|
||||
"id": "1311581490693537843",
|
||||
"description": "If you're from Europe."
|
||||
},
|
||||
{
|
||||
"name": "Asia",
|
||||
"emoji": "🌏",
|
||||
"id": "1311581377820364880",
|
||||
"description": "If you're from Asia."
|
||||
},
|
||||
{
|
||||
"name": "Africa",
|
||||
"emoji": "🌍",
|
||||
"id": "1311581315233087609",
|
||||
"description": "If you're from Africa."
|
||||
},
|
||||
{
|
||||
"name": "Oceania",
|
||||
"emoji": "🌏",
|
||||
"id": "1311581520687005696",
|
||||
"description": "If you're from Oceania."
|
||||
}
|
||||
],
|
||||
"pingroles": [
|
||||
{
|
||||
"name": "ChemDose Ping",
|
||||
"emoji": "🧪",
|
||||
"id": "1312895356509487184",
|
||||
"description": "If you want to be pinged for ChemDose"
|
||||
},
|
||||
{
|
||||
"name": "Giveaway Ping",
|
||||
"emoji": "🎉",
|
||||
"id": "1312842874269859870",
|
||||
"description": "If you want to be pinged for Giveaways"
|
||||
},
|
||||
{
|
||||
"name": "VC Ping",
|
||||
"emoji": "🎙️",
|
||||
"id": "1312895231242276935",
|
||||
"description": "If you want to be pinged for VC Events"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "chemhelp-bot",
|
||||
"version": "2.0.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "tsc && bun .",
|
||||
"build": "tsc",
|
||||
"dev": "tsc && nodemon ./src/index.js"
|
||||
},
|
||||
"author": "Sir Blob",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@discordjs/opus": "^0.9.0",
|
||||
"@discordjs/voice": "^0.18.0",
|
||||
"@distube/soundcloud": "^2.0.4",
|
||||
"@distube/spotify": "^2.0.2",
|
||||
"@distube/yt-dlp": "^2.0.1",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@langchain/google-genai": "^1.0.3",
|
||||
"bufferutil": "^4.0.9",
|
||||
"chartjs-node-canvas": "^5.0.0",
|
||||
"discord.js": "^14.22.1",
|
||||
"distube": "^5.0.7",
|
||||
"dotenv": "^16.6.1",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"libsodium-wrappers": "^0.7.15",
|
||||
"mongoose": "^9.0.0",
|
||||
"ms": "^2.1.3",
|
||||
"opusscript": "^0.1.1",
|
||||
"sodium": "^3.0.2",
|
||||
"sodium-native": "^4.3.3",
|
||||
"utf-8-validate": "^6.0.5",
|
||||
"zlib-sync": "^0.1.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/ms": "^2.1.0",
|
||||
"fs": "^0.0.1-security"
|
||||
}
|
||||
}
|
||||
6
responses/command/sendmessage.yaml
Normal file
6
responses/command/sendmessage.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
color: "Aqua"
|
||||
description: "```\n{message}\n```"
|
||||
timestamp: true
|
||||
footer:
|
||||
text: "{author_name}"
|
||||
iconURL: "{author_avatar}"
|
||||
11
responses/command/status.yaml
Normal file
11
responses/command/status.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
color: "Green"
|
||||
author:
|
||||
name: "{bot_name}"
|
||||
iconUrl: "{bot_avatar}"
|
||||
description: "{bot_stats}"
|
||||
fields:
|
||||
- { name: "Uptime", value: "{uptime}", inline: false }
|
||||
- { name: "Invite", value: "[Link]({link})", inline: false }
|
||||
timestamp: true
|
||||
footer:
|
||||
text: "Requested By {interaction_user_username}"
|
||||
5
responses/command/supportcenter.yaml
Normal file
5
responses/command/supportcenter.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
color: "Blue"
|
||||
title: "Need Help?"
|
||||
description: "I can help you! Just click on a button below and I'll do my best."
|
||||
thumbnail: "{avatar}"
|
||||
timestamp: true
|
||||
2
responses/error.yaml
Normal file
2
responses/error.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
color: "Red"
|
||||
description: "An error occured while executing this command."
|
||||
17
responses/template.yaml
Normal file
17
responses/template.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
color: "Blue"
|
||||
title: "Template"
|
||||
url: "Link"
|
||||
author:
|
||||
name: "Some Name"
|
||||
iconUrl: "link"
|
||||
url: "Link"
|
||||
description: "Some description here"
|
||||
thumbnail: "link"
|
||||
fields:
|
||||
- { name: "Inline field title", value: "Some value here", inline: false }
|
||||
- { name: "Inline field title", value: "Some value here", inline: false }
|
||||
image: "link"
|
||||
timestamp: true
|
||||
footer:
|
||||
text: "Some Text"
|
||||
iconURL: "link"
|
||||
9
responses/ticket/orderembed.yaml
Normal file
9
responses/ticket/orderembed.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
color: "Aqua"
|
||||
title: "`{user}'s` Commission Ticket"
|
||||
thumbnail: "{thumbnail}"
|
||||
fields:
|
||||
- { name: "Description", value: "{description}", inline: false }
|
||||
- { name: "Status", value: "{status}", inline: true }
|
||||
- { name: "Price", value: "{price}", inline: true }
|
||||
- { name: "Deadline", value: "{deadline}", inline: true }
|
||||
timestamp: true
|
||||
4
responses/ticket/supportembed.yaml
Normal file
4
responses/ticket/supportembed.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
color: "Aqua"
|
||||
title: "Support Ticket"
|
||||
description: "This is a support ticket."
|
||||
timestamp: true
|
||||
4
responses/user/birthdays.yaml
Normal file
4
responses/user/birthdays.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
color: "Blue"
|
||||
title: "Upcoming Birthdays"
|
||||
description: "{bdays}"
|
||||
timestamp: true
|
||||
2
responses/user/novoice.yaml
Normal file
2
responses/user/novoice.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
color: "Red"
|
||||
description: ":x: You are not in a voice channel!"
|
||||
8
responses/user/report_user.yaml
Normal file
8
responses/user/report_user.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
color: "Red"
|
||||
title: "Reported User"
|
||||
fields:
|
||||
- { name: "From", value: "`{reporter}`", inline: false }
|
||||
- { name: "Reported User", value: "{reported}", inline: false }
|
||||
- { name: "Reason", value: "```\n{reason}\n```", inline: false }
|
||||
timestamp: true
|
||||
110
src/commands/admin/adminStats.ts
Normal file
110
src/commands/admin/adminStats.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, PermissionFlagsBits } from "discord.js";
|
||||
import User from "../../models/User";
|
||||
import Team from "../../models/Team";
|
||||
import Database from "../../libs/Database";
|
||||
|
||||
export default class AdminStatsCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("serverstats", "View detailed statistics (Admin only)", "/serverstats");
|
||||
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("type")
|
||||
.setDescription("Statistics type")
|
||||
.addChoices(
|
||||
{ name: "Most Active Users", value: "active" },
|
||||
{ name: "Top Point Earners", value: "points" },
|
||||
{ name: "Team Activity", value: "teams" },
|
||||
{ name: "Overall Summary", value: "summary" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
|
||||
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
return interaction.reply({ content: "❌ Database not connected. Points system unavailable.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const type = interaction.options.getString("type", true);
|
||||
|
||||
if (type === "active") {
|
||||
const users = await User.find().sort({ lastActive: -1 }).limit(15).exec();
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("📊 Most Active Users")
|
||||
.setColor(0x22c55e)
|
||||
.setDescription(users.map((u, i) =>
|
||||
`**${i + 1}.** <@${u.userId}> - Last active <t:${Math.floor(u.lastActive.getTime() / 1000)}:R>\n` +
|
||||
` Questions: ${u.dailyQuestionsCompleted + u.weeklyQuestionsCompleted} | Points: ${u.points}`
|
||||
).join("\n\n"))
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
|
||||
} else if (type === "points") {
|
||||
const users = await User.find().sort({ points: -1 }).limit(15).exec();
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("💰 Top Point Earners")
|
||||
.setColor(0xfacc15)
|
||||
.setDescription(users.map((u, i) =>
|
||||
`**${i + 1}.** <@${u.userId}> - **${u.points}** points\n` +
|
||||
` Daily: ${u.dailyQuestionsCompleted} | Weekly: ${u.weeklyQuestionsCompleted}`
|
||||
).join("\n\n"))
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
|
||||
} else if (type === "teams") {
|
||||
const teams = await Team.find().sort({ memberCount: -1 }).exec();
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("👥 Team Activity")
|
||||
.setColor(0x60a5fa)
|
||||
.setDescription(teams.map((t, i) =>
|
||||
`**${i + 1}.** ${t.name} (Leader: <@${t.leaderId}>)\n` +
|
||||
` Members: ${t.memberCount} | Points: ${t.points} (Adjusted: ${t.adjustedPoints})`
|
||||
).join("\n\n"))
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
|
||||
} else if (type === "summary") {
|
||||
const totalUsers = await User.countDocuments();
|
||||
const totalTeams = await Team.countDocuments();
|
||||
const totalPoints = await User.aggregate([{ $group: { _id: null, total: { $sum: "$points" } } }]);
|
||||
const totalDaily = await User.aggregate([{ $group: { _id: null, total: { $sum: "$dailyQuestionsCompleted" } } }]);
|
||||
const totalWeekly = await User.aggregate([{ $group: { _id: null, total: { $sum: "$weeklyQuestionsCompleted" } } }]);
|
||||
const usersWithTeams = await User.countDocuments({ teamId: { $ne: null } });
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("📈 Overall Summary")
|
||||
.setColor(0x9333ea)
|
||||
.addFields(
|
||||
{ name: "Total Users", value: totalUsers.toString(), inline: true },
|
||||
{ name: "Total Teams", value: totalTeams.toString(), inline: true },
|
||||
{ name: "Users in Teams", value: `${usersWithTeams}/${totalUsers}`, inline: true },
|
||||
{ name: "Total Points Earned", value: (totalPoints[0]?.total || 0).toString(), inline: true },
|
||||
{ name: "Daily Questions Completed", value: (totalDaily[0]?.total || 0).toString(), inline: true },
|
||||
{ name: "Weekly Questions Completed", value: (totalWeekly[0]?.total || 0).toString(), inline: true }
|
||||
)
|
||||
.setFooter({ text: "System Statistics" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in admin-stats command:", error);
|
||||
return interaction.editReply({ content: "❌ An error occurred." });
|
||||
}
|
||||
}
|
||||
}
|
||||
245
src/commands/admin/growth.ts
Normal file
245
src/commands/admin/growth.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, PermissionFlagsBits, MessageFlags } from "discord.js";
|
||||
import { ChartJSNodeCanvas } from "chartjs-node-canvas";
|
||||
|
||||
export default class GuildGrowthCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("growth", "Generates a chart of guild member growth over time", "/growth");
|
||||
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("range")
|
||||
.setDescription("Time range to show")
|
||||
.addChoices(
|
||||
{ name: "30 days", value: "30" },
|
||||
{ name: "90 days", value: "90" },
|
||||
{ name: "180 days", value: "180" },
|
||||
{ name: "365 days", value: "365" },
|
||||
{ name: "All time", value: "all" }
|
||||
)
|
||||
.setRequired(false)
|
||||
);
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("granularity")
|
||||
.setDescription("Bucket granularity for the chart")
|
||||
.addChoices(
|
||||
{ name: "Daily", value: "daily" },
|
||||
{ name: "Weekly", value: "weekly" },
|
||||
{ name: "Monthly", value: "monthly" }
|
||||
)
|
||||
.setRequired(false)
|
||||
);
|
||||
this.data.addBooleanOption(option =>
|
||||
option.setName("refresh")
|
||||
.setDescription("Force refresh member cache from Discord")
|
||||
.setRequired(false)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const guild = interaction.guild;
|
||||
if (!guild) {
|
||||
return interaction.reply({ content: "This command must be run in a guild.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const refresh = interaction.options.getBoolean("refresh") || false;
|
||||
const granularity = (interaction.options.getString("granularity") || "monthly") as string;
|
||||
const members = await client.storage.fetchGuildMembers(client, guild.id, refresh);
|
||||
|
||||
const joinCounts = new Map<string, number>();
|
||||
let earliest: Date | null = null;
|
||||
let latest: Date | null = null;
|
||||
|
||||
members.forEach(member => {
|
||||
const j = member.joinedAt;
|
||||
if (!j) return;
|
||||
const day = j.toISOString().slice(0, 10);
|
||||
joinCounts.set(day, (joinCounts.get(day) || 0) + 1);
|
||||
if (!earliest || j < earliest) earliest = j;
|
||||
if (!latest || j > latest) latest = j;
|
||||
});
|
||||
|
||||
if (!earliest || !latest) {
|
||||
return interaction.editReply({ content: "No join data available for this guild." });
|
||||
}
|
||||
|
||||
const labels: string[] = [];
|
||||
const data: number[] = [];
|
||||
|
||||
const e = earliest as Date;
|
||||
const l = latest as Date;
|
||||
|
||||
const rangeOpt = interaction.options.getString("range") || "365";
|
||||
let start: Date;
|
||||
if (rangeOpt === "all") {
|
||||
start = new Date(Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), e.getUTCDate()));
|
||||
} else {
|
||||
const days = Math.max(1, parseInt(rangeOpt, 10) || 365);
|
||||
const desired = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
||||
const desiredUTC = new Date(Date.UTC(desired.getUTCFullYear(), desired.getUTCMonth(), desired.getUTCDate()));
|
||||
const earliestUTC = new Date(Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), e.getUTCDate()));
|
||||
start = desiredUTC > earliestUTC ? desiredUTC : earliestUTC;
|
||||
}
|
||||
const end = new Date(Date.UTC(l.getUTCFullYear(), l.getUTCMonth(), l.getUTCDate()));
|
||||
|
||||
|
||||
|
||||
const dailyCum = new Map<string, number>();
|
||||
let running = 0;
|
||||
for (let cursor = new Date(start); cursor <= end; cursor.setUTCDate(cursor.getUTCDate() + 1)) {
|
||||
const dayStr = cursor.toISOString().slice(0, 10);
|
||||
running += (joinCounts.get(dayStr) || 0);
|
||||
dailyCum.set(dayStr, running);
|
||||
}
|
||||
|
||||
const getCumUpTo = (dateStr: string) => {
|
||||
if (dailyCum.has(dateStr)) return dailyCum.get(dateStr) || 0;
|
||||
const keys = Array.from(dailyCum.keys()).sort();
|
||||
let res = 0;
|
||||
for (const k of keys) {
|
||||
if (k > dateStr) break;
|
||||
res = dailyCum.get(k) || 0;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
||||
|
||||
if (granularity === "daily") {
|
||||
const totalDays = Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
|
||||
const step = Math.max(1, Math.ceil(totalDays / 12));
|
||||
for (let d = 0; d < totalDays; d += step) {
|
||||
const dt = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate() + d));
|
||||
const label = dt.toISOString().slice(0, 10);
|
||||
const cum = getCumUpTo(label);
|
||||
labels.push(label);
|
||||
data.push(cum);
|
||||
}
|
||||
} else if (granularity === "weekly") {
|
||||
const totalDays = Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
|
||||
const totalWeeks = Math.ceil(totalDays / 7);
|
||||
const step = Math.max(1, Math.ceil(totalWeeks / 12));
|
||||
for (let w = 0; w < totalWeeks; w += step) {
|
||||
const weekStart = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate() + w * 7));
|
||||
const weekEnd = new Date(Date.UTC(weekStart.getUTCFullYear(), weekStart.getUTCMonth(), weekStart.getUTCDate() + 6));
|
||||
const label = `${MONTHS[weekStart.getUTCMonth()]} ${weekStart.getUTCDate()}, ${weekStart.getUTCFullYear()}`;
|
||||
const monthEndStr = weekEnd.toISOString().slice(0, 10);
|
||||
const cum = getCumUpTo(monthEndStr);
|
||||
labels.push(label);
|
||||
data.push(cum);
|
||||
}
|
||||
} else {
|
||||
const monthDiff = (a: Date, b: Date) => (b.getUTCFullYear() - a.getUTCFullYear()) * 12 + (b.getUTCMonth() - a.getUTCMonth());
|
||||
const totalMonths = monthDiff(start, end) + 1;
|
||||
const step = Math.max(1, Math.ceil(totalMonths / 12));
|
||||
for (let m = 0; m < totalMonths; m += step) {
|
||||
const dt = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + m, 1));
|
||||
const year = dt.getUTCFullYear();
|
||||
const month = dt.getUTCMonth();
|
||||
const label = `${MONTHS[month]} ${year}`;
|
||||
const monthEnd = new Date(Date.UTC(year, month + 1, 0));
|
||||
const monthEndStr = monthEnd.toISOString().slice(0, 10);
|
||||
const cum = getCumUpTo(monthEndStr);
|
||||
labels.push(label);
|
||||
data.push(cum);
|
||||
}
|
||||
}
|
||||
|
||||
const width = 1200;
|
||||
const height = 600;
|
||||
const canvasBackground = "#0b1220";
|
||||
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, backgroundColour: canvasBackground });
|
||||
|
||||
let trendData: number[] = [];
|
||||
if (data.length >= 2) {
|
||||
const n = data.length;
|
||||
const xs = data.map((_, i) => i);
|
||||
const ys = data;
|
||||
const sumX = xs.reduce((a, b) => a + b, 0);
|
||||
const sumY = ys.reduce((a, b) => a + b, 0);
|
||||
const sumXX = xs.reduce((a, b) => a + b * b, 0);
|
||||
const sumXY = xs.reduce((a, b, i) => a + b * ys[i], 0);
|
||||
const denom = n * sumXX - sumX * sumX;
|
||||
if (Math.abs(denom) < 1e-12) {
|
||||
trendData = ys.slice();
|
||||
} else {
|
||||
const slope = (n * sumXY - sumX * sumY) / denom;
|
||||
const intercept = (sumY - slope * sumX) / n;
|
||||
trendData = xs.map(x => intercept + slope * x);
|
||||
}
|
||||
} else {
|
||||
trendData = data.slice();
|
||||
}
|
||||
|
||||
const configuration: any = {
|
||||
type: "line",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Members",
|
||||
data,
|
||||
fill: true,
|
||||
borderColor: "#60a5fa",
|
||||
backgroundColor: "rgba(96,165,250,0.12)",
|
||||
tension: 0.2,
|
||||
pointRadius: 0,
|
||||
},
|
||||
{
|
||||
label: "Trend",
|
||||
data: trendData,
|
||||
fill: false,
|
||||
borderColor: "#facc15",
|
||||
borderDash: [6, 6],
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { labels: { color: "#e6eef8" } },
|
||||
title: { display: true, text: `Server Growth — ${guild.name}`, color: "#e6eef8", font: { size: 16 } },
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { maxRotation: 45, minRotation: 0, color: "#cbd5e1" },
|
||||
grid: { color: "rgba(255,255,255,0.04)" },
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { color: "#cbd5e1" },
|
||||
grid: { color: "rgba(255,255,255,0.04)" },
|
||||
},
|
||||
},
|
||||
backgroundColor: canvasBackground,
|
||||
responsive: false,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const buffer = await chartJSNodeCanvas.renderToBuffer(configuration);
|
||||
const attachment = new Discord.AttachmentBuilder(buffer, { name: "guild-growth.png" });
|
||||
await interaction.editReply({ content: `Guild growth for **${guild.name}** (members: ${guild.memberCount})`, files: [attachment] });
|
||||
} catch (err) {
|
||||
console.error("Error rendering guild growth chart:", err);
|
||||
await interaction.editReply({ content: "Failed to generate chart. See logs for details." });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error executing growth command:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred while generating the chart. Check logs for details." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred while generating the chart. Check logs for details.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/commands/admin/pinghelp.ts
Normal file
87
src/commands/admin/pinghelp.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, PermissionFlagsBits, MessageFlags, TextChannel } from "discord.js";
|
||||
|
||||
export default class PingHelpCommand extends BotCommand {
|
||||
private helperChannelID: string;
|
||||
|
||||
constructor() {
|
||||
super("pinghelp", "Pings the helper role", "/pinghelp");
|
||||
|
||||
this.helperChannelID = "1310993025958150166";
|
||||
|
||||
this.data.addRoleOption(option =>
|
||||
option.setName("role")
|
||||
.setDescription("The role to ping")
|
||||
.setRequired(true)
|
||||
);
|
||||
|
||||
this.data.addUserOption(option =>
|
||||
option.setName("user")
|
||||
.setDescription("The user to ping")
|
||||
.setRequired(true)
|
||||
);
|
||||
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("messageid")
|
||||
.setDescription("The message ID to ping")
|
||||
.setRequired(false)
|
||||
);
|
||||
|
||||
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
const role = interaction.options.getRole("role");
|
||||
const user = interaction.options.getUser("user");
|
||||
const messageId = interaction.options.getString("messageid");
|
||||
|
||||
if (!role) {
|
||||
return interaction.reply({
|
||||
content: "You must specify a role to ping.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
const helpType = role.name.split(" ")[0];
|
||||
|
||||
const helperChannel = await interaction.guild?.channels.fetch(this.helperChannelID) as TextChannel;
|
||||
if (!helperChannel) {
|
||||
return interaction.reply({
|
||||
content: "Helper channel not found.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
if (messageId) {
|
||||
const message = await interaction.channel?.messages.fetch(messageId).catch(() => null);
|
||||
if (!message) {
|
||||
return interaction.reply({
|
||||
content: "Invalid message ID provided.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
const messageLink = message.url;
|
||||
helperChannel.send({
|
||||
content: `${role} **\`${user?.username}\`** needs help => ${messageLink}. Please assist with their ${helpType} question.`,
|
||||
});
|
||||
|
||||
return interaction.reply({
|
||||
content: "Helper has been pinged!",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
} else {
|
||||
helperChannel.send({
|
||||
content: `${role} **\`${user?.username}\`** needs help => ${interaction.channel}. Please assist them.`,
|
||||
});
|
||||
|
||||
return interaction.reply({
|
||||
content: "Helper has been pinged!",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/commands/admin/regenerate.ts
Normal file
118
src/commands/admin/regenerate.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, PermissionFlagsBits } from "discord.js";
|
||||
import QuestionScheduler from "../../libs/QuestionScheduler";
|
||||
|
||||
export default class RegenerateQuestionCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("regenerate", "Regenerate a question for a specific subject and period (Admin only)", "/regenerate");
|
||||
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("period")
|
||||
.setDescription("Question period")
|
||||
.addChoices(
|
||||
{ name: "Daily", value: "daily" },
|
||||
{ name: "Weekly", value: "weekly" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("subject")
|
||||
.setDescription("Choose a subject")
|
||||
.addChoices(
|
||||
{ name: "Mathematics", value: "mathematics" },
|
||||
{ name: "Physics", value: "physics" },
|
||||
{ name: "Chemistry", value: "chemistry" },
|
||||
{ name: "Organic Chemistry", value: "organic chemistry" },
|
||||
{ name: "Biology", value: "biology" },
|
||||
{ name: "Computer Science", value: "computer science" },
|
||||
{ name: "Engineering", value: "engineering" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
|
||||
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const period = interaction.options.getString("period", true) as "daily" | "weekly";
|
||||
const subject = interaction.options.getString("subject", true);
|
||||
|
||||
const scheduler = new QuestionScheduler();
|
||||
await scheduler.initialize();
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🔄 Regenerating Question...")
|
||||
.setDescription(`Generating new ${period} question for **${subject}**...`)
|
||||
.setColor(0x60a5fa)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
|
||||
// Force regenerate by clearing cache for this period
|
||||
const regenerated = await scheduler.forceRegenerateQuestion(subject, period);
|
||||
|
||||
if (!regenerated) {
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle("❌ Regeneration Failed")
|
||||
.setDescription(`Failed to regenerate ${period} question for **${subject}**. Check console logs for details.`)
|
||||
.setColor(0xef4444)
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [errorEmbed] });
|
||||
}
|
||||
|
||||
const successEmbed = new EmbedBuilder()
|
||||
.setTitle("✅ Question Regenerated")
|
||||
.setDescription(`Successfully regenerated ${period} question for **${subject}**!`)
|
||||
.addFields(
|
||||
{ name: "Topic", value: regenerated.topic, inline: true },
|
||||
{ name: "Difficulty", value: regenerated.difficulty_rating, inline: true },
|
||||
{ name: "Question ID", value: regenerated.id, inline: false }
|
||||
)
|
||||
.setColor(0x22c55e)
|
||||
.setFooter({ text: `Users can now access the new question via /${period}` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [successEmbed] });
|
||||
|
||||
// Log to logs channel
|
||||
const logsChannelId = process.env.LOGS_CHANNEL_ID;
|
||||
if (logsChannelId) {
|
||||
try {
|
||||
const logsChannel = await client.channels.fetch(logsChannelId);
|
||||
if (logsChannel?.isTextBased()) {
|
||||
const logEmbed = new EmbedBuilder()
|
||||
.setTitle("🔄 Question Regenerated")
|
||||
.setColor(0x60a5fa)
|
||||
.addFields(
|
||||
{ name: "Period", value: period, inline: true },
|
||||
{ name: "Subject", value: subject, inline: true },
|
||||
{ name: "Admin", value: `<@${interaction.user.id}>`, inline: true },
|
||||
{ name: "New Topic", value: regenerated.topic, inline: false },
|
||||
{ name: "Question ID", value: regenerated.id, inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
await (logsChannel as any).send({ embeds: [logEmbed] });
|
||||
}
|
||||
} catch (logErr) {
|
||||
console.error("Failed to send log to logs channel:", logErr);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in regenerate command:", error);
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle("❌ Error")
|
||||
.setDescription("An unexpected error occurred while regenerating the question.")
|
||||
.setColor(0xef4444)
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [errorEmbed] });
|
||||
}
|
||||
}
|
||||
}
|
||||
210
src/commands/admin/selectroles.ts
Normal file
210
src/commands/admin/selectroles.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, PermissionFlagsBits, MessageFlags, TextChannel } from "discord.js";
|
||||
|
||||
export default class SelectRolesCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("selectroles", "Post the Embeds for roles", "/selectroles");
|
||||
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
|
||||
}
|
||||
|
||||
async languageRolesExecute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
const selectionMenu = new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId('select-roles-language')
|
||||
.setPlaceholder('Select the roles you want to add')
|
||||
.setMinValues(0)
|
||||
.setMaxValues(client.config.getLanguageRoles().length);
|
||||
|
||||
client.config.getLanguageRoles().forEach((role: any) => {
|
||||
const option = new Discord.StringSelectMenuOptionBuilder()
|
||||
.setLabel(role.name)
|
||||
.setValue(role.id)
|
||||
.setDescription(`If you can speak ${role.name}`)
|
||||
.setEmoji(role.emoji);
|
||||
|
||||
selectionMenu.addOptions(option);
|
||||
});
|
||||
|
||||
const row = new Discord.ActionRowBuilder()
|
||||
.addComponents(selectionMenu);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setColor("#2b2d31")
|
||||
.setTitle("Select the language roles that apply to you")
|
||||
.setDescription(`
|
||||
:flag_es: - If you can speak Spanish.
|
||||
:flag_fr: - If you can speak French.
|
||||
:flag_de: - If you can speak German.
|
||||
:flag_cn: - If you can speak Chinese.
|
||||
:flag_ru: - If you can speak Russian.
|
||||
:flag_il: - If you can speak Hebrew.
|
||||
:flag_sa: - If you can speak Arabic.
|
||||
:flag_in: - If you can speak Hindi.
|
||||
:flag_it: - If you can speak Italian.
|
||||
:flag_pt: - If you can speak Portuguese.
|
||||
`)
|
||||
.setFooter({ text: "You can select multiple roles" })
|
||||
.setTimestamp();
|
||||
|
||||
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
|
||||
}
|
||||
|
||||
async educationLevelRoles(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
const selectionMenu = new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId('select-roles-education')
|
||||
.setPlaceholder('Select the roles you want to add')
|
||||
.setMinValues(0)
|
||||
.setMaxValues(client.config.getEducationRoles().length);
|
||||
|
||||
client.config.getEducationRoles().forEach((role: any) => {
|
||||
const option = new Discord.StringSelectMenuOptionBuilder()
|
||||
.setLabel(role.name)
|
||||
.setValue(role.id)
|
||||
.setDescription(`If you are a ${role.name}`)
|
||||
.setEmoji(role.emoji);
|
||||
|
||||
selectionMenu.addOptions(option);
|
||||
});
|
||||
|
||||
const row = new Discord.ActionRowBuilder()
|
||||
.addComponents(selectionMenu);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setColor("#2b2d31")
|
||||
.setTitle("Select the education roles that apply to you")
|
||||
.setDescription(`
|
||||
:school: - If you are a High School Student
|
||||
:mortar_board: - If you are a Undergraduate Student
|
||||
👨🎓 - If you are a Postgraduate
|
||||
👨🔬 - If you are a PhD Student
|
||||
👨🏫 - If you are a Doctorate
|
||||
`)
|
||||
.setFooter({ text: "You can select multiple roles" })
|
||||
.setTimestamp();
|
||||
|
||||
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
|
||||
}
|
||||
|
||||
async sendHelperRoles(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
const selectionMenu = new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId('select-roles-helper')
|
||||
.setPlaceholder('Select the roles you want to add')
|
||||
.setMinValues(0)
|
||||
.setMaxValues(client.config.getHelperRoles().length);
|
||||
|
||||
client.config.getHelperRoles().forEach((role: any) => {
|
||||
const option = new Discord.StringSelectMenuOptionBuilder()
|
||||
.setLabel(role.name)
|
||||
.setValue(role.id)
|
||||
.setDescription(role.description)
|
||||
.setEmoji(role.emoji);
|
||||
|
||||
selectionMenu.addOptions(option);
|
||||
});
|
||||
|
||||
const row = new Discord.ActionRowBuilder()
|
||||
.addComponents(selectionMenu);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setColor("#2b2d31")
|
||||
.setTitle("Select the roles you want to add")
|
||||
.setDescription(`
|
||||
:test_tube: - If you can help with Chemistry.
|
||||
:atom: - If you can help with Physics.
|
||||
:infinity: - If you can help with Math.
|
||||
:dna: - If you can help with Biology.
|
||||
:man_technologist: - If you can help with Programming.
|
||||
:student: - I'm just a Student, hoping to learn.
|
||||
`)
|
||||
.setFooter({ text: "You can select multiple roles" })
|
||||
.setTimestamp();
|
||||
|
||||
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
|
||||
}
|
||||
|
||||
async sendLocationRoles(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
const selectionMenu = new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId('select-roles-location')
|
||||
.setPlaceholder('Select the roles you want to add')
|
||||
.setMinValues(0)
|
||||
.setMaxValues(client.config.getLocationRoles().length);
|
||||
|
||||
client.config.getLocationRoles().forEach((role: any) => {
|
||||
const option = new Discord.StringSelectMenuOptionBuilder()
|
||||
.setLabel(role.name)
|
||||
.setValue(role.id)
|
||||
.setDescription(role.description)
|
||||
.setEmoji(role.emoji);
|
||||
|
||||
selectionMenu.addOptions(option);
|
||||
});
|
||||
|
||||
const row = new Discord.ActionRowBuilder()
|
||||
.addComponents(selectionMenu);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setColor("#2b2d31")
|
||||
.setTitle("Select the location roles that apply to you")
|
||||
.setDescription(`
|
||||
:earth_americas: - If you're from North America.
|
||||
:earth_americas: - If you're from South America.
|
||||
:earth_africa: - If you're from Europe.
|
||||
:earth_asia: - If you're from Asia.
|
||||
:earth_africa: - If you're from Africa.
|
||||
:earth_asia: - If you're from Oceania.
|
||||
`)
|
||||
.setFooter({ text: "You can select multiple roles" })
|
||||
.setTimestamp();
|
||||
|
||||
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
|
||||
}
|
||||
|
||||
async sendPingRoles(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
const selectionMenu = new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId('select-roles-ping')
|
||||
.setPlaceholder('Select the roles you want to add')
|
||||
.setMinValues(0)
|
||||
.setMaxValues(client.config.getPingRoles().length);
|
||||
|
||||
client.config.getPingRoles().forEach((role: any) => {
|
||||
const option = new Discord.StringSelectMenuOptionBuilder()
|
||||
.setLabel(role.name)
|
||||
.setValue(role.id)
|
||||
.setDescription(role.description)
|
||||
.setEmoji(role.emoji);
|
||||
|
||||
selectionMenu.addOptions(option);
|
||||
});
|
||||
|
||||
const row = new Discord.ActionRowBuilder()
|
||||
.addComponents(selectionMenu);
|
||||
|
||||
let description = "";
|
||||
|
||||
client.config.getPingRoles().forEach((role: any) => {
|
||||
description += `${role.emoji} - ${role.description}\n`;
|
||||
});
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setColor("#2b2d31")
|
||||
.setTitle("Select the ping roles that apply to you")
|
||||
.setDescription(description)
|
||||
.setFooter({ text: "You can select multiple roles" })
|
||||
.setTimestamp();
|
||||
|
||||
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
await interaction.reply({ content: `⏳ One Moment Please. . .`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
await this.educationLevelRoles(Discord, client, interaction);
|
||||
await this.sendHelperRoles(Discord, client, interaction);
|
||||
await this.languageRolesExecute(Discord, client, interaction);
|
||||
await this.sendLocationRoles(Discord, client, interaction);
|
||||
await this.sendPingRoles(Discord, client, interaction);
|
||||
|
||||
return await interaction.deleteReply();
|
||||
}
|
||||
}
|
||||
181
src/commands/admin/teamManage.ts
Normal file
181
src/commands/admin/teamManage.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, PermissionFlagsBits } from "discord.js";
|
||||
import Team from "../../models/Team";
|
||||
import User from "../../models/User";
|
||||
import Database from "../../libs/Database";
|
||||
import PointsManager from "../../libs/PointsManager";
|
||||
|
||||
export default class TeamManageCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("team", "Create, edit, or delete teams (Admin only)", "/team");
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("create")
|
||||
.setDescription("Create a new team")
|
||||
.addStringOption(option =>
|
||||
option.setName("name")
|
||||
.setDescription("Team name")
|
||||
.setRequired(true))
|
||||
.addUserOption(option =>
|
||||
option.setName("leader")
|
||||
.setDescription("Team leader")
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName("description")
|
||||
.setDescription("Team description")
|
||||
.setRequired(false))
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("delete")
|
||||
.setDescription("Delete a team")
|
||||
.addStringOption(option =>
|
||||
option.setName("name")
|
||||
.setDescription("Team name")
|
||||
.setRequired(true))
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("change-leader")
|
||||
.setDescription("Change team leader")
|
||||
.addStringOption(option =>
|
||||
option.setName("name")
|
||||
.setDescription("Team name")
|
||||
.setRequired(true))
|
||||
.addUserOption(option =>
|
||||
option.setName("new-leader")
|
||||
.setDescription("New team leader")
|
||||
.setRequired(true))
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("list")
|
||||
.setDescription("List all teams")
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("change-user-team")
|
||||
.setDescription("Change a user's team (only during first week of month)")
|
||||
.addUserOption(option =>
|
||||
option.setName("user")
|
||||
.setDescription("User to move")
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName("team")
|
||||
.setDescription("Team name")
|
||||
.setRequired(true))
|
||||
);
|
||||
|
||||
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
return interaction.reply({ content: "❌ Database not connected. Points system unavailable.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
try {
|
||||
if (subcommand === "create") {
|
||||
const name = interaction.options.getString("name", true);
|
||||
const leader = interaction.options.getUser("leader", true);
|
||||
const description = interaction.options.getString("description") || "";
|
||||
|
||||
const existing = await Team.findOne({ name });
|
||||
if (existing) {
|
||||
return interaction.reply({ content: `❌ Team **${name}** already exists.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const team = new Team({
|
||||
name,
|
||||
leaderId: leader.id,
|
||||
description,
|
||||
points: 0,
|
||||
memberCount: 0
|
||||
});
|
||||
await team.save();
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("✅ Team Created")
|
||||
.setColor(0x22c55e)
|
||||
.addFields(
|
||||
{ name: "Team Name", value: name, inline: true },
|
||||
{ name: "Leader", value: `<@${leader.id}>`, inline: true },
|
||||
{ name: "Description", value: description || "None", inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
|
||||
} else if (subcommand === "delete") {
|
||||
const name = interaction.options.getString("name", true);
|
||||
const team = await Team.findOne({ name });
|
||||
|
||||
if (!team) {
|
||||
return interaction.reply({ content: `❌ Team **${name}** not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
await User.updateMany({ teamId: team._id }, { $set: { teamId: null } });
|
||||
await Team.deleteOne({ _id: team._id });
|
||||
|
||||
return interaction.reply({ content: `✅ Team **${name}** has been deleted.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
} else if (subcommand === "change-leader") {
|
||||
const name = interaction.options.getString("name", true);
|
||||
const newLeader = interaction.options.getUser("new-leader", true);
|
||||
const team = await Team.findOne({ name });
|
||||
|
||||
if (!team) {
|
||||
return interaction.reply({ content: `❌ Team **${name}** not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
team.leaderId = newLeader.id;
|
||||
await team.save();
|
||||
|
||||
return interaction.reply({ content: `✅ Team **${name}** leader changed to <@${newLeader.id}>.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
} else if (subcommand === "list") {
|
||||
const teams = await Team.find().sort({ points: -1 }).exec();
|
||||
|
||||
if (teams.length === 0) {
|
||||
return interaction.reply({ content: "No teams exist yet.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("📋 All Teams")
|
||||
.setColor(0x60a5fa)
|
||||
.setDescription(teams.map((t, i) =>
|
||||
`**${i + 1}.** ${t.name} - Leader: <@${t.leaderId}>\n` +
|
||||
` Members: ${t.memberCount} | Points: ${t.points} (Adjusted: ${t.adjustedPoints})`
|
||||
).join("\n\n"))
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
|
||||
} else if (subcommand === "change-user-team") {
|
||||
const user = interaction.options.getUser("user", true);
|
||||
const teamName = interaction.options.getString("team", true);
|
||||
const team = await Team.findOne({ name: teamName });
|
||||
|
||||
if (!team) {
|
||||
return interaction.reply({ content: `❌ Team **${teamName}** not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const result = await PointsManager.joinTeam(user.id, user.username, team._id.toString(), true);
|
||||
|
||||
if (result.success) {
|
||||
return interaction.reply({ content: `✅ <@${user.id}> has been moved to team **${teamName}**.`, flags: MessageFlags.Ephemeral });
|
||||
} else {
|
||||
return interaction.reply({ content: `❌ ${result.message}`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in team-manage command:", error);
|
||||
return interaction.reply({ content: "❌ An error occurred.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/commands/admin/test.ts
Normal file
17
src/commands/admin/test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
|
||||
export default class StatusCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("test", "Test Execute Command", "/test");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: any) {
|
||||
|
||||
await interaction.deferReply();
|
||||
await interaction.deleteReply();
|
||||
|
||||
// await interaction.reply({ content: "" });
|
||||
}
|
||||
}
|
||||
36
src/commands/core/help.ts
Normal file
36
src/commands/core/help.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ChatInputCommandInteraction, GuildMember, MessageFlags, PermissionFlagsBits } from "discord.js";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
|
||||
export default class HelpCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("help", "Help command", "/help");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction) {
|
||||
|
||||
let member = interaction.member as GuildMember;
|
||||
const isAdmin = (member.permissions.has(PermissionFlagsBits.Administrator) || member.permissions.has(PermissionFlagsBits.ManageGuild));
|
||||
|
||||
let embed = new Discord.EmbedBuilder()
|
||||
.setTitle("Help")
|
||||
.setColor("Aqua")
|
||||
.setTimestamp();
|
||||
|
||||
let description = "Here are the available commands:";
|
||||
let modDescription = "Here are the available mod commands:";
|
||||
|
||||
client.commands.forEach(command => {
|
||||
let commandData = command.data.default_member_permissions;
|
||||
if(commandData && isAdmin) {
|
||||
modDescription += `\n**${command.getName()}** - \`${command.getUse()}\` - ${command.data.description}`;
|
||||
} else {
|
||||
description += `\n**${command.getName()}** - \`${command.getUse()}\` - ${command.data.description}`;
|
||||
}
|
||||
});
|
||||
|
||||
embed.setDescription(isAdmin ? `${description}\n\n${modDescription}` : description);
|
||||
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
}
|
||||
39
src/commands/core/status.ts
Normal file
39
src/commands/core/status.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
import { MessageFlags } from "discord.js";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
|
||||
export default class StatusCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("status", "Bot Status Command", "/status");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: any) {
|
||||
|
||||
let totalSeconds = ((client.uptime || 0) / 1000);
|
||||
let days = Math.floor(totalSeconds / 86400);
|
||||
totalSeconds %= 86400;
|
||||
let hours = Math.floor(totalSeconds / 3600);
|
||||
totalSeconds %= 3600;
|
||||
let minutes = Math.floor(totalSeconds / 60);
|
||||
let seconds = Math.floor(totalSeconds % 60);
|
||||
|
||||
let obj = {
|
||||
bot: {
|
||||
name: client?.user?.username,
|
||||
icon: client?.user?.avatarURL(),
|
||||
stats: "Version: `" + `4.0.0` + "`\n" +
|
||||
"💻 **Client Latency**: `" + `${Date.now() - interaction.createdTimestamp}ms` + "`\n" +
|
||||
"📊 **API Latency**: `" + `${Math.round(client.ws.ping)}ms` + "`\n" +
|
||||
"📶 **Ping**: `" + `${client.ws.ping}ms` + "`\n" +
|
||||
":file_cabinet:**Memory**: `" + `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}mb` + "`"
|
||||
},
|
||||
uptime: `${days}d ${hours}h ${minutes}m ${seconds}s`,
|
||||
interaction
|
||||
}
|
||||
|
||||
const embed = client.formatter.buildEmbed("./responses/command/status.yaml", obj);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
32
src/commands/music/disconnect.ts
Normal file
32
src/commands/music/disconnect.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class DisconnectCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("disconnect", "Disconnect Command", "/disconnect");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
|
||||
if(queue) {
|
||||
client.distube.stop(interaction);
|
||||
client.distube.voices.leave(interaction.guild);
|
||||
} else {
|
||||
client.distube.voices.leave(interaction.guild);
|
||||
}
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Successfully **Disconnected** from the voice channel.`)
|
||||
.setFooter({ text: `Request by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL() });
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
34
src/commands/music/jump.ts
Normal file
34
src/commands/music/jump.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class JumpCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("jump", "Jump to a specific song in queue", "/jump");
|
||||
this.data.addNumberOption(option =>
|
||||
option.setName('position')
|
||||
.setDescription('Position in queue to jump to')
|
||||
.setRequired(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if (!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
const position = interaction.options.getNumber('position')!;
|
||||
|
||||
try {
|
||||
await queue.jump(position - 1); // Convert to 0-based index
|
||||
return await interaction.reply({ content: `⏭️ | Jumped to position ${position} in queue` });
|
||||
} catch (e) {
|
||||
return await interaction.reply({ content: `${e}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/commands/music/loop.ts
Normal file
40
src/commands/music/loop.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class LoopCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("loop", "Loop Command", "/loop");
|
||||
this.data.addStringOption(option =>
|
||||
option.setName('mode')
|
||||
.setDescription('Loop mode: off, song, queue')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: 'Off', value: '0' },
|
||||
{ name: 'Song', value: '1' },
|
||||
{ name: 'Queue', value: '2' }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if (!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
const mode = parseInt(interaction.options.getString('mode')!);
|
||||
|
||||
try {
|
||||
queue.setRepeatMode(mode);
|
||||
const modeText = mode === 0 ? "Off" : mode === 1 ? "Song" : "Queue";
|
||||
return await interaction.reply({ content: `🔁 | Loop mode set to: \`${modeText}\`` });
|
||||
} catch (e) {
|
||||
return await interaction.reply({ content: `${e}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/commands/music/nowplaying.ts
Normal file
72
src/commands/music/nowplaying.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class NowPlayingCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("nowplaying", "Now Playing Command", "/nowplaying");
|
||||
}
|
||||
|
||||
private progressbar(total: number, current: number, size: number, line: string, slider: string): string {
|
||||
if (current > total) {
|
||||
const bar = line.repeat(size + 2);
|
||||
return bar;
|
||||
} else {
|
||||
const percentage = current / total;
|
||||
const progress = Math.round((size * percentage));
|
||||
const emptyProgress = size - progress;
|
||||
const progressText = line.repeat(progress).replace(/.$/, slider);
|
||||
const emptyProgressText = line.repeat(emptyProgress);
|
||||
const bar = progressText + emptyProgressText;
|
||||
return bar;
|
||||
}
|
||||
}
|
||||
|
||||
private convertTime(duration: number): string {
|
||||
let seconds = parseInt(((duration / 1000) % 60).toString());
|
||||
let minutes = parseInt(((duration / (1000 * 60)) % 60).toString());
|
||||
let hours = parseInt(((duration / (1000 * 60 * 60)) % 24).toString());
|
||||
|
||||
const hoursStr = (hours < 10) ? "0" + hours : hours.toString();
|
||||
const minutesStr = (minutes < 10) ? "0" + minutes : minutes.toString();
|
||||
const secondsStr = (seconds < 10) ? "0" + seconds : seconds.toString();
|
||||
|
||||
if (duration < 3600000) {
|
||||
return minutesStr + ":" + secondsStr;
|
||||
} else {
|
||||
return hoursStr + ":" + minutesStr + ":" + secondsStr;
|
||||
}
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if (!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
const song = queue.songs[0];
|
||||
const currentTime = queue.currentTime;
|
||||
const totalTime = song.duration * 1000;
|
||||
|
||||
const progress = this.progressbar(totalTime, currentTime, 15, "▬", "🔵");
|
||||
const currentTimeStr = this.convertTime(currentTime);
|
||||
const totalTimeStr = this.convertTime(totalTime);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setTitle("Now Playing")
|
||||
.setDescription(`**[${song.name}](${song.url})**`)
|
||||
.addFields(
|
||||
{ name: "Duration", value: `\`${currentTimeStr}\` ${progress} \`${totalTimeStr}\``, inline: false },
|
||||
{ name: "Requested by", value: song.user.toString(), inline: true },
|
||||
{ name: "Volume", value: `${queue.volume}%`, inline: true }
|
||||
)
|
||||
.setThumbnail(song.thumbnail)
|
||||
.setColor("Blue");
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
30
src/commands/music/pause.ts
Normal file
30
src/commands/music/pause.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class PauseCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("pause", "Pause Command", "/pause");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if(!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
if(queue.paused) return await interaction.reply({ content: `❌ | The queue has been paused.` });
|
||||
|
||||
client.distube.pause(interaction);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`⏸ | Successfully **Paused** a song.`)
|
||||
.setFooter({ text: `Request by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL() });
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
35
src/commands/music/play.ts
Normal file
35
src/commands/music/play.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember, MessageFlags } from "discord.js";
|
||||
|
||||
export default class PlayCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("play", "Play Command", "/play");
|
||||
this.data.addStringOption(option =>
|
||||
option.setName('song')
|
||||
.setDescription('name or link of Music')
|
||||
.setRequired(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const song = interaction.options.getString('song');
|
||||
if(song != null) {
|
||||
|
||||
client.distube.play(member.voice.channel, song, {
|
||||
member: member,
|
||||
textChannel: interaction.channel
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: `Searching for ${song} . . .`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
return await interaction.reply({ content: `You must provide a song to play`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
27
src/commands/music/previous.ts
Normal file
27
src/commands/music/previous.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class PreviousCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("previous", "Play previous song", "/previous");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if (!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
try {
|
||||
const song = await queue.previous();
|
||||
return await interaction.reply({ content: `⏮️ | Playing previous song: ${song.name}` });
|
||||
} catch (e) {
|
||||
return await interaction.reply({ content: `${e}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/commands/music/queue.ts
Normal file
82
src/commands/music/queue.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class QueueCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("queue", "Queue Command", "/queue");
|
||||
}
|
||||
|
||||
private queueStatus(queue: any): string {
|
||||
const volume = queue.volume;
|
||||
const filter = queue.filter || "Off";
|
||||
const loop = queue.repeatMode ? queue.repeatMode === 2 ? "All Queue" : "This Song" : "Off";
|
||||
const autoplay = queue.autoplay ? "On" : "Off";
|
||||
|
||||
let status: string;
|
||||
|
||||
if (filter !== "Off" && loop === "Off" && autoplay === "Off") {
|
||||
status = `Filter: ${filter}`;
|
||||
}
|
||||
else if (filter !== "Off" && loop !== "Off" && autoplay === "Off") {
|
||||
status = `Filter: ${filter} | Loop: ${loop}`;
|
||||
}
|
||||
else if (filter !== "Off" && loop !== "Off" && autoplay !== "Off") {
|
||||
status = `Filter: ${filter} | Loop: ${loop} | Autoplay: ${autoplay}`;
|
||||
}
|
||||
else if (filter === "Off" && loop !== "Off" && autoplay !== "Off") {
|
||||
status = `Loop: ${loop} | Autoplay: ${autoplay}`;
|
||||
}
|
||||
else if (filter === "Off" && loop === "Off" && autoplay !== "Off") {
|
||||
status = `Autoplay: ${autoplay}`;
|
||||
}
|
||||
else {
|
||||
status = "";
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return `Volume: ${volume}%`;
|
||||
} else {
|
||||
return `Volume: ${volume}% | ${status}`;
|
||||
}
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if (!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
const songs = queue.songs;
|
||||
if (songs.length === 0) return await interaction.reply({ content: "❌ | The queue is empty!" });
|
||||
|
||||
const currentSong = songs[0];
|
||||
const upcomingSongs = songs.slice(1, 11); // Show up to 10 upcoming songs
|
||||
|
||||
let queueString = `**Now Playing:**\n🎵 [${currentSong.name}](${currentSong.url}) - \`${currentSong.formattedDuration}\`\n\n`;
|
||||
|
||||
if (upcomingSongs.length > 0) {
|
||||
queueString += "**Up Next:**\n";
|
||||
upcomingSongs.forEach((song: any, index: number) => {
|
||||
queueString += `\`${index + 1}.\` [${song.name}](${song.url}) - \`${song.formattedDuration}\`\n`;
|
||||
});
|
||||
|
||||
if (songs.length > 11) {
|
||||
queueString += `\n*...and ${songs.length - 11} more songs*`;
|
||||
}
|
||||
}
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setTitle("Music Queue")
|
||||
.setDescription(queueString)
|
||||
.setFooter({ text: this.queueStatus(queue) })
|
||||
.setColor("Blue")
|
||||
.setTimestamp();
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
30
src/commands/music/resume.ts
Normal file
30
src/commands/music/resume.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class ResumeCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("resume", "Resume Command", "/resume");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if(!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
if(!queue.paused) return await interaction.reply({ content: `❌ | The queue is being played.` });
|
||||
|
||||
client.distube.resume(interaction);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`▶ | Successfully **Resumed** a song.`)
|
||||
.setFooter({ text: `Request by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL() });
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
45
src/commands/music/seek.ts
Normal file
45
src/commands/music/seek.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class SeekCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("seek", "Seek to a specific time in the current song", "/seek");
|
||||
this.data.addStringOption(option =>
|
||||
option.setName('time')
|
||||
.setDescription('Time to seek to (e.g., 1:30 or 90)')
|
||||
.setRequired(true)
|
||||
);
|
||||
}
|
||||
|
||||
private parseTime(timeString: string): number {
|
||||
// Handle MM:SS format
|
||||
if (timeString.includes(':')) {
|
||||
const parts = timeString.split(':').map(part => parseInt(part));
|
||||
return parts.reduce((acc, time) => (60 * acc) + time) * 1000; // Convert to milliseconds
|
||||
}
|
||||
// Handle seconds format
|
||||
return parseInt(timeString) * 1000;
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if (!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
const timeString = interaction.options.getString('time')!;
|
||||
|
||||
try {
|
||||
const timeMs = this.parseTime(timeString);
|
||||
await queue.seek(timeMs);
|
||||
return await interaction.reply({ content: `⏩ | Seeked to ${timeString}` });
|
||||
} catch (e) {
|
||||
return await interaction.reply({ content: `${e}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/commands/music/skip.ts
Normal file
27
src/commands/music/skip.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class SkipCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("skip", "Skip a song or track", "/skip");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if (!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
try {
|
||||
const song = await queue.skip();
|
||||
return await interaction.reply({ content: `✅ | Skipped! Now playing:\n${song.name}` });
|
||||
} catch (e) {
|
||||
return await interaction.reply({ content: `${e}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/commands/music/stop.ts
Normal file
28
src/commands/music/stop.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class StopCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("stop", "Stop Command", "/stop");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if(!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
client.distube.stop(interaction);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(` Successfully **Stopped** the music.`)
|
||||
.setFooter({ text: `Request by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL() });
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
32
src/commands/music/volume.ts
Normal file
32
src/commands/music/volume.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||
|
||||
export default class VolumeCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("volume", "Volume Command", "/volume");
|
||||
this.data.addNumberOption(option =>
|
||||
option.setName('number').setDescription('Volume Level').setRequired(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
const volume = interaction.options.getNumber('number');
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if(!member?.voice?.channel) {
|
||||
return await interaction.reply({ content: "❌ | You need to be in a voice channel!" });
|
||||
}
|
||||
|
||||
const queue = client.distube.getQueue(interaction);
|
||||
if (!queue) return await interaction.reply({ content: `❌ | There is no music playing!` });
|
||||
|
||||
try {
|
||||
queue.setVolume(volume);
|
||||
return await interaction.reply({ content: `:white_check_mark: | Volume set to \`${volume?.toString()}\`` });
|
||||
} catch (e) {
|
||||
return await interaction.reply({ content: `${e}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
252
src/commands/users/brithday.ts
Normal file
252
src/commands/users/brithday.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags } from "discord.js";
|
||||
|
||||
export default class BirthdayCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("bday", "Birthday Command", "/bday");
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("set")
|
||||
.setDescription("Set your birthday")
|
||||
.addNumberOption(option =>
|
||||
option.setName("day")
|
||||
.setDescription("Your birthday day (1-31)")
|
||||
.setRequired(true)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option.setName("month")
|
||||
.setDescription("Your birthday month (1-12)")
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName("timezone")
|
||||
.setDescription("Your timezone (e.g., 'EST', GMT+2, etc.)")
|
||||
.setRequired(false)
|
||||
)
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("get")
|
||||
.setDescription("Get your birthday")
|
||||
.addUserOption(option =>
|
||||
option.setName("user")
|
||||
.setDescription("The user whose birthday you want to get")
|
||||
.setRequired(false)
|
||||
)
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("remove")
|
||||
.setDescription("Remove your birthday")
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("list")
|
||||
.setDescription("List all birthdays")
|
||||
);
|
||||
}
|
||||
|
||||
dateToEpooch(day: number, month: number, timezone?: string): number {
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
|
||||
let birthdayDate = new Date(currentYear, month - 1, day, 0, 0, 0, 0);
|
||||
|
||||
if (birthdayDate < now) {
|
||||
birthdayDate = new Date(currentYear + 1, month - 1, day, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
if (timezone) {
|
||||
const offsetHours = this.getTimezoneOffset(timezone);
|
||||
birthdayDate.setHours(birthdayDate.getHours() - offsetHours);
|
||||
}
|
||||
|
||||
return Math.floor(birthdayDate.getTime() / 1000);
|
||||
}
|
||||
|
||||
getTimezoneOffset(timezone: string): number {
|
||||
const tz = timezone.toLowerCase();
|
||||
|
||||
// Handle GMT+/-N format
|
||||
const gmtMatch = tz.match(/^gmt([+-])(\d+)$/);
|
||||
if (gmtMatch) {
|
||||
const sign = gmtMatch[1] === '+' ? 1 : -1;
|
||||
const hours = parseInt(gmtMatch[2]);
|
||||
return sign * hours;
|
||||
}
|
||||
|
||||
// Handle UTC+/-N format
|
||||
const utcMatch = tz.match(/^utc([+-])(\d+)$/);
|
||||
if (utcMatch) {
|
||||
const sign = utcMatch[1] === '+' ? 1 : -1;
|
||||
const hours = parseInt(utcMatch[2]);
|
||||
return sign * hours;
|
||||
}
|
||||
|
||||
// Predefined timezone offsets (in hours from UTC)
|
||||
const offsets: { [key: string]: number } = {
|
||||
"est": -5, // Eastern Standard Time
|
||||
"edt": -4, // Eastern Daylight Time
|
||||
"cst": -6, // Central Standard Time
|
||||
"cdt": -5, // Central Daylight Time
|
||||
"mst": -7, // Mountain Standard Time
|
||||
"mdt": -6, // Mountain Daylight Time
|
||||
"pst": -8, // Pacific Standard Time
|
||||
"pdt": -7, // Pacific Daylight Time
|
||||
"gmt": 0, // Greenwich Mean Time
|
||||
"utc": 0 // Coordinated Universal Time
|
||||
};
|
||||
|
||||
return offsets[tz] || 0; // Default to UTC if not found
|
||||
}
|
||||
|
||||
async setCommand(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const day = interaction.options.getNumber("day")!;
|
||||
const month = interaction.options.getNumber("month")!;
|
||||
|
||||
let timezone = interaction.options.getString("timezone");
|
||||
timezone = timezone ? timezone.trim() : null;
|
||||
timezone = timezone ? timezone.toLowerCase() : null;
|
||||
|
||||
// Validate timezone format to lowercase alphanumeric and special characters
|
||||
if (timezone && !/^[a-zA-Z0-9+_-]+$/.test(timezone)) {
|
||||
return interaction.reply({
|
||||
content: "Invalid timezone format. Please use alphanumeric characters, plus (+), underscore (_), or hyphen (-).",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
if (isNaN(day) || isNaN(month)) {
|
||||
return interaction.reply({
|
||||
content: "Invalid date provided. Please ensure the day and month are numbers.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
if (day < 1 || day > 31 || month < 1 || month > 12) {
|
||||
return interaction.reply({
|
||||
content: "Invalid date provided. Please ensure the day is between 1-31 and the month is between 1-12.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
client.config.setBday(interaction.user.id, { day, month, timezone: timezone || undefined });
|
||||
|
||||
return interaction.reply({
|
||||
content: `Your birthday has been set to ${day}/${month} ${timezone ? `in timezone ${timezone}` : ''}.\nYour next birthday will be on <t:${this.dateToEpooch(day, month, timezone || undefined)}:D>.`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
async getCommand(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const user = interaction.options.getUser("user") || interaction.user;
|
||||
const bday = client.config.getBday(user.id);
|
||||
|
||||
if (!bday) {
|
||||
return interaction.reply({
|
||||
content: `\`${user.username}\` has not set a birthday.`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
const timezone = bday.timezone ? ` in timezone ${bday.timezone}` : '';
|
||||
return interaction.reply({
|
||||
content: `
|
||||
\`${user.username}\`'s birthday is set to ${bday.month}/${bday.day}${timezone}.\nYour next birthday will be on <t:${this.dateToEpooch(bday.day, bday.month, bday.timezone)}:D>.
|
||||
`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
async removeCommand(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const user = interaction.user;
|
||||
const bday = client.config.getBday(user.id);
|
||||
if (!bday) {
|
||||
return interaction.reply({
|
||||
content: `\`${user.username}\`, you have not set a birthday to remove.`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
client.config.removeBday(user.id);
|
||||
return interaction.reply({
|
||||
content: `\`${user.username}\`, your birthday has been removed.`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
async listCommand(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const bdays = client.config.getBdays();
|
||||
if (!bdays || Object.keys(bdays).length === 0) {
|
||||
return interaction.reply({
|
||||
content: "No birthdays have been set yet.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
// Upcoming birthdays
|
||||
let response = "";
|
||||
const today = new Date();
|
||||
const upcoming = Object.entries(bdays).map(([userId, bday]) => {
|
||||
const nextBirthday = new Date(today.getFullYear(), bday.month - 1, bday.day);
|
||||
if (nextBirthday < today) {
|
||||
nextBirthday.setFullYear(today.getFullYear() + 1); // Move to next year if birthday has passed
|
||||
}
|
||||
return { userId, bday, nextBirthday };
|
||||
}).sort((a, b) => a.nextBirthday.getTime() - b.nextBirthday.getTime());
|
||||
|
||||
// Filter out birthdays that are more than 30 days away
|
||||
const thirtyDaysFromNow = new Date(today);
|
||||
thirtyDaysFromNow.setDate(today.getDate() + 30);
|
||||
const filteredUpcoming = upcoming.filter(b => b.nextBirthday <= thirtyDaysFromNow);
|
||||
if (filteredUpcoming.length === 0) {
|
||||
response = "No upcoming birthdays within the next 30 days.";
|
||||
return interaction.reply({
|
||||
content: response,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
for (const { userId, bday, nextBirthday } of upcoming) {
|
||||
const user = await client.users.fetch(userId);
|
||||
response += `\`${user.username}\`: <t:${Math.floor(nextBirthday.getTime() / 1000)}:D>\n`;
|
||||
}
|
||||
|
||||
if (upcoming.length === 0) {
|
||||
response = "No upcoming birthdays.";
|
||||
}
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setTitle("Upcoming Birthdays")
|
||||
.setDescription(response)
|
||||
.setColor("Blue")
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [embed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
switch (subcommand) {
|
||||
case "set":
|
||||
return this.setCommand(Discord, client, interaction);
|
||||
case "get":
|
||||
return this.getCommand(Discord, client, interaction);
|
||||
case "remove":
|
||||
return this.removeCommand(Discord, client, interaction);
|
||||
case "list":
|
||||
return this.listCommand(Discord, client, interaction);
|
||||
default:
|
||||
return interaction.reply({
|
||||
content: "Unknown subcommand.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/commands/users/daily.ts
Normal file
121
src/commands/users/daily.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, TextChannel } from "discord.js";
|
||||
import QuestionScheduler from "../../libs/QuestionScheduler";
|
||||
import AnswerGrader from "../../libs/AnswerGrader";
|
||||
|
||||
const SUBJECTS = ["Mathematics", "Physics", "Chemistry", "Biology", "Computer Science", "Engineering"];
|
||||
|
||||
export default class DailyCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("daily", "Answer today's daily STEM question", "/daily");
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("subject")
|
||||
.setDescription("Choose a subject")
|
||||
.addChoices(
|
||||
{ name: "Mathematics", value: "mathematics" },
|
||||
{ name: "Physics", value: "physics" },
|
||||
{ name: "Chemistry", value: "chemistry" },
|
||||
{ name: "Organic Chemistry", value: "organic chemistry" },
|
||||
{ name: "Biology", value: "biology" },
|
||||
{ name: "Computer Science", value: "computer science" },
|
||||
{ name: "Engineering", value: "engineering" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const subject = interaction.options.getString("subject", true);
|
||||
|
||||
const scheduler = new QuestionScheduler();
|
||||
await scheduler.initialize();
|
||||
|
||||
const question = await scheduler.getQuestionForPeriod(subject, "daily");
|
||||
|
||||
if (!question) {
|
||||
return interaction.editReply({ content: `Failed to load today's ${subject} question. Please try again later.` });
|
||||
}
|
||||
|
||||
const hasAnswered = await scheduler.hasUserAnswered(interaction.user.id, question.id);
|
||||
if (hasAnswered) {
|
||||
return interaction.editReply({ content: "You've already submitted an answer for today's question! Check back tomorrow." });
|
||||
}
|
||||
|
||||
const imagePath = await client.typst.renderToImage(question.typst_source);
|
||||
|
||||
if (!imagePath) {
|
||||
// Log error to logs channel
|
||||
const logsChannelId = process.env.LOGS_CHANNEL_ID;
|
||||
if (logsChannelId) {
|
||||
try {
|
||||
const logsChannel = await client.channels.fetch(logsChannelId) as TextChannel;
|
||||
if (logsChannel?.isTextBased()) {
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle("❌ Typst Render Error - Daily Question")
|
||||
.setColor(0xef4444)
|
||||
.addFields(
|
||||
{ name: "Subject", value: subject, inline: true },
|
||||
{ name: "Question ID", value: question.id, inline: true },
|
||||
{ name: "Topic", value: question.topic, inline: false },
|
||||
{ name: "User", value: `<@${interaction.user.id}>`, inline: true },
|
||||
{ name: "Error", value: "Failed to render Typst image", inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
await logsChannel.send({ embeds: [errorEmbed] });
|
||||
}
|
||||
} catch (logErr) {
|
||||
console.error("Failed to send error to logs channel:", logErr);
|
||||
}
|
||||
}
|
||||
return interaction.editReply({
|
||||
content: `❌ An error occurred while generating the question image. This has been reported to administrators.`
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`📅 Daily ${subject} Question`)
|
||||
.setDescription(`**Topic:** ${question.topic}\n**Difficulty:** ${question.difficulty_rating}`)
|
||||
.setColor(0x60a5fa)
|
||||
.setImage(`attachment://daily_${subject}.png`)
|
||||
.setFooter({ text: `Resets at 12 AM local time` })
|
||||
.setTimestamp();
|
||||
|
||||
const attachment = new AttachmentBuilder(imagePath, { name: `daily_${subject}.png` });
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`daily_answer_${question.id}`)
|
||||
.setLabel("Submit Answer")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji("✍️"),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`daily_report_${question.id}`)
|
||||
.setLabel("Report Issue")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setEmoji("⚠️")
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [embed],
|
||||
files: [attachment],
|
||||
components: [row],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error executing daily command:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred. Please try again later." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred. Please try again later.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/commands/users/joinTeam.ts
Normal file
61
src/commands/users/joinTeam.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder } from "discord.js";
|
||||
import Team from "../../models/Team";
|
||||
import PointsManager from "../../libs/PointsManager";
|
||||
import Database from "../../libs/Database";
|
||||
|
||||
export default class JoinTeamCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("jointeam", "Join a team", "/jointeam");
|
||||
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("team")
|
||||
.setDescription("Team name to join")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
return interaction.reply({ content: "❌ Database not connected. Points system unavailable.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const teamName = interaction.options.getString("team", true);
|
||||
const team = await Team.findOne({ name: teamName });
|
||||
|
||||
if (!team) {
|
||||
return interaction.editReply({ content: `❌ Team **${teamName}** not found.` });
|
||||
}
|
||||
|
||||
const result = await PointsManager.joinTeam(interaction.user.id, interaction.user.username, team._id.toString());
|
||||
|
||||
if (result.success) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("✅ Team Joined!")
|
||||
.setColor(0x22c55e)
|
||||
.setDescription(result.message)
|
||||
.addFields(
|
||||
{ name: "Team", value: team.name, inline: true },
|
||||
{ name: "Members", value: team.memberCount.toString(), inline: true },
|
||||
{ name: "Team Points", value: `${team.adjustedPoints}`, inline: true }
|
||||
)
|
||||
.setFooter({ text: "Earn points for your team by completing questions!" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ content: null, embeds: [embed] });
|
||||
} else {
|
||||
return interaction.editReply({ content: `❌ ${result.message}` });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in join-team command:", error);
|
||||
return interaction.editReply({ content: "❌ An error occurred while joining the team." });
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/commands/users/leaderboard.ts
Normal file
95
src/commands/users/leaderboard.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, PermissionFlagsBits } from "discord.js";
|
||||
import PointsManager from "../../libs/PointsManager";
|
||||
import Database from "../../libs/Database";
|
||||
|
||||
export default class LeaderboardCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("leaderboard", "View user or team leaderboards", "/leaderboard");
|
||||
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("type")
|
||||
.setDescription("Leaderboard type")
|
||||
.addChoices(
|
||||
{ name: "Users", value: "users" },
|
||||
{ name: "Teams", value: "teams" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
|
||||
this.data.addIntegerOption(option =>
|
||||
option.setName("limit")
|
||||
.setDescription("Number of entries to show (default: 10)")
|
||||
.setMinValue(5)
|
||||
.setMaxValue(25)
|
||||
.setRequired(false)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
return interaction.reply({ content: "❌ Database not connected. Points system unavailable.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.deferReply();
|
||||
|
||||
const type = interaction.options.getString("type", true);
|
||||
const limit = interaction.options.getInteger("limit") || 10;
|
||||
|
||||
if (type === "users") {
|
||||
const users = await PointsManager.getLeaderboard(limit);
|
||||
|
||||
if (users.length === 0) {
|
||||
return interaction.editReply({ content: "No users found in the leaderboard yet." });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🏆 User Leaderboard")
|
||||
.setColor(0xfacc15)
|
||||
.setDescription(users.map((u, i) => {
|
||||
const medal = i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : `**${i + 1}.**`;
|
||||
return `${medal} <@${u.userId}> - **${u.points}** points\n` +
|
||||
` 📊 Daily: ${u.dailyQuestionsCompleted} | Weekly: ${u.weeklyQuestionsCompleted}`;
|
||||
}).join("\n\n"))
|
||||
.setFooter({ text: "Complete questions to earn points!" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
|
||||
} else {
|
||||
const teams = await PointsManager.getTeamLeaderboard(limit);
|
||||
|
||||
if (teams.length === 0) {
|
||||
return interaction.editReply({ content: "No teams found in the leaderboard yet." });
|
||||
}
|
||||
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) || false;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🏆 Team Leaderboard")
|
||||
.setColor(0x60a5fa)
|
||||
.setDescription(teams.map((t, i) => {
|
||||
const medal = i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : `**${i + 1}.**`;
|
||||
if (isAdmin) {
|
||||
return `${medal} **${t.name}** - **${t.adjustedPoints}** points (adjusted)\n` +
|
||||
` Members: ${t.memberCount} | Raw Points: ${t.points}`;
|
||||
} else {
|
||||
return `${medal} **${t.name}** - **${t.adjustedPoints}** points\n` +
|
||||
` Members: ${t.memberCount}`;
|
||||
}
|
||||
}).join("\n\n"))
|
||||
.setFooter({ text: "Smaller teams get bonus multipliers!" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in leaderboard command:", error);
|
||||
return interaction.editReply({ content: "❌ An error occurred." });
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/commands/users/report.ts
Normal file
32
src/commands/users/report.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction } from "discord.js";
|
||||
|
||||
export default class ReportCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("report", "Report Command", "/report");
|
||||
this.data.addUserOption(option => option.setName("user").setDescription("User to report").setRequired(true));
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
const user = interaction.options.getUser('user');
|
||||
|
||||
const modal = new Discord.ModalBuilder()
|
||||
.setCustomId(`report-${user?.id}`)
|
||||
.setTitle("Report User")
|
||||
|
||||
const reasonInput = new Discord.TextInputBuilder()
|
||||
.setCustomId("reasonInput")
|
||||
.setLabel("Enter a reason")
|
||||
.setStyle(Discord.TextInputStyle.Paragraph)
|
||||
.setPlaceholder('Please be specific and include any evidence.\nCopy and paste any messages or message links.')
|
||||
.setRequired(true);
|
||||
|
||||
const firstActionRow = new Discord.ActionRowBuilder().addComponents(reasonInput);
|
||||
|
||||
modal.addComponents(firstActionRow);
|
||||
|
||||
return await interaction.showModal(modal);
|
||||
}
|
||||
}
|
||||
45
src/commands/users/solved.ts
Normal file
45
src/commands/users/solved.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember, ThreadChannel } from "discord.js";
|
||||
|
||||
export default class SolvedCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("solved", "Solved Command", "/solved");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
await interaction.reply({ content: `⏳ One Moment Please. . .` });
|
||||
|
||||
const channel = interaction.channel as ThreadChannel;
|
||||
const isThreadOwner = channel?.ownerId === interaction.user.id;
|
||||
const StaffRoles = ["1310700035842768908", "1311937894306414632"];
|
||||
const member = interaction.member as GuildMember;
|
||||
const isStaff = member?.roles?.cache?.some(role => StaffRoles.includes(role.id));
|
||||
|
||||
if(!isThreadOwner && !isStaff) {
|
||||
return await interaction.editReply({ content: "You do not have permission to use this command." });
|
||||
}
|
||||
|
||||
const availableTags = (channel?.parent as any)?.availableTags;
|
||||
const channelTags = channel?.appliedTags;
|
||||
|
||||
if (!availableTags || !channelTags) {
|
||||
return await interaction.editReply({ content: "This command can only be used in thread channels." });
|
||||
}
|
||||
|
||||
const solvedTag = availableTags.find((tag: any) => tag.name === "Solved");
|
||||
|
||||
if (!solvedTag) {
|
||||
return await interaction.editReply({ content: "No 'Solved' tag found in this forum." });
|
||||
}
|
||||
|
||||
if(!channelTags.includes(solvedTag.id)) {
|
||||
await channel?.setAppliedTags([...channelTags, solvedTag.id]);
|
||||
return await interaction.editReply({ content: "This channel has been marked `Solved`" });
|
||||
} else {
|
||||
await channel?.setAppliedTags(channelTags.filter((tag: string) => tag !== solvedTag.id));
|
||||
return await interaction.editReply({ content: "This channel has been unmarked `Solved`" });
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/commands/users/stats.ts
Normal file
60
src/commands/users/stats.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder } from "discord.js";
|
||||
import PointsManager from "../../libs/PointsManager";
|
||||
import Database from "../../libs/Database";
|
||||
|
||||
export default class StatsCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("stats", "View your stats or another user's stats", "/stats");
|
||||
|
||||
this.data.addUserOption(option =>
|
||||
option.setName("user")
|
||||
.setDescription("User to view stats for (leave empty for yourself)")
|
||||
.setRequired(false)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
return interaction.reply({ content: "❌ Database not connected. Points system unavailable.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const targetUser = interaction.options.getUser("user") || interaction.user;
|
||||
const userStats = await PointsManager.getUserStats(targetUser.id);
|
||||
|
||||
if (!userStats) {
|
||||
return interaction.editReply({
|
||||
content: `${targetUser.id === interaction.user.id ? "You haven't" : `<@${targetUser.id}> hasn't`} completed any questions yet.`
|
||||
});
|
||||
}
|
||||
|
||||
const teamInfo = userStats.teamId ? (userStats.teamId as any).name : "No team";
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`📊 Stats for ${targetUser.username}`)
|
||||
.setColor(0x60a5fa)
|
||||
.setThumbnail(targetUser.displayAvatarURL())
|
||||
.addFields(
|
||||
{ name: "Points", value: `**${userStats.points}**`, inline: true },
|
||||
{ name: "Team", value: teamInfo, inline: true },
|
||||
{ name: "Joined", value: `<t:${Math.floor(userStats.joinedAt.getTime() / 1000)}:R>`, inline: true },
|
||||
{ name: "Daily Questions", value: userStats.dailyQuestionsCompleted.toString(), inline: true },
|
||||
{ name: "Weekly Questions", value: userStats.weeklyQuestionsCompleted.toString(), inline: true },
|
||||
{ name: "Last Active", value: `<t:${Math.floor(userStats.lastActive.getTime() / 1000)}:R>`, inline: true }
|
||||
)
|
||||
.setFooter({ text: "Keep solving to climb the leaderboard!" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in stats command:", error);
|
||||
return interaction.editReply({ content: "❌ An error occurred." });
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/commands/users/weekly.ts
Normal file
121
src/commands/users/weekly.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, TextChannel } from "discord.js";
|
||||
import QuestionScheduler from "../../libs/QuestionScheduler";
|
||||
import AnswerGrader from "../../libs/AnswerGrader";
|
||||
|
||||
const SUBJECTS = ["Mathematics", "Physics", "Chemistry", "Biology", "Computer Science", "Engineering"];
|
||||
|
||||
export default class WeeklyCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("weekly", "Answer this week's weekly STEM question", "/weekly");
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("subject")
|
||||
.setDescription("Choose a subject")
|
||||
.addChoices(
|
||||
{ name: "Mathematics", value: "mathematics" },
|
||||
{ name: "Physics", value: "physics" },
|
||||
{ name: "Chemistry", value: "chemistry" },
|
||||
{ name: "Organic Chemistry", value: "organic chemistry" },
|
||||
{ name: "Biology", value: "biology" },
|
||||
{ name: "Computer Science", value: "computer science" },
|
||||
{ name: "Engineering", value: "engineering" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const subject = interaction.options.getString("subject", true);
|
||||
|
||||
const scheduler = new QuestionScheduler();
|
||||
await scheduler.initialize();
|
||||
|
||||
const question = await scheduler.getQuestionForPeriod(subject, "weekly");
|
||||
|
||||
if (!question) {
|
||||
return interaction.editReply({ content: `Failed to load this week's ${subject} question. Please try again later.` });
|
||||
}
|
||||
|
||||
const hasAnswered = await scheduler.hasUserAnswered(interaction.user.id, question.id);
|
||||
if (hasAnswered) {
|
||||
return interaction.editReply({ content: "You've already submitted an answer for this week's question! Check back next Sunday." });
|
||||
}
|
||||
|
||||
const imagePath = await client.typst.renderToImage(question.typst_source);
|
||||
|
||||
if (!imagePath) {
|
||||
// Log error to logs channel
|
||||
const logsChannelId = process.env.LOGS_CHANNEL_ID;
|
||||
if (logsChannelId) {
|
||||
try {
|
||||
const logsChannel = await client.channels.fetch(logsChannelId) as TextChannel;
|
||||
if (logsChannel?.isTextBased()) {
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle("❌ Typst Render Error - Weekly Question")
|
||||
.setColor(0xef4444)
|
||||
.addFields(
|
||||
{ name: "Subject", value: subject, inline: true },
|
||||
{ name: "Question ID", value: question.id, inline: true },
|
||||
{ name: "Topic", value: question.topic, inline: false },
|
||||
{ name: "User", value: `<@${interaction.user.id}>`, inline: true },
|
||||
{ name: "Error", value: "Failed to render Typst image", inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
await logsChannel.send({ embeds: [errorEmbed] });
|
||||
}
|
||||
} catch (logErr) {
|
||||
console.error("Failed to send error to logs channel:", logErr);
|
||||
}
|
||||
}
|
||||
return interaction.editReply({
|
||||
content: `❌ An error occurred while generating the question image. This has been reported to administrators.`
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`🎓 Weekly ${subject} Question (PhD Level)`)
|
||||
.setDescription(`**Topic:** ${question.topic}\n**Difficulty:** ${question.difficulty_rating}`)
|
||||
.setColor(0xfacc15)
|
||||
.setImage(`attachment://weekly_${subject}.png`)
|
||||
.setFooter({ text: `Resets at 12 AM Sunday` })
|
||||
.setTimestamp();
|
||||
|
||||
const attachment = new AttachmentBuilder(imagePath, { name: `weekly_${subject}.png` });
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`weekly_answer_${question.id}`)
|
||||
.setLabel("Submit Answer")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji("✍️"),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`weekly_report_${question.id}`)
|
||||
.setLabel("Report Issue")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setEmoji("⚠️")
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [embed],
|
||||
files: [attachment],
|
||||
components: [row],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error executing weekly command:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred. Please try again later." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred. Please try again later.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/events/bot/client/clientReady.ts
Normal file
19
src/events/bot/client/clientReady.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { post } from "../../../handlers/command_handler";
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
import ms from "ms";
|
||||
|
||||
export default async(Discord: any, client: BotClient) => {
|
||||
client.user?.setPresence({ activities: [{ name: '/help' }] });
|
||||
try {
|
||||
post(client);
|
||||
|
||||
setInterval(() => {
|
||||
client.emit("birthdayCheck");
|
||||
}, ms('30m'));
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
console.log(`Logged in as ${client.user?.tag}!`);
|
||||
}
|
||||
}
|
||||
76
src/events/bot/custom/aichat.ts
Normal file
76
src/events/bot/custom/aichat.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import fs from 'fs';
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient, message: any) => {
|
||||
// let content = message.content;
|
||||
|
||||
// if (message.channel && typeof message.channel.sendTyping === 'function') {
|
||||
// try {
|
||||
// await message.channel.sendTyping();
|
||||
// } catch (e) {
|
||||
// console.error('Error sending typing indicator:', e);
|
||||
// }
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const { content: messageContent, typst } = await client.ai.generateText(message.author.id, message.author.username, content);
|
||||
// const userId = message.author.id;
|
||||
// const typPath = `./storage/typst/message_${userId}.typ`;
|
||||
// const pdfPath = `./storage/typst/message_${userId}.pdf`;
|
||||
// try {
|
||||
// fs.writeFileSync(typPath, typst || messageContent);
|
||||
// } catch (err) {
|
||||
// console.error('Error writing typst file:', err);
|
||||
// await message.reply({
|
||||
// content: "Failed to write Typst file.",
|
||||
// allowedMentions: { repliedUser: true }
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
// const { execSync } = require('child_process');
|
||||
// let pdfBuffer = null;
|
||||
// let hasPDF = false;
|
||||
// try {
|
||||
// execSync(`typst compile "${typPath}" "${pdfPath}"`);
|
||||
// if (fs.existsSync(pdfPath)) {
|
||||
// try {
|
||||
// pdfBuffer = fs.readFileSync(pdfPath);
|
||||
// hasPDF = true;
|
||||
// } catch (readErr) {
|
||||
// console.error('Error reading PDF file:', readErr);
|
||||
// }
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error('Typst compile error:', e);
|
||||
// await message.reply({
|
||||
// content: "Failed to compile Typst to PDF.",
|
||||
// allowedMentions: { repliedUser: true }
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
// const messageOptions: any = {
|
||||
// content: messageContent,
|
||||
// allowedMentions: { repliedUser: true }
|
||||
// };
|
||||
// if (hasPDF && pdfBuffer) {
|
||||
// messageOptions.files = [
|
||||
// {
|
||||
// attachment: pdfBuffer,
|
||||
// name: `message_${userId}.pdf`
|
||||
// }
|
||||
// ];
|
||||
// }
|
||||
// console.log('Sending message with options:', {
|
||||
// contentLength: messageContent.length,
|
||||
// hasFiles: !!messageOptions.files,
|
||||
// fileCount: messageOptions.files ? messageOptions.files.length : 0
|
||||
// });
|
||||
// await message.reply(messageOptions);
|
||||
// } catch (error) {
|
||||
// console.error('Error in AI chat:', error);
|
||||
// await message.reply({
|
||||
// content: "I'm having trouble processing your request right now. Please try again later.",
|
||||
// allowedMentions: { repliedUser: true }
|
||||
// });
|
||||
// }
|
||||
};
|
||||
115
src/events/bot/custom/birthdayCheck.ts
Normal file
115
src/events/bot/custom/birthdayCheck.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
|
||||
const birthdayRole = "1385648190551888014";
|
||||
const chemistryGuildID = "1310183749320835173";
|
||||
|
||||
function getTimezoneOffset(timezone: string): number {
|
||||
const tz = timezone.toLowerCase();
|
||||
|
||||
// Handle GMT+/-N format
|
||||
const gmtMatch = tz.match(/^gmt([+-])(\d+)$/);
|
||||
if (gmtMatch) {
|
||||
const sign = gmtMatch[1] === '+' ? 1 : -1;
|
||||
const hours = parseInt(gmtMatch[2]);
|
||||
return sign * hours;
|
||||
}
|
||||
|
||||
// Handle UTC+/-N format
|
||||
const utcMatch = tz.match(/^utc([+-])(\d+)$/);
|
||||
if (utcMatch) {
|
||||
const sign = utcMatch[1] === '+' ? 1 : -1;
|
||||
const hours = parseInt(utcMatch[2]);
|
||||
return sign * hours;
|
||||
}
|
||||
|
||||
// Predefined timezone offsets (in hours from UTC)
|
||||
const offsets: { [key: string]: number } = {
|
||||
"est": -5, // Eastern Standard Time
|
||||
"edt": -4, // Eastern Daylight Time
|
||||
"cst": -6, // Central Standard Time
|
||||
"cdt": -5, // Central Daylight Time
|
||||
"mst": -7, // Mountain Standard Time
|
||||
"mdt": -6, // Mountain Daylight Time
|
||||
"pst": -8, // Pacific Standard Time
|
||||
"pdt": -7, // Pacific Daylight Time
|
||||
"gmt": 0, // Greenwich Mean Time
|
||||
"utc": 0 // Coordinated Universal Time
|
||||
};
|
||||
|
||||
return offsets[tz] || 0; // Default to UTC if not found
|
||||
}
|
||||
|
||||
function getTodayInTimezone(timezone?: string): Date {
|
||||
const now = new Date();
|
||||
|
||||
if (!timezone) {
|
||||
const today = new Date(now);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today;
|
||||
}
|
||||
|
||||
const offsetHours = getTimezoneOffset(timezone);
|
||||
const userTime = new Date(now.getTime() + (offsetHours * 60 * 60 * 1000));
|
||||
|
||||
userTime.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
return userTime;
|
||||
}
|
||||
|
||||
export default async(Discord: any, client: BotClient) => {
|
||||
|
||||
let bdays = client.config.getBdays();
|
||||
|
||||
if (!bdays || Object.keys(bdays).length === 0) return;
|
||||
|
||||
let guild = client.guilds.cache.get(chemistryGuildID);
|
||||
if (!guild) return console.error("Guild not found");
|
||||
|
||||
let birthdayRoleObj = guild.roles.cache.get(birthdayRole);
|
||||
if (!birthdayRoleObj) return console.error("Birthday role not found");
|
||||
|
||||
for (let userId in bdays) {
|
||||
let bday = bdays[userId];
|
||||
|
||||
if (!bday || !bday.day || !bday.month) {
|
||||
console.warn(`Invalid birthday data for user ${userId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { day, month, timezone } = bday;
|
||||
|
||||
const todayInUserTimezone = getTodayInTimezone(timezone);
|
||||
|
||||
const isBirthday = (
|
||||
todayInUserTimezone.getUTCDate() === day &&
|
||||
todayInUserTimezone.getUTCMonth() === month - 1
|
||||
);
|
||||
|
||||
let member = await guild.members.fetch(userId);
|
||||
if (!member) {
|
||||
console.warn(`Member with ID ${userId} not found in the guild.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isBirthday) {
|
||||
if (!member.roles.cache.has(birthdayRole)) {
|
||||
try {
|
||||
console.log(`🎉 It's ${member.user.tag}'s birthday!`);
|
||||
await member.roles.add(birthdayRoleObj);
|
||||
console.log(`✅ Added birthday role to ${member.user.tag}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to add birthday role to ${member.user.tag}:`, error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (member.roles.cache.has(birthdayRole)) {
|
||||
try {
|
||||
await member.roles.remove(birthdayRoleObj);
|
||||
console.log(`🗑️ Removed birthday role from ${member.user.tag}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to remove birthday role from ${member.user.tag}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/events/bot/custom/typst.ts
Normal file
50
src/events/bot/custom/typst.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
|
||||
export default async (Discord: any, client: BotClient, message: any) => {
|
||||
try {
|
||||
if (message?.author?.bot) return;
|
||||
|
||||
const match = message.content?.match(/```typ\s*\n([\s\S]*?)\n```/i);
|
||||
if (!match) return;
|
||||
|
||||
const typSource = match[1];
|
||||
const messageId = message.id?.toString();
|
||||
if (messageId && client.typst.hasMessage(messageId)) return;
|
||||
|
||||
try { if (message.channel?.sendTyping) await message.channel.sendTyping(); } catch (e) {}
|
||||
|
||||
const res = await client.typst.compile(typSource, { cleanup: true });
|
||||
if (res.error) {
|
||||
await message.reply({ content: `Typst compile error: ${res.error}`, allowedMentions: { repliedUser: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
const buffers = res.buffers ?? [];
|
||||
if (buffers.length === 0) {
|
||||
await message.reply({ content: 'Compilation finished but no PNG files were produced.', allowedMentions: { repliedUser: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToSend = buffers.map((b: Buffer, i: number) => ({ attachment: b, name: `typ_${messageId ?? Date.now()}_${i+1}.png` }));
|
||||
|
||||
const makeDeleteRow = () => {
|
||||
const btn = new Discord.ButtonBuilder().setCustomId('typst_delete').setLabel('Delete').setStyle(Discord.ButtonStyle.Danger);
|
||||
return new Discord.ActionRowBuilder().addComponents(btn);
|
||||
};
|
||||
|
||||
const replyOpts = { content: `Here's your Typst render (${filesToSend.length} page(s)):`, files: filesToSend, components: [makeDeleteRow()], allowedMentions: { repliedUser: true } };
|
||||
|
||||
const replyMsg = await message.reply(replyOpts);
|
||||
if (messageId && replyMsg?.id) {
|
||||
client.typst.addMessage(messageId, replyMsg.id, message.author?.id);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
client.typst.removeMessage(messageId);
|
||||
try { await replyMsg.edit({ components: [] }); } catch (e) {}
|
||||
} catch (e) {}
|
||||
}, 2 * 60 * 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('typst message handler error:', err);
|
||||
}
|
||||
};
|
||||
6
src/events/bot/guild/guildMemberAdd.ts
Normal file
6
src/events/bot/guild/guildMemberAdd.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient, member: any) => {
|
||||
let memberRole = await member.guild.roles.cache.find((role: any) => role.id === '1310840470037069936');
|
||||
member.roles.add(memberRole);
|
||||
}
|
||||
54
src/events/bot/guild/interactionCreate.ts
Normal file
54
src/events/bot/guild/interactionCreate.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
import { PermissionFlagsBits } from "discord.js";
|
||||
import { MessageFlags } from "discord.js";
|
||||
import Team from "../../../models/Team";
|
||||
|
||||
export default async(Discord: any, client: BotClient, interaction: any) => {
|
||||
|
||||
if(interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) {
|
||||
client.action.forEach((value: any, key: string) => {
|
||||
if(interaction.customId.includes(key)) return value.execute(Discord, client, interaction);
|
||||
});
|
||||
};
|
||||
|
||||
if (interaction.isAutocomplete()) {
|
||||
if (interaction.commandName === "jointeam") {
|
||||
const focusedValue = interaction.options.getFocused().toLowerCase();
|
||||
const teams = await Team.find().exec();
|
||||
const filtered = teams
|
||||
.filter(t => t.name.toLowerCase().includes(focusedValue))
|
||||
.slice(0, 25)
|
||||
.map(t => ({ name: t.name, value: t.name }));
|
||||
return interaction.respond(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.isChatInputCommand()) {
|
||||
|
||||
if(process.env.isDev == "true" && !(interaction.member.permissions.has(PermissionFlagsBits.ManageGuild) || interaction.member.permissions.has(PermissionFlagsBits.Administrator))) {
|
||||
return interaction.reply({ content: `:x: - You are not allowed to use this bot in a development environment`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
let command = client.commands.get(interaction.commandName);
|
||||
if(!command) return;
|
||||
if(!command.isEnabled()) return interaction.reply({ content: `:x: - This Command is not Enabled`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
try {
|
||||
client.logger.log(`&2Command Executed: &f${interaction.commandName} - &5${interaction.user.username}`);
|
||||
await command.execute(Discord, client, interaction);
|
||||
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
client.logger.log(`An error occured while executing the command: ${interaction.commandName}`);
|
||||
client.logger.log(`${error}`);
|
||||
|
||||
const embed = client.formatter.buildEmbed("./responses/error.yaml");
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
};
|
||||
|
||||
if(interaction.isCommand()) {
|
||||
if(!interaction.isChatInputCommand()) client.logger.log(`&2Interaction created: &f${interaction.commandName} - &5${interaction.user.username}`);
|
||||
};
|
||||
}
|
||||
39
src/events/bot/guild/messageCreate.ts
Normal file
39
src/events/bot/guild/messageCreate.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
import ms from "ms";
|
||||
|
||||
let cooldown = false;
|
||||
|
||||
export default async(Discord: any, client: BotClient, message: any) => {
|
||||
|
||||
if(message.channel.id == "1311034475727028305") {
|
||||
if(message.author.bot) return;
|
||||
message.react(message.guild.emojis.cache.get('1310753663001563156'))
|
||||
}
|
||||
|
||||
if(message.author.bot) return;
|
||||
|
||||
// typst code block -> emit typst event
|
||||
try {
|
||||
const hasTypst = /```typ\s*\n([\s\S]*?)\n```/i.test(message.content || "");
|
||||
if(hasTypst) {
|
||||
client.emit("typst", message);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error checking for typst block:', e);
|
||||
}
|
||||
|
||||
const replyText = client.config.getReplyText();
|
||||
if(replyText) {
|
||||
for(const [key, value] of Object.entries(replyText)) {
|
||||
if(message.content == key && !cooldown) {
|
||||
cooldown = true;
|
||||
setTimeout(() => {
|
||||
cooldown = false;
|
||||
}, ms("10s"));
|
||||
return message.reply(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
69
src/events/bot/guild/messageUpdate.ts
Normal file
69
src/events/bot/guild/messageUpdate.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient, oldMessage: any, newMessage: any) => {
|
||||
try {
|
||||
if (!newMessage || newMessage.author?.bot) return;
|
||||
|
||||
const match = newMessage.content?.match(/```typ\s*\n([\s\S]*?)\n```/i);
|
||||
if (!match) return;
|
||||
|
||||
const userMsgId = newMessage.id?.toString();
|
||||
if (!userMsgId) return;
|
||||
|
||||
if (!client.typst.hasMessage(userMsgId)) return;
|
||||
|
||||
const typSource = match[1];
|
||||
|
||||
const res = await client.typst.compile(typSource, { cleanup: true });
|
||||
if (res.error) {
|
||||
const botReplyId = client.typst.getResponse(userMsgId);
|
||||
if (botReplyId && newMessage.channel?.messages) {
|
||||
try {
|
||||
const botMsg = await newMessage.channel.messages.fetch(botReplyId);
|
||||
if (botMsg) await botMsg.edit({ content: `Typst compile error: ${res.error}` });
|
||||
} catch (e) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.buffers || res.buffers.length === 0) return;
|
||||
|
||||
const filesToSend = res.buffers.map((b: Buffer, i: number) => ({ attachment: b, name: `typ_${userMsgId}_${i+1}.png` }));
|
||||
|
||||
const makeDeleteRow = () => {
|
||||
const deleteBtn = new Discord.ButtonBuilder().setCustomId('typst_delete').setLabel('Delete').setStyle(Discord.ButtonStyle.Danger);
|
||||
return new Discord.ActionRowBuilder().addComponents(deleteBtn);
|
||||
};
|
||||
|
||||
const replyOptions = { content: `Here's your updated Typst render (${filesToSend.length} page(s)):`, files: filesToSend, components: [makeDeleteRow()], allowedMentions: { repliedUser: true } };
|
||||
|
||||
const botReplyId = client.typst.getResponse(userMsgId);
|
||||
let sent: any = null;
|
||||
|
||||
if (botReplyId && newMessage.channel?.messages) {
|
||||
try {
|
||||
const botMsg = await newMessage.channel.messages.fetch(botReplyId);
|
||||
if (botMsg) {
|
||||
try { await botMsg.delete(); } catch (e) {}
|
||||
sent = await newMessage.reply(replyOptions);
|
||||
}
|
||||
} catch (e) {
|
||||
try { sent = await newMessage.reply(replyOptions); } catch (e) {}
|
||||
}
|
||||
} else {
|
||||
try { sent = await newMessage.reply(replyOptions); } catch (e) {}
|
||||
}
|
||||
|
||||
if (sent && sent.id) {
|
||||
client.typst.addMessage(userMsgId, sent.id, newMessage.author?.id);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
client.typst.removeMessage(userMsgId);
|
||||
try { await sent.edit({ components: [] }); } catch (e) {}
|
||||
} catch (e) {}
|
||||
}, 2 * 60 * 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('messageUpdate typst handler error:', err);
|
||||
}
|
||||
}
|
||||
202
src/events/bot/guild/threadCreate.ts
Normal file
202
src/events/bot/guild/threadCreate.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import BotClient from "../../../libs/BotClient";
|
||||
import ms from "ms";
|
||||
|
||||
async function sendTimeOutMessage(client: BotClient, threadChannel: any, roleID: string, category: string) {
|
||||
|
||||
let SolvedTag = await threadChannel.parent.availableTags.find((tag: any) => tag.name === "Solved");
|
||||
if(threadChannel.appliedTags.includes(SolvedTag.id)) return;
|
||||
|
||||
let threadMembers = await threadChannel.guildMembers;
|
||||
|
||||
let helperRoles = client.config.getHelperRoles();
|
||||
helperRoles = helperRoles.filter((role: any) => role.name !== "Student");
|
||||
helperRoles = helperRoles.map((role: any) => role.id);
|
||||
|
||||
let helperInChannel = false;
|
||||
threadMembers.forEach(async(threadMember: any) => {
|
||||
if(threadMember.user.id == threadChannel.ownerId) return;
|
||||
let member = await threadChannel.guild.members.fetch(threadMember.user.id);
|
||||
let memberRoles = await member.roles.cache.map((role: any) => role.id);
|
||||
memberRoles.forEach((role: any) => {
|
||||
if (helperRoles.includes(role)) {
|
||||
helperInChannel = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(async() => {
|
||||
if(helperInChannel) return;
|
||||
let threadOwner = await threadChannel.guild.members.fetch(threadChannel.ownerId);
|
||||
|
||||
let helperChannel = await threadChannel.guild.channels.cache.find((channel: any) => channel.id === "1310993025958150166");
|
||||
await helperChannel.send({
|
||||
content: `<@&${roleID}> **${threadOwner.displayName}** has created ${threadChannel.url}. Please help the user with their ${category} question.`
|
||||
});
|
||||
}, ms("5s"));
|
||||
}
|
||||
|
||||
async function mathHelpChannel(Discord: any, client: BotClient, threadChannel: any) {
|
||||
const confirm = new Discord.ButtonBuilder()
|
||||
.setCustomId('btn-rule-agree')
|
||||
.setLabel('I Agree')
|
||||
.setStyle(Discord.ButtonStyle.Success);
|
||||
|
||||
const threadEmbed = new Discord.EmbedBuilder()
|
||||
.setColor("Red")
|
||||
.setTitle("Math Help")
|
||||
.setDescription(`
|
||||
Make sure to follow the rules and guidelines of the server. <#1310725282357055518>
|
||||
If you need help with a math problem, please provide the problem and any work you have done so far.
|
||||
If you need help with a concept, please provide the concept you need help with.
|
||||
Remember to be respectful and patient with others.
|
||||
Ask your question and someone will help you as soon as possible.
|
||||
Click the button below to confirm that you have read and understood the rules.
|
||||
`)
|
||||
.setTimestamp()
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(confirm);
|
||||
|
||||
setTimeout(() => { threadChannel.send({ embeds: [threadEmbed], components: [row] }); }, ms("2s"));
|
||||
|
||||
setTimeout(async() => {
|
||||
await sendTimeOutMessage(client, threadChannel, "1310718557016948788", "math");
|
||||
}, ms("30m"));
|
||||
}
|
||||
|
||||
async function programmingHelpChannel(Discord: any, client: BotClient, threadChannel: any) {
|
||||
const confirm = new Discord.ButtonBuilder()
|
||||
.setCustomId('btn-rule-agree')
|
||||
.setLabel('I Agree')
|
||||
.setStyle(Discord.ButtonStyle.Success);
|
||||
|
||||
const threadEmbed = new Discord.EmbedBuilder()
|
||||
.setColor("Red")
|
||||
.setTitle("Programming Help")
|
||||
.setDescription(`
|
||||
Make sure to follow the rules and guidelines of the server. <#1310725282357055518>
|
||||
If you need help with a programming problem, please provide the problem and any code you have written so far.
|
||||
If you need help with a concept, please provide the concept you need help with.
|
||||
Remember to be respectful and patient with others.
|
||||
Ask your question and someone will help you as soon as possible.
|
||||
Click the button below to confirm that you have read and understood the rules.
|
||||
`)
|
||||
.setTimestamp()
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(confirm);
|
||||
|
||||
setTimeout(() => { threadChannel.send({ embeds: [threadEmbed], components: [row] }); }, ms("2s"));
|
||||
|
||||
setTimeout(async() => {
|
||||
await sendTimeOutMessage(client, threadChannel, "1310720256561381457", "programming");
|
||||
}, ms("30m"));
|
||||
}
|
||||
|
||||
async function chemistryHelpChannel(Discord: any, client: BotClient, threadChannel: any) {
|
||||
const confirm = new Discord.ButtonBuilder()
|
||||
.setCustomId('btn-rule-agree')
|
||||
.setLabel('I Agree')
|
||||
.setStyle(Discord.ButtonStyle.Success);
|
||||
|
||||
const threadEmbed = new Discord.EmbedBuilder()
|
||||
.setColor("Red")
|
||||
.setTitle("Chemistry Help")
|
||||
.setDescription(`
|
||||
Make sure to follow the rules and guidelines of the server. <#1310725282357055518>
|
||||
If you need help with a chemistry problem, please provide the problem and any work you have done so far.
|
||||
If you need help with a concept, please provide the concept you need help with.
|
||||
Remember to be respectful and patient with others.
|
||||
Ask your question and someone will help you as soon as possible.
|
||||
Click the button below to confirm that you have read and understood the rules.
|
||||
`)
|
||||
.setTimestamp()
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(confirm);
|
||||
|
||||
setTimeout(() => { threadChannel.send({ embeds: [threadEmbed], components: [row] }); }, ms("2s"));
|
||||
|
||||
setTimeout(async() => {
|
||||
await sendTimeOutMessage(client, threadChannel, "1310715870821220497", "chemistry");
|
||||
}, ms("30m"));
|
||||
}
|
||||
|
||||
async function physicsHelpChannel(Discord: any, client: BotClient, threadChannel: any) {
|
||||
const confirm = new Discord.ButtonBuilder()
|
||||
.setCustomId('btn-rule-agree')
|
||||
.setLabel('I Agree')
|
||||
.setStyle(Discord.ButtonStyle.Success);
|
||||
|
||||
const threadEmbed = new Discord.EmbedBuilder()
|
||||
.setColor("Red")
|
||||
.setTitle("Physics Help")
|
||||
.setDescription(`
|
||||
Make sure to follow the rules and guidelines of the server. <#1310725282357055518>
|
||||
If you need help with a physics problem, please provide the problem and any work you have done so far.
|
||||
If you need help with a concept, please provide the concept you need help with.
|
||||
Remember to be respectful and patient with others.
|
||||
Ask your question and someone will help you as soon as possible.
|
||||
Click the button below to confirm that you have read and understood the rules.
|
||||
`)
|
||||
.setTimestamp()
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(confirm);
|
||||
|
||||
setTimeout(() => { threadChannel.send({ embeds: [threadEmbed], components: [row] }); }, ms("2s"));
|
||||
|
||||
setTimeout(async() => {
|
||||
await sendTimeOutMessage(client, threadChannel, "1310717935588868206", "physics");
|
||||
}, ms("30m"));
|
||||
}
|
||||
|
||||
async function biologyHelpChannel(Discord: any, client: BotClient, threadChannel: any) {
|
||||
const confirm = new Discord.ButtonBuilder()
|
||||
.setCustomId('btn-rule-agree')
|
||||
.setLabel('I Agree')
|
||||
.setStyle(Discord.ButtonStyle.Success);
|
||||
|
||||
const threadEmbed = new Discord.EmbedBuilder()
|
||||
.setColor("Red")
|
||||
.setTitle("Biology Help")
|
||||
.setDescription(`
|
||||
Make sure to follow the rules and guidelines of the server. <#1310725282357055518>
|
||||
If you need help with a biology problem, please provide the problem and any work you have done so far.
|
||||
If you need help with a concept, please provide the concept you need help with.
|
||||
Remember to be respectful and patient with others.
|
||||
Ask your question and someone will help you as soon as possible.
|
||||
Click the button below to confirm that you have read and understood the rules.
|
||||
`)
|
||||
.setTimestamp()
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(confirm);
|
||||
|
||||
setTimeout(() => { threadChannel.send({ embeds: [threadEmbed], components: [row] }); }, ms("2s"));
|
||||
|
||||
setTimeout(async() => {
|
||||
await sendTimeOutMessage(client, threadChannel, "1310719093996785705", "biology");
|
||||
}, ms("30m"));
|
||||
}
|
||||
|
||||
export default async(Discord: any, client: BotClient, threadChannel: any) => {
|
||||
let parent = threadChannel.parent;
|
||||
switch (parent.name) {
|
||||
case "math-help":
|
||||
await mathHelpChannel(Discord, client, threadChannel);
|
||||
break;
|
||||
case "programming-help":
|
||||
await programmingHelpChannel(Discord, client, threadChannel);
|
||||
break;
|
||||
case "high-school-chemistry":
|
||||
await chemistryHelpChannel(Discord, client, threadChannel);
|
||||
break;
|
||||
case "general-chemistry":
|
||||
await chemistryHelpChannel(Discord, client, threadChannel);
|
||||
break;
|
||||
case "physics-help":
|
||||
await physicsHelpChannel(Discord, client, threadChannel);
|
||||
break;
|
||||
case "biology-help":
|
||||
await biologyHelpChannel(Discord, client, threadChannel);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
11
src/events/distube/addSong.ts
Normal file
11
src/events/distube/addSong.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import BotClient from "../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient, queue: any, song: any) => {
|
||||
|
||||
let addSong = new Discord.EmbedBuilder()
|
||||
.setDescription(` Your Song has been added \n[${song.name}](${song.url}) - \`[${song.formattedDuration}]\``)
|
||||
.setThumbnail(song.thumbnail)
|
||||
|
||||
await queue.textChannel.send({ embeds: [addSong] })
|
||||
|
||||
}
|
||||
10
src/events/distube/disconnect.ts
Normal file
10
src/events/distube/disconnect.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import BotClient from "../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient, queue: any) => {
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`✅ | **Leave** the voice channel.\nThank you for using ${client?.user?.username}!`)
|
||||
.setFooter({ text: client?.user?.username, iconURL: client?.user?.displayAvatarURL() });
|
||||
queue.textChannel.send({ embeds: [embed] });
|
||||
|
||||
}
|
||||
11
src/events/distube/error.ts
Normal file
11
src/events/distube/error.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import BotClient from "../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient, error: any, queue: any, song: any) => {
|
||||
|
||||
console.log(error);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`❌ | There was an Error\n ${error}`)
|
||||
queue.textChannel.send({ embeds: [embed] })
|
||||
|
||||
}
|
||||
10
src/events/distube/finish.ts
Normal file
10
src/events/distube/finish.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import BotClient from "../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient, queue: any) => {
|
||||
|
||||
let playSong = new Discord.EmbedBuilder()
|
||||
.setDescription(`No more song in queue`)
|
||||
|
||||
queue.textChannel.send({ embeds: [playSong] })
|
||||
|
||||
}
|
||||
7
src/events/distube/placeholder.ts
Normal file
7
src/events/distube/placeholder.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
import BotClient from "../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient) => {
|
||||
|
||||
// SOMETHING HERE
|
||||
}
|
||||
12
src/events/distube/playSong.ts
Normal file
12
src/events/distube/playSong.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import BotClient from "../../libs/BotClient";
|
||||
|
||||
export default async(Discord: any, client: BotClient, queue: any, song: any) => {
|
||||
|
||||
let playSong = new Discord.EmbedBuilder()
|
||||
.setThumbnail(song.thumbnail)
|
||||
.setDescription(`Song Playing \n[${song.name}](${song.url}) - \`[${song.formattedDuration}]\``)
|
||||
.setFooter({ text: `Request by ${song.user.tag}`, iconURL: song.user.displayAvatarURL() });
|
||||
|
||||
queue.textChannel.send({ embeds: [playSong] })
|
||||
|
||||
}
|
||||
38
src/handlers/command_handler.ts
Normal file
38
src/handlers/command_handler.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { REST, Routes, SlashCommandBuilder } from "discord.js";
|
||||
import BotClient from "../libs/BotClient";
|
||||
import BotCommand from "../libs/BotCommand";
|
||||
|
||||
// @ts-ignore
|
||||
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
|
||||
|
||||
export default (Discord: any, client: BotClient) => {
|
||||
fs.readdirSync(path.join(__dirname, "../commands")).forEach((dir: string) => {
|
||||
fs.readdirSync(path.join(__dirname, `../commands/${dir}`)).forEach( async(file: string) => {
|
||||
let command: BotCommand = new (await( await import(path.join(__dirname, `../commands/${dir}/${file}`)))).default();
|
||||
client.commands.set(command.name, command);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function post(client: BotClient) {
|
||||
let botCommands: Array<SlashCommandBuilder> = [];
|
||||
client.commands.forEach((value, key) => {
|
||||
botCommands.push(value.data);
|
||||
});
|
||||
|
||||
try {
|
||||
client.guilds.cache.forEach(async (guild) => {
|
||||
await rest.put(
|
||||
// @ts-ignore
|
||||
Routes.applicationGuildCommands(client.user.id, guild.id),
|
||||
{ body: botCommands }
|
||||
);
|
||||
client.logger.log(`&bPosted (/) Commands for &f${guild.name}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
52
src/handlers/event_handler.ts
Normal file
52
src/handlers/event_handler.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import ms from "ms";
|
||||
|
||||
import BotClient from "../libs/BotClient";
|
||||
|
||||
let botEvents: Array<string> = [];
|
||||
|
||||
const wait = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay));
|
||||
|
||||
function isEvent(file: string): boolean {
|
||||
return file.endsWith(".ts") || file.endsWith(".js");
|
||||
}
|
||||
|
||||
function ScanDir(Discord: any, client: BotClient, dir: string) {
|
||||
fs.readdirSync(dir).forEach((file: string) => {
|
||||
if(isEvent(file)) return loadEvent(Discord, client, `${dir}/${file}`);
|
||||
else return ScanDir(Discord, client, `${dir}/${file}`);
|
||||
});
|
||||
}
|
||||
|
||||
function ScanDistubeDir(Discord: any, client: BotClient, dir: string) {
|
||||
fs.readdirSync(dir).forEach((file: string) => {
|
||||
if(isEvent(file)) return loadDistube(Discord, client, `${dir}/${file}`);
|
||||
else return ScanDistubeDir(Discord, client, `${dir}/${file}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadEvent(Discord: any, client: BotClient, file: string): Promise<void> {
|
||||
const event = await import(file);
|
||||
let event_name: any = file.split("/");
|
||||
event_name = event_name[event_name.length - 1].split(".")[0];
|
||||
client.on(event_name, event.default.bind(null, Discord, client));
|
||||
botEvents.push(event_name);
|
||||
}
|
||||
|
||||
async function loadDistube(Discord: any, client: BotClient, file: string): Promise<void> {
|
||||
const event = await import(file);
|
||||
let event_name: any = file.split("/");
|
||||
event_name = event_name[event_name.length - 1].split(".")[0];
|
||||
client.distube.on(event_name, event.default.bind(null, Discord, client));
|
||||
botEvents.push(`${event_name}`);
|
||||
}
|
||||
|
||||
export default async(Discord: any, client: BotClient) => {
|
||||
ScanDir(Discord, client, path.join(__dirname, `../events/bot`));
|
||||
ScanDistubeDir(Discord, client, path.join(__dirname, `../events/distube`));
|
||||
await wait(ms("5s"));
|
||||
botEvents.forEach((event: string) => {
|
||||
client.logger.log(`&6${event} &a- Event Loaded`);
|
||||
});
|
||||
}
|
||||
31
src/handlers/interaction_handler.ts
Normal file
31
src/handlers/interaction_handler.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import ms from "ms";
|
||||
import BotClient from "../libs/BotClient";
|
||||
|
||||
const wait = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay));
|
||||
|
||||
|
||||
function isAction(file: string) {
|
||||
return file.endsWith(".ts") || file.endsWith(".js");
|
||||
}
|
||||
|
||||
function ScanDir(Discord: any, client: BotClient, dir: any) {
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
if(isAction(file)) return loadAction(Discord, client, `${dir}/${file}`);
|
||||
else return ScanDir(Discord, client, `${dir}/${file}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAction(Discord: any, client: BotClient, file: any) {
|
||||
const action = new (await import(file)).default();
|
||||
client.action.set(action.getId(), action);
|
||||
}
|
||||
|
||||
export default async(Discord: any, client: BotClient) => {
|
||||
ScanDir(Discord, client, path.join(__dirname, `../interactions/`));
|
||||
await wait(ms("5s"));
|
||||
client.logger.log(`${client.action.size} - Actions Loaded`);
|
||||
}
|
||||
|
||||
43
src/index.ts
Normal file
43
src/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import * as Discord from "discord.js";
|
||||
|
||||
import BotClient from "./libs/BotClient";
|
||||
import Database from "./libs/Database";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const client = new BotClient({
|
||||
partials: [Discord.Partials.Message, Discord.Partials.Channel, Discord.Partials.Reaction],
|
||||
intents: [
|
||||
Discord.GatewayIntentBits.Guilds,
|
||||
Discord.GatewayIntentBits.GuildMembers,
|
||||
Discord.GatewayIntentBits.DirectMessages,
|
||||
Discord.GatewayIntentBits.GuildMessages,
|
||||
Discord.GatewayIntentBits.GuildVoiceStates,
|
||||
Discord.GatewayIntentBits.MessageContent,
|
||||
],
|
||||
allowedMentions: { parse: ['users', 'roles'], repliedUser: true }
|
||||
});
|
||||
|
||||
(async() => {
|
||||
// Initialize database connection
|
||||
const db = Database.getInstance();
|
||||
await db.connect();
|
||||
|
||||
fs.readdirSync(path.join(__dirname, "./handlers")).forEach( async(file: string) => {
|
||||
await (await import(path.join(__dirname, `./handlers/${file}`))).default(Discord, client);
|
||||
});
|
||||
})();
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN).then(() => client.logger.log("&aBot Online!")).catch(() => console.log(new Error("Invalid Discord Bot Token Provided!")));
|
||||
|
||||
// PROCESS
|
||||
process.on('uncaughtException', (err) => { client.logger.log("&4" + err); });
|
||||
process.on('unhandledRejection', (err) => { client.logger.log("&4" + err); });
|
||||
process.on('SIGINT', async () => {
|
||||
const db = Database.getInstance();
|
||||
await db.disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
31
src/interactions/DailyAnswer.ts
Normal file
31
src/interactions/DailyAnswer.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ModalActionRowComponentBuilder } from "discord.js";
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
|
||||
export default class DailyAnswer extends BotAction {
|
||||
constructor() {
|
||||
super("daily_answer_");
|
||||
}
|
||||
|
||||
public async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
const questionId = interaction.customId.split("_").slice(2).join("_");
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`daily_submit_${questionId}`)
|
||||
.setTitle("Submit Your Answer");
|
||||
|
||||
const answerInput = new TextInputBuilder()
|
||||
.setCustomId("answer")
|
||||
.setLabel("Your Answer")
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder("Enter your detailed answer here...")
|
||||
.setRequired(true)
|
||||
.setMaxLength(2000);
|
||||
|
||||
const row = new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(answerInput);
|
||||
|
||||
modal.addComponents(row);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
}
|
||||
31
src/interactions/DailyReport.ts
Normal file
31
src/interactions/DailyReport.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ModalActionRowComponentBuilder } from "discord.js";
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
|
||||
export default class DailyReport extends BotAction {
|
||||
constructor() {
|
||||
super("daily_report_");
|
||||
}
|
||||
|
||||
public async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
const questionId = interaction.customId.split("_").slice(2).join("_");
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`daily_report_submit_${questionId}`)
|
||||
.setTitle("Report Question Issue");
|
||||
|
||||
const issueInput = new TextInputBuilder()
|
||||
.setCustomId("issue")
|
||||
.setLabel("What's wrong with this question?")
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder("Describe the issue (e.g., unclear wording, incorrect answer, etc.)")
|
||||
.setRequired(true)
|
||||
.setMaxLength(1000);
|
||||
|
||||
const row = new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(issueInput);
|
||||
|
||||
modal.addComponents(row);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
}
|
||||
72
src/interactions/DailyReportSubmit.ts
Normal file
72
src/interactions/DailyReportSubmit.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { MessageFlags, EmbedBuilder } from "discord.js";
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default class DailyReportSubmit extends BotAction {
|
||||
constructor() {
|
||||
super("daily_report_submit_");
|
||||
}
|
||||
|
||||
public async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const questionId = interaction.customId.split("_").slice(3).join("_");
|
||||
const issue = interaction.fields.getTextInputValue("issue");
|
||||
|
||||
const reportData = {
|
||||
questionId,
|
||||
reportedBy: {
|
||||
id: interaction.user.id,
|
||||
username: interaction.user.username,
|
||||
},
|
||||
issue,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const reportsDir = path.join(process.cwd(), "data");
|
||||
const reportsFile = path.join(reportsDir, "question_reports.json");
|
||||
|
||||
await fs.mkdir(reportsDir, { recursive: true });
|
||||
|
||||
let reports = [];
|
||||
try {
|
||||
const data = await fs.readFile(reportsFile, "utf-8");
|
||||
reports = JSON.parse(data.toString());
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
}
|
||||
|
||||
reports.push(reportData);
|
||||
await fs.writeFile(reportsFile, JSON.stringify(reports, null, 2));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("✅ Report Submitted")
|
||||
.setDescription("Thank you for reporting this issue! Our team will review it shortly.")
|
||||
.setColor(0x22c55e)
|
||||
.addFields(
|
||||
{ name: "Question ID", value: questionId, inline: true },
|
||||
{ name: "Report Time", value: new Date().toLocaleString(), inline: true }
|
||||
)
|
||||
.setFooter({ text: "Your feedback helps improve the quality of questions" })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [embed],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error in DailyReportSubmit:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred while submitting your report. Please try again later." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred. Please try again later.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/interactions/DailySubmit.ts
Normal file
104
src/interactions/DailySubmit.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { MessageFlags, EmbedBuilder } from "discord.js";
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
import QuestionScheduler from "../libs/QuestionScheduler";
|
||||
import AnswerGrader from "../libs/AnswerGrader";
|
||||
import PointsManager from "../libs/PointsManager";
|
||||
|
||||
export default class DailySubmit extends BotAction {
|
||||
constructor() {
|
||||
super("daily_submit_");
|
||||
}
|
||||
|
||||
public async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const questionId = interaction.customId.split("_").slice(2).join("_");
|
||||
const userAnswer = interaction.fields.getTextInputValue("answer");
|
||||
|
||||
const scheduler = new QuestionScheduler();
|
||||
await scheduler.initialize();
|
||||
|
||||
const hasAnswered = await scheduler.hasUserAnswered(interaction.user.id, questionId);
|
||||
if (hasAnswered) {
|
||||
await interaction.editReply({ content: "You've already submitted an answer for this question!" });
|
||||
return;
|
||||
}
|
||||
|
||||
const history = await scheduler.getHistory();
|
||||
const question = history.find((q: any) => q.id === questionId);
|
||||
|
||||
if (!question) {
|
||||
await interaction.editReply({ content: "Question not found. Please try again." });
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.editReply({ content: "✅ Answer submitted! Sending to grader..." });
|
||||
|
||||
const grader = new AnswerGrader();
|
||||
const grade = await grader.gradeAnswer(question, userAnswer);
|
||||
|
||||
await scheduler.saveSubmission({
|
||||
userId: interaction.user.id,
|
||||
username: interaction.user.username,
|
||||
questionId: question.id,
|
||||
answer: userAnswer,
|
||||
gradeResult: grade,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const color = grade.is_correct ? 0x22c55e : 0xef4444;
|
||||
const verdict = grade.is_correct ? "✅ CORRECT" : "❌ INCORRECT";
|
||||
|
||||
const score = await scheduler.getUserScore(interaction.user.id, "daily");
|
||||
|
||||
let pointsInfo = "";
|
||||
if (grade.is_correct) {
|
||||
const pointsResult = await PointsManager.awardPoints(interaction.user.id, interaction.user.username, "daily");
|
||||
if (pointsResult) {
|
||||
pointsInfo = `\n**Points Earned:** +2 points (Total: ${pointsResult.userPoints})`;
|
||||
if (pointsResult.teamPoints !== undefined) {
|
||||
pointsInfo += `\n**Team Points:** ${pointsResult.teamPoints}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(verdict)
|
||||
.setColor(color)
|
||||
.addFields(
|
||||
{ name: "Subject", value: question.subject, inline: true },
|
||||
{ name: "Topic", value: question.topic, inline: true },
|
||||
{ name: "Difficulty", value: question.difficulty_rating, inline: true },
|
||||
{ name: "Your Daily Score", value: `${score.correct}/${score.total} correct today${pointsInfo}`, inline: false }
|
||||
)
|
||||
.setFooter({ text: "Come back tomorrow for a new question!" })
|
||||
.setTimestamp();
|
||||
|
||||
try {
|
||||
const dmChannel = await interaction.user.createDM();
|
||||
await dmChannel.send({ embeds: [embed] });
|
||||
console.log(`[DM] Successfully sent daily grade result to ${interaction.user.username} (${interaction.user.id})`);
|
||||
} catch (dmError) {
|
||||
console.log(`[DM] Failed to DM ${interaction.user.username} (${interaction.user.id}):`, dmError);
|
||||
await interaction.followUp({
|
||||
content: "⚠️ I couldn't DM you the results. Here they are:",
|
||||
embeds: [embed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error in DailySubmit:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred while grading. Please try again later." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred. Please try again later.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/interactions/ReportUser.ts
Normal file
43
src/interactions/ReportUser.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
import { MessageFlags } from "discord.js";
|
||||
|
||||
export default class ReportUserAction extends BotAction {
|
||||
constructor() {
|
||||
super("report");
|
||||
}
|
||||
|
||||
async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
const userID = interaction.customId?.split("-")[1];
|
||||
if (!userID) {
|
||||
await interaction.reply({ content: "❌ Invalid interaction data.", flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await client.users.fetch(userID).catch(() => null);
|
||||
if (!user) {
|
||||
await interaction.reply({ content: "❌ User not found.", flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = interaction.fields?.getTextInputValue('reasonInput') ?? "(no reason provided)";
|
||||
|
||||
const obj = {
|
||||
reported: `<@${user.id}> (` + "`" + `${user.id}` + "`" + `)`,
|
||||
reason,
|
||||
reporter: interaction.user?.username ?? interaction.user?.tag ?? String(interaction.user?.id ?? "unknown"),
|
||||
};
|
||||
|
||||
const embed = client.formatter.buildEmbed("./responses/user/report_user.yaml", obj);
|
||||
|
||||
const channel = client.channels.cache.get("1310725282357055521");
|
||||
if (!channel) {
|
||||
client.logger?.error?.("ReportUserAction: target channel not found or invalid.");
|
||||
await interaction.reply({ content: "❌ Could not send report. Please contact an administrator.", flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
|
||||
await (channel as any).send({ content: "<@&1310700035842768908>", embeds: [embed] });
|
||||
await interaction.reply({ content: "User has been reported. Thank You.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
187
src/interactions/SelectRoles.ts
Normal file
187
src/interactions/SelectRoles.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
import { MessageFlags } from "discord.js";
|
||||
|
||||
export default class SelectionRoles extends BotAction {
|
||||
constructor() {
|
||||
super("select-roles");
|
||||
}
|
||||
|
||||
async helperSelector(Discord: any, client: BotClient, interaction: any) {
|
||||
const guildMember = await interaction.guild.members.fetch(interaction.user.id);
|
||||
if (!interaction.values || !Array.isArray(interaction.values)) return interaction.editReply({ content: `❌ Invalid selection.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
for (const value of interaction.values) {
|
||||
const role = interaction.guild.roles.cache.get(value) || interaction.guild.roles.cache.find((r: any) => r.id === value);
|
||||
if (!role) {
|
||||
console.log(`Role not found: ${value}`);
|
||||
return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (guildMember.roles.cache.has(role.id)) continue;
|
||||
|
||||
await guildMember.roles.add(role.id);
|
||||
}
|
||||
|
||||
for (const role of client.config.getHelperRoles()) {
|
||||
if (!interaction.values.includes(role.id)) {
|
||||
const guildRole = interaction.guild.roles.cache.get(role.id) || interaction.guild.roles.cache.find((r: any) => r.id === role.id);
|
||||
if (!guildRole) return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
if (!guildMember.roles.cache.has(guildRole.id)) continue;
|
||||
|
||||
await guildMember.roles.remove(guildRole.id);
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.editReply({ content: `✅ Roles added successfully.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
async educationSelector(Discord: any, client: BotClient, interaction: any) {
|
||||
const guildMember = await interaction.guild.members.fetch(interaction.user.id);
|
||||
if (!interaction.values || !Array.isArray(interaction.values)) return interaction.editReply({ content: `❌ Invalid selection.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
for (const value of interaction.values) {
|
||||
const role = interaction.guild.roles.cache.get(value) || interaction.guild.roles.cache.find((r: any) => r.id === value);
|
||||
if (!role) {
|
||||
console.log(`Role not found: ${value}`);
|
||||
return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (guildMember.roles.cache.has(role.id)) continue;
|
||||
|
||||
await guildMember.roles.add(role.id);
|
||||
}
|
||||
|
||||
for (const role of client.config.getEducationRoles()) {
|
||||
if (!interaction.values.includes(role.id)) {
|
||||
const guildRole = interaction.guild.roles.cache.get(role.id) || interaction.guild.roles.cache.find((r: any) => r.id === role.id);
|
||||
if (!guildRole) return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
if (!guildMember.roles.cache.has(guildRole.id)) continue;
|
||||
|
||||
await guildMember.roles.remove(guildRole.id);
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.editReply({ content: `✅ Roles added successfully.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
async languageSelector(Discord: any, client: BotClient, interaction: any) {
|
||||
const guildMember = await interaction.guild.members.fetch(interaction.user.id);
|
||||
if (!interaction.values || !Array.isArray(interaction.values)) return interaction.editReply({ content: `❌ Invalid selection.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
for (const value of interaction.values) {
|
||||
const role = interaction.guild.roles.cache.get(value) || interaction.guild.roles.cache.find((r: any) => r.id === value);
|
||||
if (!role) {
|
||||
console.log(`Role not found: ${value}`);
|
||||
return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (guildMember.roles.cache.has(role.id)) continue;
|
||||
|
||||
await guildMember.roles.add(role.id);
|
||||
}
|
||||
|
||||
for (const role of client.config.getLanguageRoles()) {
|
||||
if (!interaction.values.includes(role.id)) {
|
||||
const guildRole = interaction.guild.roles.cache.get(role.id) || interaction.guild.roles.cache.find((r: any) => r.id === role.id);
|
||||
if (!guildRole) return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
if (!guildMember.roles.cache.has(guildRole.id)) continue;
|
||||
|
||||
await guildMember.roles.remove(guildRole.id);
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.editReply({ content: `✅ Roles added successfully.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
async locationSelector(Discord: any, client: BotClient, interaction: any) {
|
||||
const guildMember = await interaction.guild.members.fetch(interaction.user.id);
|
||||
if (!interaction.values || !Array.isArray(interaction.values)) return interaction.editReply({ content: `❌ Invalid selection.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
for (const value of interaction.values) {
|
||||
const role = interaction.guild.roles.cache.get(value) || interaction.guild.roles.cache.find((r: any) => r.id === value);
|
||||
if (!role) {
|
||||
console.log(`Role not found: ${value}`);
|
||||
return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (guildMember.roles.cache.has(role.id)) continue;
|
||||
|
||||
await guildMember.roles.add(role.id);
|
||||
}
|
||||
|
||||
for (const role of client.config.getLocationRoles()) {
|
||||
if (!interaction.values.includes(role.id)) {
|
||||
const guildRole = interaction.guild.roles.cache.get(role.id) || interaction.guild.roles.cache.find((r: any) => r.id === role.id);
|
||||
if (!guildRole) return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
if (!guildMember.roles.cache.has(guildRole.id)) continue;
|
||||
|
||||
await guildMember.roles.remove(guildRole.id);
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.editReply({ content: `✅ Roles added successfully.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
async pingSelector(Discord: any, client: BotClient, interaction: any) {
|
||||
const guildMember = await interaction.guild.members.fetch(interaction.user.id);
|
||||
if (!interaction.values || !Array.isArray(interaction.values)) return interaction.editReply({ content: `❌ Invalid selection.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
for (const value of interaction.values) {
|
||||
const role = interaction.guild.roles.cache.get(value) || interaction.guild.roles.cache.find((r: any) => r.id === value);
|
||||
if (!role) {
|
||||
console.log(`Role not found: ${value}`);
|
||||
return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (guildMember.roles.cache.has(role.id)) continue;
|
||||
|
||||
await guildMember.roles.add(role.id);
|
||||
}
|
||||
|
||||
for (const role of client.config.getPingRoles()) {
|
||||
if (!interaction.values.includes(role.id)) {
|
||||
const guildRole = interaction.guild.roles.cache.get(role.id) || interaction.guild.roles.cache.find((r: any) => r.id === role.id);
|
||||
if (!guildRole) return interaction.editReply({ content: `❌ Role not found.`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
if (!guildMember.roles.cache.has(guildRole.id)) continue;
|
||||
|
||||
await guildMember.roles.remove(guildRole.id);
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.editReply({ content: `✅ Roles added successfully.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
async execute(Discord: any, client: BotClient, interaction: any) {
|
||||
|
||||
await interaction.reply({ content: `⏳ One Moment Please. . .`, flags: MessageFlags.Ephemeral });
|
||||
|
||||
const type = interaction.customId?.split('-')[2];
|
||||
|
||||
switch(type) {
|
||||
case 'helper':
|
||||
this.helperSelector(Discord, client, interaction);
|
||||
break;
|
||||
case 'education':
|
||||
this.educationSelector(Discord, client, interaction);
|
||||
break;
|
||||
case 'language':
|
||||
this.languageSelector(Discord, client, interaction);
|
||||
break;
|
||||
case 'location':
|
||||
this.locationSelector(Discord, client, interaction);
|
||||
break;
|
||||
case 'ping':
|
||||
this.pingSelector(Discord, client, interaction);
|
||||
break;
|
||||
default:
|
||||
return interaction.editReply({ content: `❌ Invalid selection.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
40
src/interactions/TypstDelete.ts
Normal file
40
src/interactions/TypstDelete.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
import { MessageFlags } from "discord.js";
|
||||
|
||||
import BotAction from "../libs/BotAction";
|
||||
import type BotClient from "../libs/BotClient";
|
||||
|
||||
export default class TypstAction extends BotAction {
|
||||
constructor() {
|
||||
super("typst_delete", true);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: any): Promise<any> {
|
||||
try {
|
||||
if (interaction.isButton() && typeof interaction.customId === 'string' && interaction.customId === 'typst_delete') {
|
||||
const replyId = interaction.message?.id;
|
||||
if (!replyId) return;
|
||||
|
||||
const found = client.typst.findByReply(replyId);
|
||||
const ownerId = found?.ownerId;
|
||||
|
||||
const requesterId = interaction.user?.id;
|
||||
const member = interaction.member;
|
||||
const isAdmin = member?.permissions?.has && member.permissions.has && (member.permissions.has(Discord.PermissionFlagsBits?.Administrator) || member.permissions.has(Discord.PermissionFlagsBits?.ManageGuild));
|
||||
|
||||
if (requesterId === ownerId || isAdmin) {
|
||||
try { await interaction.message.delete(); } catch (e) {}
|
||||
if (found?.userMessage) client.typst.removeMessage(found.userMessage);
|
||||
try { await interaction.reply({ content: 'Deleted output.', flags: MessageFlags.Ephemeral }); } catch (e) {}
|
||||
return;
|
||||
} else {
|
||||
try { await interaction.reply({ content: 'You are not allowed to delete this output.', flags: Discord.MessageFlags.Ephemeral }); } catch (e) {}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Typst action error:', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
31
src/interactions/WeeklyAnswer.ts
Normal file
31
src/interactions/WeeklyAnswer.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ModalActionRowComponentBuilder } from "discord.js";
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
|
||||
export default class WeeklyAnswer extends BotAction {
|
||||
constructor() {
|
||||
super("weekly_answer_");
|
||||
}
|
||||
|
||||
public async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
const questionId = interaction.customId.split("_").slice(2).join("_");
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`weekly_submit_${questionId}`)
|
||||
.setTitle("Submit Your Answer");
|
||||
|
||||
const answerInput = new TextInputBuilder()
|
||||
.setCustomId("answer")
|
||||
.setLabel("Your Answer")
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder("Enter your detailed answer here...")
|
||||
.setRequired(true)
|
||||
.setMaxLength(2000);
|
||||
|
||||
const row = new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(answerInput);
|
||||
|
||||
modal.addComponents(row);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
}
|
||||
31
src/interactions/WeeklyReport.ts
Normal file
31
src/interactions/WeeklyReport.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ModalActionRowComponentBuilder } from "discord.js";
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
|
||||
export default class WeeklyReport extends BotAction {
|
||||
constructor() {
|
||||
super("weekly_report_");
|
||||
}
|
||||
|
||||
public async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
const questionId = interaction.customId.split("_").slice(2).join("_");
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`weekly_report_submit_${questionId}`)
|
||||
.setTitle("Report Question Issue");
|
||||
|
||||
const issueInput = new TextInputBuilder()
|
||||
.setCustomId("issue")
|
||||
.setLabel("What's wrong with this question?")
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder("Describe the issue (e.g., unclear wording, incorrect answer, etc.)")
|
||||
.setRequired(true)
|
||||
.setMaxLength(1000);
|
||||
|
||||
const row = new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(issueInput);
|
||||
|
||||
modal.addComponents(row);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
}
|
||||
72
src/interactions/WeeklyReportSubmit.ts
Normal file
72
src/interactions/WeeklyReportSubmit.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { MessageFlags, EmbedBuilder } from "discord.js";
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default class WeeklyReportSubmit extends BotAction {
|
||||
constructor() {
|
||||
super("weekly_report_submit_");
|
||||
}
|
||||
|
||||
public async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const questionId = interaction.customId.split("_").slice(3).join("_");
|
||||
const issue = interaction.fields.getTextInputValue("issue");
|
||||
|
||||
const reportData = {
|
||||
questionId,
|
||||
reportedBy: {
|
||||
id: interaction.user.id,
|
||||
username: interaction.user.username,
|
||||
},
|
||||
issue,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const reportsDir = path.join(process.cwd(), "data");
|
||||
const reportsFile = path.join(reportsDir, "question_reports.json");
|
||||
|
||||
await fs.mkdir(reportsDir, { recursive: true });
|
||||
|
||||
let reports = [];
|
||||
try {
|
||||
const data = await fs.readFile(reportsFile, "utf-8");
|
||||
reports = JSON.parse(data.toString());
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
}
|
||||
|
||||
reports.push(reportData);
|
||||
await fs.writeFile(reportsFile, JSON.stringify(reports, null, 2));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("✅ Report Submitted")
|
||||
.setDescription("Thank you for reporting this issue! Our team will review it shortly.")
|
||||
.setColor(0x22c55e)
|
||||
.addFields(
|
||||
{ name: "Question ID", value: questionId, inline: true },
|
||||
{ name: "Report Time", value: new Date().toLocaleString(), inline: true }
|
||||
)
|
||||
.setFooter({ text: "Your feedback helps improve the quality of questions" })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [embed],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error in WeeklyReportSubmit:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred while submitting your report. Please try again later." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred. Please try again later.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/interactions/WeeklySubmit.ts
Normal file
104
src/interactions/WeeklySubmit.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { MessageFlags, EmbedBuilder } from "discord.js";
|
||||
import BotAction from "../libs/BotAction";
|
||||
import BotClient from "../libs/BotClient";
|
||||
import QuestionScheduler from "../libs/QuestionScheduler";
|
||||
import AnswerGrader from "../libs/AnswerGrader";
|
||||
import PointsManager from "../libs/PointsManager";
|
||||
|
||||
export default class WeeklySubmit extends BotAction {
|
||||
constructor() {
|
||||
super("weekly_submit_");
|
||||
}
|
||||
|
||||
public async execute(Discord: any, client: BotClient, interaction: any): Promise<void> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const questionId = interaction.customId.split("_").slice(2).join("_");
|
||||
const userAnswer = interaction.fields.getTextInputValue("answer");
|
||||
|
||||
const scheduler = new QuestionScheduler();
|
||||
await scheduler.initialize();
|
||||
|
||||
const hasAnswered = await scheduler.hasUserAnswered(interaction.user.id, questionId);
|
||||
if (hasAnswered) {
|
||||
await interaction.editReply({ content: "You've already submitted an answer for this question!" });
|
||||
return;
|
||||
}
|
||||
|
||||
const history = await scheduler.getHistory();
|
||||
const question = history.find((q: any) => q.id === questionId);
|
||||
|
||||
if (!question) {
|
||||
await interaction.editReply({ content: "Question not found. Please try again." });
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.editReply({ content: "✅ Answer submitted! Sending to grader..." });
|
||||
|
||||
const grader = new AnswerGrader();
|
||||
const grade = await grader.gradeAnswer(question, userAnswer);
|
||||
|
||||
await scheduler.saveSubmission({
|
||||
userId: interaction.user.id,
|
||||
username: interaction.user.username,
|
||||
questionId: question.id,
|
||||
answer: userAnswer,
|
||||
gradeResult: grade,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const color = grade.is_correct ? 0x22c55e : 0xef4444;
|
||||
const verdict = grade.is_correct ? "✅ CORRECT" : "❌ INCORRECT";
|
||||
|
||||
const score = await scheduler.getUserScore(interaction.user.id, "weekly");
|
||||
|
||||
let pointsInfo = "";
|
||||
if (grade.is_correct) {
|
||||
const pointsResult = await PointsManager.awardPoints(interaction.user.id, interaction.user.username, "weekly");
|
||||
if (pointsResult) {
|
||||
pointsInfo = `\n**Points Earned:** +10 points (Total: ${pointsResult.userPoints})`;
|
||||
if (pointsResult.teamPoints !== undefined) {
|
||||
pointsInfo += `\n**Team Points:** ${pointsResult.teamPoints}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(verdict)
|
||||
.setColor(color)
|
||||
.addFields(
|
||||
{ name: "Subject", value: question.subject, inline: true },
|
||||
{ name: "Topic", value: question.topic, inline: true },
|
||||
{ name: "Difficulty", value: question.difficulty_rating, inline: true },
|
||||
{ name: "Your Weekly Score", value: `${score.correct}/${score.total} correct this week${pointsInfo}`, inline: false }
|
||||
)
|
||||
.setFooter({ text: "Come back next week for a new challenge!" })
|
||||
.setTimestamp();
|
||||
|
||||
try {
|
||||
const dmChannel = await interaction.user.createDM();
|
||||
await dmChannel.send({ embeds: [embed] });
|
||||
console.log(`[DM] Successfully sent weekly grade result to ${interaction.user.username} (${interaction.user.id})`);
|
||||
} catch (dmError) {
|
||||
console.log(`[DM] Failed to DM ${interaction.user.username} (${interaction.user.id}):`, dmError);
|
||||
await interaction.followUp({
|
||||
content: "⚠️ I couldn't DM you the results. Here they are:",
|
||||
embeds: [embed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error in WeeklySubmit:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred while grading. Please try again later." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred. Please try again later.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/libs/AnswerGrader.ts
Normal file
87
src/libs/AnswerGrader.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { QuestionData } from "./QuestionGenerator";
|
||||
|
||||
export interface GradeResult {
|
||||
comparison_analysis: string;
|
||||
is_correct: boolean;
|
||||
score: number;
|
||||
feedback: string;
|
||||
}
|
||||
|
||||
export default class AnswerGrader {
|
||||
private ollamaUrl: string;
|
||||
private ollamaModel: string;
|
||||
|
||||
constructor() {
|
||||
this.ollamaUrl = process.env.OLLAMA_URL || "http://192.168.1.214:11434";
|
||||
this.ollamaModel = process.env.OLLAMA_MODEL || "llama3.2:latest";
|
||||
}
|
||||
|
||||
async gradeAnswer(questionData: QuestionData, userAnswer: string): Promise<GradeResult> {
|
||||
const prompt = `
|
||||
You are a strict academic Teaching Assistant. Your job is to compare a Student's Answer to the Official Reference Key.
|
||||
|
||||
--- QUESTION METADATA ---
|
||||
Subject: ${questionData.subject}
|
||||
Difficulty: ${questionData.difficulty_rating}
|
||||
Topic: ${questionData.topic}
|
||||
|
||||
--- OFFICIAL REFERENCE ANSWER (TRUTH) ---
|
||||
${questionData.reference_answer}
|
||||
|
||||
--- STUDENT ANSWER ---
|
||||
"${userAnswer}"
|
||||
|
||||
--- INSTRUCTIONS ---
|
||||
1. Compare the Student Answer to the Reference Answer.
|
||||
2. Ignore minor formatting differences (e.g., "0.5" vs "1/2", or "joules" vs "J").
|
||||
3. If the question asks for a calculation, check if the numbers match (allow 1% tolerance).
|
||||
4. If the question is conceptual, check if the key concepts in the Reference are present in the Student Answer.
|
||||
5. Provide your response in JSON format with these fields:
|
||||
- comparison_analysis: string (brief analysis of differences)
|
||||
- is_correct: boolean
|
||||
- score: number (0-100)
|
||||
- feedback: string (constructive feedback for the student)
|
||||
|
||||
Output ONLY valid JSON, no markdown formatting.
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.ollamaUrl}/api/generate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.ollamaModel,
|
||||
prompt: prompt,
|
||||
stream: false,
|
||||
format: "json"
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const responseText = data.response;
|
||||
|
||||
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
||||
|
||||
if (!jsonMatch) {
|
||||
throw new Error("No JSON found in response");
|
||||
}
|
||||
|
||||
const grade = JSON.parse(jsonMatch[0]) as GradeResult;
|
||||
return grade;
|
||||
} catch (error) {
|
||||
console.error("Grading error:", error);
|
||||
return {
|
||||
comparison_analysis: "Error occurred during grading",
|
||||
is_correct: false,
|
||||
score: 0,
|
||||
feedback: "An error occurred while grading your answer. Please try again.",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
212
src/libs/BotAI.ts
Normal file
212
src/libs/BotAI.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
|
||||
import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai';
|
||||
import fs from 'fs';
|
||||
|
||||
const MODEL = process.env.AI_MODEL || 'gemini-2.5-flash';
|
||||
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
||||
|
||||
if (!GEMINI_API_KEY) {
|
||||
throw new Error('GEMINI_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
||||
|
||||
const BASE_PROMPT = fs.readFileSync('./config/prompt.txt', 'utf8');
|
||||
const SYSTEM_PROMPT = `${BASE_PROMPT}\n\nYou are currently helping {user_name}. The date is {Date} and it's {time} EST. Remember to always output valid JSON.`;
|
||||
|
||||
interface ChatEntry {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface Chat {
|
||||
entries: ChatEntry[];
|
||||
}
|
||||
|
||||
class User {
|
||||
public id: string;
|
||||
public name: string;
|
||||
public chat: Chat;
|
||||
|
||||
constructor(id: string, name: string) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.chat = { entries: [] };
|
||||
}
|
||||
|
||||
addChatEntry(role: 'user' | 'assistant' | 'system', content: string): void {
|
||||
this.chat.entries.push({ role, content });
|
||||
}
|
||||
|
||||
formatBasePrompt(): string {
|
||||
return SYSTEM_PROMPT.replace('{user_name}', this.name)
|
||||
.replace('{Date}', new Date().toLocaleDateString())
|
||||
.replace('{time}', new Date().toLocaleTimeString('en-US', { timeZone: 'America/New_York' }));
|
||||
}
|
||||
|
||||
getChatEntries(): ChatEntry[] {
|
||||
let typstMD: any[] = [];
|
||||
|
||||
fs.readdirSync('./config/md').forEach(file => {
|
||||
if (file.endsWith('.md')) {
|
||||
const content = fs.readFileSync(`./config/md/${file}`, 'utf8');
|
||||
typstMD.push({ role: 'system', content });
|
||||
}
|
||||
});
|
||||
|
||||
let entries: ChatEntry[] = [
|
||||
{ role: 'system', content: this.formatBasePrompt() },
|
||||
...typstMD,
|
||||
...this.chat.entries
|
||||
];
|
||||
|
||||
if (entries.length > 17) this.clearChat(); // Increased to account for additional system message
|
||||
return entries;
|
||||
}
|
||||
|
||||
loadChat(): void {
|
||||
const path = `./storage/chats/${this.id}.json`;
|
||||
|
||||
try {
|
||||
if (fs.existsSync(path)) {
|
||||
const data = fs.readFileSync(path, 'utf8');
|
||||
this.chat = JSON.parse(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading chat for user ${this.id}:`, error);
|
||||
this.chat = { entries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
saveChat(): void {
|
||||
const path = `./storage/chats/${this.id}.json`;
|
||||
|
||||
try {
|
||||
const dir = './storage/chats';
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(path, JSON.stringify(this.chat, null, 2));
|
||||
} catch (error) {
|
||||
console.error(`Error saving chat for user ${this.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
clearChat(): void {
|
||||
this.chat.entries = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class BotAI {
|
||||
private users: Map<string, User>;
|
||||
|
||||
constructor() {
|
||||
this.users = new Map();
|
||||
this.loadUsers();
|
||||
}
|
||||
|
||||
loadUsers(): void {
|
||||
const chatsDir = './storage/chats';
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(chatsDir)) {
|
||||
fs.mkdirSync(chatsDir, { recursive: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(chatsDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.json')) {
|
||||
const userId = file.replace('.json', '');
|
||||
|
||||
try {
|
||||
const user = new User(userId, 'Unknown User');
|
||||
user.loadChat();
|
||||
this.users.set(userId, user);
|
||||
} catch (error) {
|
||||
console.error(`Error loading user ${userId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Loaded ${this.users.size} users from storage`);
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getUser(id: string): User | undefined {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
createUser(id: string, name: string): User {
|
||||
const user = new User(id, name);
|
||||
this.users.set(id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async generateText(userId: string, username: string, prompt: string): Promise<{ content: string, typst: string }> {
|
||||
let user = this.getUser(userId) || this.createUser(userId, username);
|
||||
user.name = username;
|
||||
|
||||
try {
|
||||
const model = genAI.getGenerativeModel({
|
||||
model: MODEL,
|
||||
generationConfig: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: SchemaType.OBJECT,
|
||||
properties: {
|
||||
content: { type: SchemaType.STRING },
|
||||
typst: { type: SchemaType.STRING }
|
||||
},
|
||||
required: ["content", "typst"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Build system prompt with typst examples
|
||||
let systemPrompt = user.formatBasePrompt();
|
||||
// fs.readdirSync('./config/md').forEach(file => {
|
||||
// if (file.endsWith('.md')) {
|
||||
// const content = fs.readFileSync(`./config/md/${file}`, 'utf8');
|
||||
// systemPrompt += '\n\n' + content;
|
||||
// }
|
||||
// });
|
||||
|
||||
const result = await model.generateContent(systemPrompt + '\n\nUser request: ' + prompt);
|
||||
const response = await result.response;
|
||||
const rawText = response.text();
|
||||
console.log('Raw AI response:', rawText);
|
||||
|
||||
let botMessage = rawText;
|
||||
let parsed: { content?: string, typst?: string } = {};
|
||||
try {
|
||||
parsed = JSON.parse(botMessage);
|
||||
} catch {
|
||||
botMessage = botMessage
|
||||
.replace(/"([^"]*)"([^"]*)"([^"]*)"/g, '"$1\\"$2\\"$3"')
|
||||
.replace(/\n(?=\s*[^"}])/g, '\\n')
|
||||
.replace(/,(\s*[}\]])/g, '$1')
|
||||
.replace(/"([^"\\]*(\\.[^"\\]*)*)"?$/g, '"$1"');
|
||||
try {
|
||||
parsed = JSON.parse(botMessage);
|
||||
} catch {
|
||||
parsed = { content: botMessage, typst: '' };
|
||||
}
|
||||
}
|
||||
|
||||
user.addChatEntry('assistant', JSON.stringify(parsed));
|
||||
user.saveChat();
|
||||
return {
|
||||
content: typeof parsed.content === 'string' ? parsed.content : botMessage,
|
||||
typst: typeof parsed.typst === 'string' ? parsed.typst : ''
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error generating text:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/libs/BotAction.ts
Normal file
23
src/libs/BotAction.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { MessageFlags } from "discord.js";
|
||||
import BotClient from "./BotClient";
|
||||
|
||||
export default class BotAction {
|
||||
id: string;
|
||||
enabled: boolean = true;
|
||||
constructor(id: string, enabled: boolean = true) {
|
||||
this.id = id;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
async execute(Discord: any, client: BotClient, interaction: any): Promise<any> {
|
||||
interaction.reply({ content: "This Action is not Implemented Yet!", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
52
src/libs/BotClient.ts
Normal file
52
src/libs/BotClient.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Client, Collection } from "discord.js";
|
||||
import Distube from "distube";
|
||||
|
||||
import { SpotifyPlugin } from "@distube/spotify";
|
||||
import { SoundCloudPlugin } from "@distube/soundcloud";
|
||||
import { YtDlpPlugin } from "@distube/yt-dlp";
|
||||
|
||||
import BotCommand from "./BotCommand";
|
||||
import Logger from "./Logger";
|
||||
import Formatter from "./Formatter";
|
||||
import BotAction from "./BotAction";
|
||||
import Storage from "./Storage";
|
||||
import { BotAI } from "./BotAI";
|
||||
import Config from "./Config";
|
||||
import Typst from "./Typst";
|
||||
|
||||
|
||||
export default class BotClient extends Client {
|
||||
commands: Collection<string, BotCommand>;
|
||||
action: Collection<string, BotAction>
|
||||
logger: Logger;
|
||||
formatter: Formatter;
|
||||
distube: any;
|
||||
storage: Storage;
|
||||
// ai: BotAI;
|
||||
config: Config;
|
||||
typst: Typst;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.logger = new Logger();
|
||||
this.formatter = new Formatter();
|
||||
this.commands = new Collection();
|
||||
this.action = new Collection();
|
||||
this.storage = new Storage();
|
||||
|
||||
this.config = new Config();
|
||||
// this.ai = new BotAI();
|
||||
this.typst = new Typst();
|
||||
|
||||
this.distube = new Distube(this, {
|
||||
nsfw: true,
|
||||
plugins: [
|
||||
new SpotifyPlugin({
|
||||
api: { clientId: process.env.spotID, clientSecret: process.env.spotKey }
|
||||
}),
|
||||
new SoundCloudPlugin(),
|
||||
new YtDlpPlugin()
|
||||
]
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
38
src/libs/BotCommand.ts
Normal file
38
src/libs/BotCommand.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js";
|
||||
import BotClient from "./BotClient";
|
||||
|
||||
export default class BotCommand {
|
||||
name: string
|
||||
enabled: boolean
|
||||
use: string
|
||||
data: SlashCommandBuilder
|
||||
constructor(name: string, description: string, use: string, enabled: boolean = true) {
|
||||
this.name = name;
|
||||
this.use = use;
|
||||
this.enabled = enabled;
|
||||
this.data = new SlashCommandBuilder();
|
||||
this.data.setName(name);
|
||||
this.data.setDescription(description);
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
getUse(): string {
|
||||
return this.use;
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
await interaction.reply({ content: "This Command is not Implemented Yet!", flags: MessageFlags.Ephemeral })
|
||||
}
|
||||
|
||||
}
|
||||
81
src/libs/Config.ts
Normal file
81
src/libs/Config.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import fs from 'fs';
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface RolesConfig {
|
||||
helperroles: Role[];
|
||||
educationroles: Role[];
|
||||
languageroles: Role[];
|
||||
locationroles: Role[];
|
||||
pingroles: Role[];
|
||||
}
|
||||
|
||||
interface Birthday {
|
||||
day: number;
|
||||
month: number;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
interface BirthdaysConfig {
|
||||
[userId: string]: Birthday;
|
||||
}
|
||||
|
||||
interface ReplyTextConfig {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export default class Config {
|
||||
private roles: RolesConfig | null;
|
||||
private replyText: ReplyTextConfig | null;
|
||||
private bdays: BirthdaysConfig | null;
|
||||
|
||||
constructor() {
|
||||
this.roles = null;
|
||||
this.replyText = null;
|
||||
this.bdays = null;
|
||||
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
async loadConfig(): Promise<void> {
|
||||
let rawdata: Buffer;
|
||||
|
||||
// Load the roles.json file
|
||||
rawdata = fs.readFileSync('./config/roles.json');
|
||||
this.roles = JSON.parse(rawdata.toString());
|
||||
|
||||
rawdata = fs.readFileSync('./config/replytext.json');
|
||||
this.replyText = JSON.parse(rawdata.toString());
|
||||
|
||||
rawdata = fs.readFileSync('./storage/bdays.json');
|
||||
this.bdays = JSON.parse(rawdata.toString());
|
||||
}
|
||||
|
||||
getRoles(): RolesConfig | null { return this.roles; }
|
||||
getHelperRoles(): Role[] { return this.roles?.helperroles || []; }
|
||||
getEducationRoles(): Role[] { return this.roles?.educationroles || []; }
|
||||
getLanguageRoles(): Role[] { return this.roles?.languageroles || []; }
|
||||
getLocationRoles(): Role[] { return this.roles?.locationroles || []; }
|
||||
getPingRoles(): Role[] { return this.roles?.pingroles || []; }
|
||||
|
||||
getReplyText(): ReplyTextConfig | null { return this.replyText; }
|
||||
|
||||
getBdays(): BirthdaysConfig | null { return this.bdays; }
|
||||
getBday(userId: string): Birthday | null {
|
||||
return this.bdays?.[userId] || null;
|
||||
}
|
||||
setBday(userId: string, bday: Birthday): void {
|
||||
if (!this.bdays) this.bdays = {};
|
||||
this.bdays[userId] = bday;
|
||||
fs.writeFileSync('./storage/bdays.json', JSON.stringify(this.bdays, null, 2));
|
||||
}
|
||||
removeBday(userId: string): void {
|
||||
if (this.bdays?.[userId]) {
|
||||
delete this.bdays[userId];
|
||||
fs.writeFileSync('./storage/bdays.json', JSON.stringify(this.bdays, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/libs/Database.ts
Normal file
46
src/libs/Database.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
export default class Database {
|
||||
private static instance: Database;
|
||||
private connected: boolean = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): Database {
|
||||
if (!Database.instance) {
|
||||
Database.instance = new Database();
|
||||
}
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
if (this.connected) return;
|
||||
|
||||
const mongoUri = process.env.MONGODB_URI;
|
||||
if (!mongoUri) {
|
||||
console.warn("[Database] MONGODB_URI not set. Points system disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await mongoose.connect(mongoUri);
|
||||
this.connected = true;
|
||||
console.log("[Database] Connected to MongoDB");
|
||||
} catch (error) {
|
||||
console.error("[Database] Connection failed:", error);
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return this.connected && mongoose.connection.readyState === 1;
|
||||
}
|
||||
|
||||
public async disconnect(): Promise<void> {
|
||||
if (this.connected) {
|
||||
await mongoose.disconnect();
|
||||
this.connected = false;
|
||||
console.log("[Database] Disconnected from MongoDB");
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/libs/Formatter.ts
Normal file
133
src/libs/Formatter.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
export default class Formatter {
|
||||
obj: any;
|
||||
constructor() {
|
||||
this.obj = null;
|
||||
}
|
||||
readYaml(path: string) {
|
||||
try {
|
||||
const fileContents = fs.readFileSync(path, 'utf8');
|
||||
return yaml.load(fileContents);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
jsonPathToValue(jsonData: any, path: string) {
|
||||
if (!(jsonData instanceof Object) || typeof (path) === "undefined") {
|
||||
throw "InvalidArgumentException(jsonData:" + jsonData + ", path:" + path + ")";
|
||||
}
|
||||
path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
||||
path = path.replace(/^\./, ''); // strip a leading dot
|
||||
var pathArray = path.split('.');
|
||||
for (var i = 0, n = pathArray.length; i < n; ++i) {
|
||||
var key = pathArray[i];
|
||||
if (key && key in jsonData) {
|
||||
if (jsonData[key] !== null) {
|
||||
jsonData = jsonData[key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return jsonData;
|
||||
}
|
||||
trimBrackets(input: any) {
|
||||
let start = false, end = false;
|
||||
let final = input.split('');
|
||||
while(start == false) {
|
||||
if(final[0] != '{') {
|
||||
final.shift();
|
||||
} else {
|
||||
start = true;
|
||||
}
|
||||
}
|
||||
while(end == false) {
|
||||
if(final[final.length - 1] != '}') {
|
||||
final.pop();
|
||||
} else {
|
||||
end = true;
|
||||
}
|
||||
}
|
||||
return final.join('');
|
||||
}
|
||||
parseString(input: any) {
|
||||
let result = input;
|
||||
input.split(' ').forEach(async(word: any) => {
|
||||
if(!word.includes('{') && !word.includes('}')) return;
|
||||
let key = this.trimBrackets(word);
|
||||
if(!key.startsWith('{') && !key.endsWith('}')) return;
|
||||
key = key.replace('{', '').replace('}', '').split('_');
|
||||
if(key[0] == 'role') result = result.replace(`{${key[0]}_${key[1]}}`, "<@&" + key[1] + ">");
|
||||
if(key[0] == 'user') result = result.replace(`{${key[0]}_${key[1]}}`, "<@" + key[1] + ">");
|
||||
if(key[0] == 'channel') result = result.replace(`{${key[0]}_${key[1]}}`, "<#" + key[1] + ">");
|
||||
if(this.obj != null) {
|
||||
if(this.obj[key[0]] != null) result = result.replace(`{${key.join("_")}}`, this.jsonPathToValue(this.obj, key.join(".")));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
parseObject(input: any) {
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (typeof input[key] == "string") input[key] = this.parseString(value);
|
||||
if (typeof input[key] == "object") input[key] = this.parseObject(input[key]);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
buildEmbed(path: string, obj: any = {}): EmbedBuilder {
|
||||
|
||||
obj = this.format(path, obj);
|
||||
|
||||
let embed = new EmbedBuilder()
|
||||
if(obj.title) embed.setTitle(obj.title);
|
||||
if(obj.description) embed.setDescription(obj.description);
|
||||
if(obj.color) embed.setColor(obj.color);
|
||||
if(obj.url) embed.setURL(obj.url);
|
||||
if(obj.image) embed.setImage(obj.image);
|
||||
if(obj.thumbnail) embed.setThumbnail(obj.thumbnail);
|
||||
if(obj.timestamp) embed.setTimestamp();
|
||||
|
||||
if(obj.author) {
|
||||
let name = obj.author.name || null;
|
||||
let url = obj.author.url || null;
|
||||
let iconURL = obj.author.iconURL || null;
|
||||
embed.setAuthor({ name: name, url: url, iconURL: iconURL });
|
||||
}
|
||||
|
||||
if(obj.footer) {
|
||||
let text = obj.footer.text || null;
|
||||
let iconURL = obj.footer.iconURL || null;
|
||||
embed.setFooter({ text: text, iconURL: iconURL });
|
||||
}
|
||||
|
||||
if(obj.fields) {
|
||||
obj.fields.forEach((field: any) => {
|
||||
embed.addFields({ name: field.name, value: field.value, inline: field.inline || false });
|
||||
});
|
||||
}
|
||||
return embed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param path The path to the yaml file
|
||||
* @param obj Json object to replace the placeholders
|
||||
* @returns The formatted object
|
||||
*/
|
||||
format(path: string, obj: any = {}) {
|
||||
let result = null;
|
||||
try {
|
||||
this.obj = obj;
|
||||
result = this.parseObject(this.readYaml(path))
|
||||
} finally {
|
||||
this.obj = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
101
src/libs/Logger.ts
Normal file
101
src/libs/Logger.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
export default class Logger {
|
||||
|
||||
colorize(inputString: string) {
|
||||
const minecraftColorCodes: any = {
|
||||
'0': '\x1b[30m', // Black
|
||||
'1': '\x1b[34m', // Dark Blue
|
||||
'2': '\x1b[32m', // Dark Green
|
||||
'3': '\x1b[36m', // Dark Aqua
|
||||
'4': '\x1b[31m', // Dark Red
|
||||
'5': '\x1b[35m', // Purple
|
||||
'6': '\x1b[33m', // Gold
|
||||
'7': '\x1b[37m', // Gray
|
||||
'8': '\x1b[90m', // Dark Gray
|
||||
'9': '\x1b[94m', // Blue
|
||||
'a': '\x1b[92m', // Green
|
||||
'b': '\x1b[96m', // Aqua
|
||||
'c': '\x1b[91m', // Red
|
||||
'd': '\x1b[95m', // Light Purple
|
||||
'e': '\x1b[93m', // Yellow
|
||||
'f': '\x1b[97m', // White
|
||||
'k': '\x1b[5m', // Obfuscated
|
||||
'l': '\x1b[1m', // Bold
|
||||
'm': '\x1b[9m', // Strikethrough
|
||||
'n': '\x1b[4m', // Underline
|
||||
'o': '\x1b[3m', // Italic
|
||||
'r': '\x1b[0m' // Reset
|
||||
};
|
||||
|
||||
const parts = inputString.split('&');
|
||||
let outputString = parts[0];
|
||||
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
|
||||
const colorCode = parts[i]?.[0];
|
||||
const restOfString = parts[i]?.slice(1);
|
||||
|
||||
if (colorCode && minecraftColorCodes.hasOwnProperty(colorCode)) {
|
||||
outputString += minecraftColorCodes[colorCode] + restOfString;
|
||||
} else {
|
||||
outputString += '&' + parts[i];
|
||||
}
|
||||
}
|
||||
|
||||
return outputString;
|
||||
}
|
||||
|
||||
decolorize(inputString: string) {
|
||||
const minecraftColorCodes: any = {
|
||||
'0': '',
|
||||
'1': '',
|
||||
'2': '',
|
||||
'3': '',
|
||||
'4': '',
|
||||
'5': '',
|
||||
'6': '',
|
||||
'7': '',
|
||||
'8': '',
|
||||
'9': '',
|
||||
'a': '',
|
||||
'b': '',
|
||||
'c': '',
|
||||
'd': '',
|
||||
'e': '',
|
||||
'f': '',
|
||||
'k': '',
|
||||
'l': '',
|
||||
'm': '',
|
||||
'n': '',
|
||||
'o': '',
|
||||
'r': '',
|
||||
};
|
||||
|
||||
const parts = inputString.split('&');
|
||||
let outputString = parts[0];
|
||||
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (!part) continue;
|
||||
|
||||
const colorCode = part[0];
|
||||
const restOfString = part.slice(1);
|
||||
|
||||
if (colorCode && minecraftColorCodes.hasOwnProperty(colorCode)) {
|
||||
outputString += minecraftColorCodes[colorCode] + restOfString;
|
||||
} else {
|
||||
outputString += '&' + parts[i];
|
||||
}
|
||||
}
|
||||
|
||||
return outputString;
|
||||
}
|
||||
|
||||
log(input: string) {
|
||||
console.log(this.colorize(input + "&r"));
|
||||
}
|
||||
|
||||
error(input: string) {
|
||||
console.error(this.colorize("&4" + input + "&r"));
|
||||
}
|
||||
}
|
||||
150
src/libs/PointsManager.ts
Normal file
150
src/libs/PointsManager.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import User, { IUser } from "../models/User";
|
||||
import Team, { ITeam } from "../models/Team";
|
||||
import Database from "./Database";
|
||||
|
||||
export default class PointsManager {
|
||||
private static DAILY_POINTS = 2;
|
||||
private static WEEKLY_POINTS = 10;
|
||||
|
||||
public static async awardPoints(userId: string, username: string, type: "daily" | "weekly"): Promise<{ userPoints: number; teamPoints?: number } | null> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) return null;
|
||||
|
||||
const points = type === "daily" ? this.DAILY_POINTS : this.WEEKLY_POINTS;
|
||||
|
||||
try {
|
||||
let user = await User.findOne({ userId });
|
||||
|
||||
if (!user) {
|
||||
user = new User({
|
||||
userId,
|
||||
username,
|
||||
points: 0,
|
||||
dailyQuestionsCompleted: 0,
|
||||
weeklyQuestionsCompleted: 0
|
||||
});
|
||||
}
|
||||
|
||||
user.points += points;
|
||||
user.username = username;
|
||||
user.lastActive = new Date();
|
||||
|
||||
if (type === "daily") {
|
||||
user.dailyQuestionsCompleted += 1;
|
||||
} else {
|
||||
user.weeklyQuestionsCompleted += 1;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
let teamPoints: number | undefined;
|
||||
if (user.teamId) {
|
||||
const team = await Team.findById(user.teamId);
|
||||
if (team) {
|
||||
team.points += points;
|
||||
(team as any).calculateAdjustedPoints();
|
||||
await team.save();
|
||||
teamPoints = team.adjustedPoints;
|
||||
}
|
||||
}
|
||||
|
||||
return { userPoints: user.points, teamPoints };
|
||||
} catch (error) {
|
||||
console.error("[PointsManager] Error awarding points:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getUserStats(userId: string): Promise<IUser | null> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) return null;
|
||||
|
||||
try {
|
||||
return await User.findOne({ userId }).populate("teamId");
|
||||
} catch (error) {
|
||||
console.error("[PointsManager] Error fetching user stats:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getLeaderboard(limit: number = 10): Promise<IUser[]> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) return [];
|
||||
|
||||
try {
|
||||
return await User.find().sort({ points: -1 }).limit(limit).exec();
|
||||
} catch (error) {
|
||||
console.error("[PointsManager] Error fetching leaderboard:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static async getTeamLeaderboard(limit: number = 10): Promise<ITeam[]> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) return [];
|
||||
|
||||
try {
|
||||
const teams = await Team.find().exec();
|
||||
teams.forEach(team => (team as any).calculateAdjustedPoints());
|
||||
await Promise.all(teams.map(t => t.save()));
|
||||
|
||||
return teams.sort((a, b) => b.adjustedPoints - a.adjustedPoints).slice(0, limit);
|
||||
} catch (error) {
|
||||
console.error("[PointsManager] Error fetching team leaderboard:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static canChangeTeam(): boolean {
|
||||
const now = new Date();
|
||||
const dayOfMonth = now.getDate();
|
||||
return dayOfMonth <= 7;
|
||||
}
|
||||
|
||||
public static async joinTeam(userId: string, username: string, teamId: string, requireTimeCheck: boolean = false): Promise<{ success: boolean; message: string }> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) return { success: false, message: "Database not connected" };
|
||||
|
||||
if (requireTimeCheck && !this.canChangeTeam()) {
|
||||
return { success: false, message: "You can only change teams during the first week of the month (1st-7th)" };
|
||||
}
|
||||
|
||||
try {
|
||||
let user = await User.findOne({ userId });
|
||||
if (!user) {
|
||||
user = new User({ userId, username, points: 0 });
|
||||
}
|
||||
|
||||
const team = await Team.findById(teamId);
|
||||
if (!team) {
|
||||
return { success: false, message: "Team not found" };
|
||||
}
|
||||
|
||||
const oldTeamId = user.teamId;
|
||||
if (oldTeamId && oldTeamId.toString() === teamId) {
|
||||
return { success: false, message: "You're already in this team" };
|
||||
}
|
||||
|
||||
if (oldTeamId) {
|
||||
const oldTeam = await Team.findById(oldTeamId);
|
||||
if (oldTeam) {
|
||||
oldTeam.memberCount = Math.max(0, oldTeam.memberCount - 1);
|
||||
(oldTeam as any).calculateAdjustedPoints();
|
||||
await oldTeam.save();
|
||||
}
|
||||
}
|
||||
|
||||
user.teamId = team._id as any;
|
||||
await user.save();
|
||||
|
||||
team.memberCount += 1;
|
||||
(team as any).calculateAdjustedPoints();
|
||||
await team.save();
|
||||
|
||||
return { success: true, message: `Successfully joined team **${team.name}**!` };
|
||||
} catch (error) {
|
||||
console.error("[PointsManager] Error joining team:", error);
|
||||
return { success: false, message: "An error occurred while joining the team" };
|
||||
}
|
||||
}
|
||||
}
|
||||
455
src/libs/QuestionGenerator.ts
Normal file
455
src/libs/QuestionGenerator.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
||||
import Question, { IQuestion } from "../models/Question";
|
||||
import Database from "./Database";
|
||||
|
||||
const SUBJECTS = ["Mathematics", "Physics", "Chemistry", "Organic Chemistry", "Biology", "Computer Science", "Engineering"];
|
||||
|
||||
export interface QuestionData {
|
||||
id: string;
|
||||
subject: string;
|
||||
frequency: "daily" | "weekly";
|
||||
topic: string;
|
||||
difficulty_rating: string;
|
||||
typst_source: string;
|
||||
reference_answer: string;
|
||||
timestamp: string;
|
||||
image_path?: string;
|
||||
}
|
||||
|
||||
export default class QuestionGenerator {
|
||||
private genAI: GoogleGenerativeAI;
|
||||
private model: string;
|
||||
private outputDir: string;
|
||||
private chatModel: ChatGoogleGenerativeAI;
|
||||
|
||||
constructor() {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("GEMINI_API_KEY environment variable is missing.");
|
||||
}
|
||||
this.genAI = new GoogleGenerativeAI(apiKey);
|
||||
this.model = process.env.AI_MODEL || "gemini-2.5-flash";
|
||||
this.outputDir = path.join(process.cwd(), "data", "generated_problems");
|
||||
this.chatModel = new ChatGoogleGenerativeAI({
|
||||
apiKey: apiKey,
|
||||
model: this.model,
|
||||
temperature: 0.9,
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await fs.mkdir(this.outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
private getSubjectTypstGuide(subjects: string[]): string {
|
||||
const guides: Record<string, string> = {
|
||||
"Mathematics": `
|
||||
**MATHEMATICS TYPST GUIDE:**
|
||||
- Variables: \`#let x = $5$;\` (semicolon required)
|
||||
- Functions: \`$f(x)$\`, \`$sin(theta)$\`, \`$ln(x)$\`
|
||||
- Multiple variables: ALWAYS add spaces: \`$x y$\` NOT \`$xy$\` (xy = one variable)
|
||||
- CORRECT: \`$2x y$\`, \`$a b c$\`, \`$x^2 y z$\`
|
||||
- WRONG: \`$2xy$\` (Typst reads as one variable "xy")
|
||||
- Subscripts: \`$x_1$\`, \`$x_(i+1)$\` (use parentheses for complex subscripts)
|
||||
- Matrices: Use \`mat()\`: \`$mat(1,2;3,4)$\` for 2×2 matrix
|
||||
- Vectors: \`$vec(x,y,z)$\` or \`$arrow(v)$\`
|
||||
- Limits: \`$lim_(x -> oo)$\`, \`$sum_(i=1)^n$\`
|
||||
- Integrals: \`$integral_0^1 f(x) dif x$\` (use \`dif\` for dx)
|
||||
- Greek: \`$alpha$\`, \`$beta$\`, \`$theta$\`, \`$pi$\`
|
||||
- NEVER: \`$x_i+1$\` (use parentheses: \`$x_(i+1)$\`)
|
||||
- NEVER: Strings in subscripts like \`$sigma_("text")$\` (use \`$sigma_"text"$\`)
|
||||
- NEVER: Adjacent variables without spaces like \`$xy$\`, \`$abc$\``,
|
||||
|
||||
"Physics": `
|
||||
**PHYSICS TYPST GUIDE:**
|
||||
- All variables in math mode: \`$F = m a$\` (note space between m and a)
|
||||
- Multiple variables: MUST have spaces: \`$m v$\`, \`$F d$\` NOT \`$mv$\`, \`$Fd$\`
|
||||
- Units as text: \`$9.8 "m/s"^2$\`, \`$5 "kg"$\`, \`$100 "N"$\`
|
||||
- Vectors: \`$arrow(F)$\`, \`$arrow(v)$\`, \`$hat(x)$\` for unit vectors
|
||||
- Dot product: \`$arrow(a) dot arrow(b)$\`
|
||||
- Cross product: \`$arrow(a) times arrow(b)$\`
|
||||
- Subscripts: \`$v_0$\`, \`$F_"net"$\`, \`$E_"kinetic"$\`
|
||||
- Greek: \`$omega$\`, \`$theta$\`, \`$phi$\`, \`$lambda$\`
|
||||
- Constants: \`$g = 9.8 "m/s"^2$\`, \`$c = 3 times 10^8 "m/s"$\`
|
||||
- NEVER: \`$F_net$\` without quotes (use \`$F_"net"$\`)
|
||||
- NEVER: Complex subscripts without parentheses
|
||||
- NEVER: Adjacent variables without spaces like \`$mv$\`, \`$xy$\``,
|
||||
|
||||
"Chemistry": `
|
||||
**CHEMISTRY TYPST GUIDE:**
|
||||
- Chemical formulas: ALL multi-letter elements need quotes
|
||||
- \`$"H"_2"O"$\`, \`$"CO"_2$\`, \`$"NaCl"$\`, \`$"CH"_4$\`
|
||||
- \`$"Ca"("OH")_2$\`, \`$"Fe"_2"O"_3$\`
|
||||
- Single elements: \`$"H"$\`, \`$"C"$\`, \`$"N"$\`, \`$"O"$\`
|
||||
- Charges: \`$"H"^+$\`, \`$"OH"^-$\`, \`$"Ca"^(2+)$\`, \`$"SO"_4^(2-)$\`
|
||||
- States: \`$"H"_2"O"(l)$\`, \`$"CO"_2(g)$\`, \`$"NaCl"(s)$\`
|
||||
- Arrows: \`$arrow.r$\` or \`$-->$\` for reactions
|
||||
- Equilibrium: \`$arrow.l.r$\` or \`$<-->$\`
|
||||
- Concentration: \`$["H"^+] = 0.1 "M"$\`
|
||||
- NEVER: \`$H_2O$\` or \`$CO_2$\` (needs quotes)
|
||||
- NEVER: \`$NaCl$\` without quotes`,
|
||||
|
||||
"Organic Chemistry": `
|
||||
**ORGANIC CHEMISTRY TYPST GUIDE:**
|
||||
- All molecules need quotes: \`$"CH"_3"CH"_2"OH"$\`
|
||||
- Functional groups: \`$"COOH"$\`, \`$"NH"_2$\`, \`$"OH"$\`
|
||||
- Benzene ring: Describe in text, then use \`$"C"_6"H"_6$\`
|
||||
- Naming: Keep as regular text outside math mode
|
||||
- Stereochemistry: \`$(R)$\`, \`$(S)$\`, \`$E$\`, \`$Z$\`
|
||||
- Mechanisms: Use arrows: \`$arrow.r$\`, \`$arrow.curve$\`
|
||||
- Charges: \`$delta^+$\`, \`$delta^-$\`
|
||||
- IUPAC names: Regular text (not in $ $)
|
||||
- NEVER: \`$CH_3$\` (use \`$"CH"_3$\`)`,
|
||||
|
||||
"Biology": `
|
||||
**BIOLOGY TYPST GUIDE:**
|
||||
- Species names: _Italics_ outside math: \`_E. coli_\` or \`_Homo sapiens_\`
|
||||
- Genes/proteins: Regular text or math: \`$"ATP"$\`, \`$"DNA"$\`, \`$"NADH"$\`
|
||||
- Concentrations: \`$["Ca"^(2+)] = 1 "mM"$\`
|
||||
- pH: \`$"pH" = 7.4$\`
|
||||
- Equations: \`$"C"_6"H"_(12)"O"_6 + 6"O"_2 arrow.r 6"CO"_2 + 6"H"_2"O"$\`
|
||||
- Ratios: \`$3:1$\` or \`$9:3:3:1$\`
|
||||
- Units: \`$"mg/mL"$\`, \`$mu"m"$\` (mu for micro)
|
||||
- NEVER: Unquoted chemical formulas`,
|
||||
|
||||
"Computer Science": `
|
||||
**COMPUTER SCIENCE TYPST GUIDE:**
|
||||
- Code snippets: Use code blocks with backticks: \`\`\`python ... \`\`\`
|
||||
- Algorithms: Use lists with \`+\` or \`-\`
|
||||
- Big O: \`$O(n)$\`, \`$O(n log n)$\`, \`$Theta(n^2)$\`
|
||||
- Math notation: \`$sum_(i=1)^n i$\`, \`$log_2 n$\`
|
||||
- Boolean: \`$and$\`, \`$or$\`, \`$not$\`
|
||||
- Sets: \`$\\{1,2,3\\}$\`, \`$A union B$\`, \`$A sect B$\`
|
||||
- Logic: \`$forall$\`, \`$exists$\`, \`$in$\`, \`$subset.eq$\`
|
||||
- Variables in math: \`$x_i$\`, \`$a_n$\`
|
||||
- Keep actual code outside math mode`,
|
||||
|
||||
"Engineering": `
|
||||
**ENGINEERING TYPST GUIDE:**
|
||||
- Variables with units: \`$F = 100 "N"$\`, \`$sigma = 50 "MPa"$\`
|
||||
- Multiple variables: MUST have spaces: \`$F A$\`, \`$L h$\` NOT \`$FA$\`, \`$Lh$\`
|
||||
- Subscripts for properties: \`$sigma_"yield"$\`, \`$E_"young"$\`
|
||||
- Greek symbols: \`$sigma$\`, \`$tau$\`, \`$epsilon$\`, \`$rho$\`
|
||||
- Stress/strain: \`$sigma = F/A$\`, \`$epsilon = Delta L / L_0$\`
|
||||
- Vectors: \`$arrow(F)$\`, \`$arrow(M)$\`
|
||||
- Moments: \`$M_x$\`, \`$M_"max"$\`
|
||||
- Units: \`$"Pa"$\`, \`$"MPa"$\`, \`$"kN"$\`, \`$"mm"$\`
|
||||
- NEVER: \`$sigma_(allow, a)$\` (use \`$sigma_"allow,a"$\` with quotes)
|
||||
- NEVER: Complex subscripts without quotes
|
||||
- NEVER: Adjacent variables without spaces like \`$FA$\`, \`$xy$\``
|
||||
};
|
||||
|
||||
return subjects.map((s: string) => {
|
||||
const normalized = s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
||||
return guides[normalized] || guides["Mathematics"];
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
async getHistory(limit: number = 100): Promise<QuestionData[]> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
console.warn("[QuestionGenerator] Database not connected, returning empty history");
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const questions = await Question.find()
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
return questions.map(q => ({
|
||||
id: q.id,
|
||||
subject: q.subject,
|
||||
frequency: q.frequency as "daily" | "weekly",
|
||||
topic: q.topic,
|
||||
difficulty_rating: q.difficulty_rating,
|
||||
typst_source: q.typst_source,
|
||||
reference_answer: q.reference_answer,
|
||||
timestamp: q.timestamp.toISOString(),
|
||||
image_path: q.image_path
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("[QuestionGenerator] Error fetching history from DB:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async saveHistory(newQuestions: QuestionData[]) {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
console.warn("[QuestionGenerator] Database not connected, cannot save questions");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const questionDocs = newQuestions.map(q => ({
|
||||
id: q.id,
|
||||
subject: q.subject,
|
||||
frequency: q.frequency,
|
||||
topic: q.topic,
|
||||
difficulty_rating: q.difficulty_rating,
|
||||
typst_source: q.typst_source,
|
||||
reference_answer: q.reference_answer,
|
||||
timestamp: new Date(q.timestamp),
|
||||
image_path: q.image_path
|
||||
}));
|
||||
|
||||
await Question.insertMany(questionDocs, { ordered: false }).catch(err => {
|
||||
// Ignore duplicate key errors
|
||||
if (err.code !== 11000) throw err;
|
||||
});
|
||||
|
||||
console.log(`[QuestionGenerator] Saved ${newQuestions.length} questions to database`);
|
||||
} catch (error) {
|
||||
console.error("[QuestionGenerator] Error saving questions to DB:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getContextualHistory(frequency: "daily" | "weekly", subject?: string): Promise<string> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) return "No previous questions available.";
|
||||
|
||||
try {
|
||||
const query: any = { frequency };
|
||||
if (subject) query.subject = subject;
|
||||
|
||||
const recentQuestions = await Question.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(30)
|
||||
.select('subject topic difficulty_rating createdAt')
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (recentQuestions.length === 0) {
|
||||
return "No previous questions found for this category.";
|
||||
}
|
||||
|
||||
const context = recentQuestions.map((q, i) =>
|
||||
`${i + 1}. [${q.subject}] ${q.topic} (${q.difficulty_rating}) - Generated ${new Date(q.createdAt).toLocaleDateString()}`
|
||||
).join("\n");
|
||||
|
||||
return `Recent ${frequency} questions (last 30):\n${context}`;
|
||||
} catch (error) {
|
||||
console.error("[QuestionGenerator] Error fetching contextual history:", error);
|
||||
return "Error retrieving question history.";
|
||||
}
|
||||
}
|
||||
|
||||
async generateProblems(frequency: "daily" | "weekly", specificSubject?: string): Promise<QuestionData[]> {
|
||||
// Get contextual history from database
|
||||
const contextualHistory = await this.getContextualHistory(frequency, specificSubject);
|
||||
|
||||
const subjectsToGenerate = specificSubject ? [specificSubject] : SUBJECTS;
|
||||
|
||||
const subjectGuides = this.getSubjectTypstGuide(subjectsToGenerate);
|
||||
|
||||
const model = this.genAI.getGenerativeModel({
|
||||
model: this.model,
|
||||
generationConfig: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: SchemaType.ARRAY,
|
||||
items: {
|
||||
type: SchemaType.OBJECT,
|
||||
properties: {
|
||||
subject: { type: SchemaType.STRING },
|
||||
frequency: { type: SchemaType.STRING },
|
||||
topic: { type: SchemaType.STRING },
|
||||
difficulty_rating: { type: SchemaType.STRING },
|
||||
typst_source: { type: SchemaType.STRING },
|
||||
reference_answer: { type: SchemaType.STRING },
|
||||
},
|
||||
required: ["subject", "frequency", "topic", "difficulty_rating", "typst_source", "reference_answer"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const difficultyDesc = frequency === "daily"
|
||||
? "Easy undergraduate level (freshman/sophomore) requiring detailed calculations and showing work. Maximum 2 parts (e.g., Part a and Part b) that test foundational concepts."
|
||||
: "Advanced undergraduate level (junior/senior year) requiring deeper theoretical understanding and complex problem-solving. Maximum 2 parts that synthesize multiple upper-level concepts.";
|
||||
|
||||
const prompt = `
|
||||
Generate rigorous STEM practice problems in **Typst** code format.
|
||||
|
||||
SUBJECTS: ${subjectsToGenerate.join(", ")}
|
||||
FREQUENCY: ${frequency}
|
||||
|
||||
QUESTION HISTORY CONTEXT (avoid repeating these topics):
|
||||
${contextualHistory}
|
||||
|
||||
IMPORTANT: Review the question history above and generate NEW topics that haven't been covered recently. Avoid repeating the same concepts or problem types.
|
||||
|
||||
${subjectGuides}
|
||||
|
||||
CRITICAL TYPST SYNTAX RULES - FOLLOW EXACTLY:
|
||||
|
||||
**BEFORE YOU GENERATE:** Validate your output against these regex patterns to catch errors:
|
||||
1. Check for variables without spaces: \`/\$[0-9]*[a-z][a-z]+/g\` should NOT match (e.g., \`$xy$\`, \`$2ab$\` are INVALID)
|
||||
2. Check subscript parentheses: \`/_\("\\w+"\)/g\` should NOT match (e.g., \`$x_("text")$\` is INVALID, use \`$x_"text"$\`)
|
||||
3. Check chemical formulas: \`/\$[A-Z][a-z]?(?![_"])(?!\s)/g\` should require quotes (e.g., \`$NaCl$\` is INVALID, use \`$"NaCl"$\`)
|
||||
4. Check semicolons: \`/#let\\s+\\w+\\s*=\\s*\\\$[^;]*\\\$/g\` should NOT match (must end with \`;\`)
|
||||
5. Check set commands: \`/#set (text|page)/g\` should NOT match (font/page declarations forbidden)
|
||||
|
||||
**CORE SYNTAX RULES:**
|
||||
1. **Variables:** Must have semicolon after closing $ (e.g., \`#let x = $5$;\` NOT \`#let x = $5$ kg\`)
|
||||
2. **Math mode:** Use \`$...$\` for ALL equations. For chemical formulas use \`$"NiCl"_2$\` (quotes for multi-letter variables)
|
||||
3. **SPACES BETWEEN VARIABLES:** When writing multiple single-letter variables, ADD SPACES:
|
||||
- CORRECT: \`$x y$\`, \`$2x y$\`, \`$a b c$\`, \`$6x y$\`, \`$m v^2$\`, \`$a b + c d$\`
|
||||
- WRONG: \`$xy$\`, \`$2xy$\`, \`$abc$\`, \`$mv$\`, \`$ab+cd$\` (Typst reads these as ONE variable name)
|
||||
- EXCEPTION: Function names like \`$sin$\`, \`$cos$\`, \`$ln$\` are okay without spaces
|
||||
4. **Text in math:** Use quotes like \`$"text"$\` or \`$upright("text")$\`
|
||||
5. **Subscripts/Superscripts:** \`$x_2$\`, \`$x^2$\`, \`$10^(-27)$\` (use parentheses for negative exponents)
|
||||
6. **Complex subscripts:** ALWAYS use parentheses: \`$x_(i+1)$\` NOT \`$x_i+1$\`
|
||||
7. **Subscript text:** Use quotes directly WITHOUT parentheses: \`$sigma_"yield"$\` NOT \`$sigma_("yield")$\`
|
||||
8. **Multi-char subscripts:** Always wrap: \`$T_"max"$\`, \`$F_"net"$\`, \`$v_"initial"$\`
|
||||
9. **Lists:** Use \`- Item\` or \`+ Item\`. Never use single quotes.
|
||||
10. **Units:** Write as text: \`$5 "kg"$\`, \`$10 "m/s"^2$\`, \`$25 "MPa"$\`
|
||||
11. **Chemical formulas:** EVERY multi-letter element needs quotes:
|
||||
- \`$"H"_2"O"$\`, \`$"CO"_2$\`, \`$"NaCl"$\`, \`$"Ca"("OH")_2$\`, \`$"Fe"_2"O"_3$\`
|
||||
- Single letters can skip quotes: \`$"H"^+$\`, but safer to use quotes always
|
||||
12. **Greek letters:** \`$alpha$\`, \`$beta$\`, \`$Delta$\`, \`$theta$\`, \`$omega$\` (no backslash)
|
||||
13. **Operators:** \`$times$\`, \`$div$\`, \`$sum$\`, \`$integral$\`, \`$arrow.r$\`, \`$arrow.l.r$\`
|
||||
14. **Fractions:** \`$1/2$\` or \`$(a+b)/(c+d)$\`
|
||||
15. **Parentheses in math:** \`$(x+y)$\`, \`$[0, 1]$\`, \`$\\{x | x > 0\\}$\` (escape braces)
|
||||
16. **NO FONT/PAGE DECLARATIONS:** Never use \`#set text(font: ...)\` or \`#set page(...)\`
|
||||
|
||||
VALID EXAMPLES:
|
||||
\`\`\`typst
|
||||
#let mass = $5$;
|
||||
#let velocity = $10 "m/s"$;
|
||||
#let sigma_"yield" = $250 "MPa"$;
|
||||
#let pressure_"max" = $100 "kPa"$;
|
||||
|
||||
Calculate the energy: $E = 1/2 m v^2$ (note space between m and v)
|
||||
|
||||
Kinetic energy formula: $K E = 1/2 m v^2$
|
||||
|
||||
Function: $f(x, y) = x^3 - 6x y + 8y^3$ (space between x and y)
|
||||
|
||||
Polynomial: $p(x) = a x^3 + b x^2 + c x + d$ (spaces between all variables)
|
||||
|
||||
The compound $"H"_2"SO"_4$ reacts with $"NaOH"$ to form $"Na"_2"SO"_4$ and $"H"_2"O"$.
|
||||
|
||||
Ionic equation: $"Ca"^(2+) + "CO"_3^(2-) arrow.r "CaCO"_3(s)$
|
||||
|
||||
Subscript with expression: $x_(i+1) = x_i + 1$
|
||||
|
||||
Multiple subscripts: $T_"initial" = 298 "K"$ and $T_"final" = 373 "K"$
|
||||
|
||||
- Part (a): Find the derivative of $f(x)$
|
||||
- Part (b): Evaluate at $x = 2$
|
||||
- Part (c): Determine if $x y < 0$ (note space)
|
||||
\`\`\`
|
||||
|
||||
INVALID (DO NOT USE):
|
||||
- \`#set text(font: "Linux Libertine")\` (never declare fonts)
|
||||
- \`#set page(width: ...)\` (never set page properties)
|
||||
- \`#let x = $5$ kg\` (no text after closing $, must end with semicolon)
|
||||
- \`$xy$\` when you mean x times y (use \`$x y$\` with space)
|
||||
- \`$6xy$\` (use \`$6x y$\` with space between variables)
|
||||
- \`$mv^2$\` (use \`$m v^2$\` with space between m and v)
|
||||
- \`$ab + cd$\` (use \`$a b + c d$\` with spaces)
|
||||
- \`$sigma_("yield")$\` (quotes should be direct WITHOUT parentheses: \`$sigma_"yield"$\`)
|
||||
- \`$x_i+1$\` (use parentheses for expressions: \`$x_(i+1)$\`)
|
||||
- \`$NiCl_2$\` (multi-letter needs quotes: \`$"NiCl"_2$\`)
|
||||
- \`$H_2O$\` (needs quotes: \`$"H"_2"O"$\`)
|
||||
- \`$CO2$\` (needs quotes and underscore: \`$"CO"_2$\`)
|
||||
- \`$F_net$\` (text subscript needs quotes: \`$F_"net"$\`)
|
||||
- \`$KE$\` when meaning kinetic energy (use \`$K E$\` with space or write out)
|
||||
|
||||
**SELF-VALIDATION CHECKLIST (before submitting):**
|
||||
□ All multi-letter variables between single letters have spaces (e.g., \`$x y$\` not \`$xy$\`)
|
||||
□ All #let statements end with semicolon
|
||||
□ All text subscripts use quotes WITHOUT parentheses (e.g., \`$x_"text"$\`)
|
||||
□ All chemical formulas have quotes on multi-letter elements
|
||||
□ No #set text() or #set page() declarations
|
||||
□ All units are quoted (e.g., \`$5 "kg"$\`)
|
||||
□ Complex subscripts use parentheses (e.g., \`$x_(i+1)$\`)
|
||||
|
||||
REQUIREMENTS:
|
||||
1. **Typst Source:** Follow the syntax rules above EXACTLY. Validate against the regex patterns and checklist.
|
||||
2. **Reference Answer:** Provide the *exact* correct answer and the steps to derive it.
|
||||
3. **Difficulty:** ${difficultyDesc}
|
||||
4. **Question Structure:** MAXIMUM 2 parts per question (Part a and Part b). Do NOT create Part c, Part d, etc.
|
||||
5. **Quantity:** One '${frequency}' question per subject (${subjectsToGenerate.length} total).
|
||||
`;
|
||||
|
||||
const maxRetries = 3;
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
const delay = Math.pow(2, attempt) * 1000;
|
||||
console.log(`[Gemini API] Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms delay`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
console.log(`[Gemini API] Sending request to ${this.model} for ${frequency} question generation (${subjectsToGenerate.length} subjects: ${subjectsToGenerate.join(", ")})`);
|
||||
const result = await model.generateContent(prompt);
|
||||
console.log(`[Gemini API] Response received successfully`);
|
||||
const problems = JSON.parse(result.response.text()) as Omit<QuestionData, "id" | "timestamp">[];
|
||||
|
||||
const questionsWithMeta: QuestionData[] = problems.map(p => ({
|
||||
...p,
|
||||
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
await this.saveHistory(questionsWithMeta);
|
||||
return questionsWithMeta;
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
console.error(`[Gemini API] Error on attempt ${attempt + 1}:`, error?.message || error);
|
||||
|
||||
if (error?.status === 429) {
|
||||
console.log(`[Gemini API] Rate limit hit. Retrying...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.error("[Gemini API] All retry attempts failed:", lastError);
|
||||
return [];
|
||||
}
|
||||
|
||||
getOutputDir(): string {
|
||||
return this.outputDir;
|
||||
}
|
||||
|
||||
async getQuestionById(id: string): Promise<QuestionData | null> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
console.warn("[QuestionGenerator] Database not connected, cannot fetch question");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const question = await Question.findOne({ id }).lean().exec();
|
||||
if (!question) return null;
|
||||
|
||||
return {
|
||||
id: question.id,
|
||||
subject: question.subject,
|
||||
frequency: question.frequency as "daily" | "weekly",
|
||||
topic: question.topic,
|
||||
difficulty_rating: question.difficulty_rating,
|
||||
typst_source: question.typst_source,
|
||||
reference_answer: question.reference_answer,
|
||||
timestamp: question.timestamp.toISOString(),
|
||||
image_path: question.image_path
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[QuestionGenerator] Error fetching question by ID:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
238
src/libs/QuestionScheduler.ts
Normal file
238
src/libs/QuestionScheduler.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import QuestionGenerator, { QuestionData } from "./QuestionGenerator";
|
||||
import ScheduledQuestion from "../models/ScheduledQuestion";
|
||||
import Submission from "../models/Submission";
|
||||
import Database from "./Database";
|
||||
|
||||
interface UserSubmission {
|
||||
userId: string;
|
||||
username: string;
|
||||
questionId: string;
|
||||
answer: string;
|
||||
gradeResult: any;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export default class QuestionScheduler {
|
||||
private dataDir: string;
|
||||
private generator: QuestionGenerator;
|
||||
|
||||
constructor() {
|
||||
this.dataDir = path.join(process.cwd(), "data");
|
||||
this.generator = new QuestionGenerator();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.generator.initialize();
|
||||
await fs.mkdir(this.dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
async getHistory(): Promise<QuestionData[]> {
|
||||
return await this.generator.getHistory();
|
||||
}
|
||||
|
||||
private getPeriodKey(period: "daily" | "weekly"): string {
|
||||
const now = new Date();
|
||||
if (period === "daily") {
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||
} else {
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 1);
|
||||
const days = Math.floor((now.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000));
|
||||
const weekNumber = Math.ceil((days + startOfYear.getDay() + 1) / 7);
|
||||
return `${now.getFullYear()}-W${String(weekNumber).padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
async getQuestionForPeriod(subject: string, period: "daily" | "weekly"): Promise<QuestionData | null> {
|
||||
const periodKey = this.getPeriodKey(period);
|
||||
const cacheKey = `${subject}-${period}-${periodKey}`;
|
||||
|
||||
const db = Database.getInstance();
|
||||
if (db.isConnected()) {
|
||||
try {
|
||||
// Check MongoDB cache
|
||||
const cached = await ScheduledQuestion.findOne({ cacheKey }).lean().exec();
|
||||
if (cached && cached.periodKey === periodKey) {
|
||||
// Fetch the full question data
|
||||
const question = await this.generator.getQuestionById(cached.questionId);
|
||||
if (question) return question;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[QuestionScheduler] Error checking MongoDB cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new question
|
||||
const questions = await this.generator.generateProblems(period, subject);
|
||||
const subjectQuestion = questions.find(q =>
|
||||
q.subject.toLowerCase() === subject.toLowerCase() &&
|
||||
q.frequency === period
|
||||
);
|
||||
|
||||
if (!subjectQuestion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cache in MongoDB
|
||||
if (db.isConnected()) {
|
||||
try {
|
||||
await ScheduledQuestion.findOneAndUpdate(
|
||||
{ cacheKey },
|
||||
{
|
||||
cacheKey,
|
||||
questionId: subjectQuestion.id,
|
||||
subject,
|
||||
period,
|
||||
periodKey
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[QuestionScheduler] Error caching to MongoDB:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return subjectQuestion;
|
||||
}
|
||||
|
||||
async forceRegenerateQuestion(subject: string, period: "daily" | "weekly"): Promise<QuestionData | null> {
|
||||
const periodKey = this.getPeriodKey(period);
|
||||
const cacheKey = `${subject}-${period}-${periodKey}`;
|
||||
|
||||
const db = Database.getInstance();
|
||||
|
||||
// Clear the cached question from MongoDB
|
||||
if (db.isConnected()) {
|
||||
try {
|
||||
await ScheduledQuestion.deleteOne({ cacheKey });
|
||||
} catch (error) {
|
||||
console.error("[QuestionScheduler] Error clearing MongoDB cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new question
|
||||
const questions = await this.generator.generateProblems(period, subject);
|
||||
const subjectQuestion = questions.find(q =>
|
||||
q.subject.toLowerCase() === subject.toLowerCase() &&
|
||||
q.frequency === period
|
||||
);
|
||||
|
||||
if (!subjectQuestion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cache the new question in MongoDB
|
||||
if (db.isConnected()) {
|
||||
try {
|
||||
await ScheduledQuestion.create({
|
||||
cacheKey,
|
||||
questionId: subjectQuestion.id,
|
||||
subject,
|
||||
period,
|
||||
periodKey
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[QuestionScheduler] Error caching new question:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return subjectQuestion;
|
||||
}
|
||||
|
||||
async getUserSubmissions(): Promise<UserSubmission[]> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
console.warn("[QuestionScheduler] Database not connected");
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const submissions = await Submission.find().lean().exec();
|
||||
return submissions.map(s => ({
|
||||
userId: s.userId,
|
||||
username: s.username,
|
||||
questionId: s.questionId,
|
||||
answer: s.answer,
|
||||
gradeResult: s.gradeResult,
|
||||
timestamp: s.timestamp.toISOString()
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("[QuestionScheduler] Error fetching submissions:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async saveSubmission(submission: UserSubmission) {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
console.warn("[QuestionScheduler] Database not connected, cannot save submission");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Submission.create({
|
||||
userId: submission.userId,
|
||||
username: submission.username,
|
||||
questionId: submission.questionId,
|
||||
answer: submission.answer,
|
||||
gradeResult: submission.gradeResult,
|
||||
timestamp: new Date(submission.timestamp)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[QuestionScheduler] Error saving submission:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async hasUserAnswered(userId: string, questionId: string): Promise<boolean> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
console.warn("[QuestionScheduler] Database not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const submission = await Submission.findOne({ userId, questionId }).exec();
|
||||
return !!submission;
|
||||
} catch (error) {
|
||||
console.error("[QuestionScheduler] Error checking submission:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getUserScore(userId: string, period: "daily" | "weekly", periodKey?: string): Promise<{ correct: number; total: number }> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
console.warn("[QuestionScheduler] Database not connected");
|
||||
return { correct: 0, total: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
const targetPeriodKey = periodKey || this.getPeriodKey(period);
|
||||
|
||||
// Get relevant question IDs for this period
|
||||
const scheduledQuestions = await ScheduledQuestion.find({
|
||||
period,
|
||||
periodKey: targetPeriodKey
|
||||
}).select('questionId').lean().exec();
|
||||
|
||||
const relevantQuestionIds = scheduledQuestions.map(sq => sq.questionId);
|
||||
|
||||
if (relevantQuestionIds.length === 0) {
|
||||
return { correct: 0, total: 0 };
|
||||
}
|
||||
|
||||
// Get user submissions for these questions
|
||||
const userSubmissions = await Submission.find({
|
||||
userId,
|
||||
questionId: { $in: relevantQuestionIds }
|
||||
}).lean().exec();
|
||||
|
||||
const correct = userSubmissions.filter(s => s.gradeResult?.is_correct).length;
|
||||
return { correct, total: userSubmissions.length };
|
||||
} catch (error) {
|
||||
console.error("[QuestionScheduler] Error calculating user score:", error);
|
||||
return { correct: 0, total: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/libs/Storage.ts
Normal file
94
src/libs/Storage.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
|
||||
import { Client, Collection, GuildMember, Message } from "discord.js";
|
||||
import BotClient from "./BotClient";
|
||||
|
||||
type MemberCacheEntry = {
|
||||
members: Collection<string, GuildMember>;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
export default class Storage {
|
||||
db: any;
|
||||
private memberCache: Map<string, MemberCacheEntry>;
|
||||
private defaultTTL: number;
|
||||
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.memberCache = new Map();
|
||||
|
||||
this.defaultTTL = 5 * 60 * 1000; // 5 minutes
|
||||
}
|
||||
|
||||
async fetchGuildMembers(client: BotClient | Client, guildId: string, forceRefresh: boolean = false, ttl?: number): Promise<Collection<string, GuildMember>> {
|
||||
const now = Date.now();
|
||||
const useTTL = typeof ttl === "number" ? ttl : this.defaultTTL;
|
||||
|
||||
if (!forceRefresh) {
|
||||
const cached = this.memberCache.get(guildId);
|
||||
if (cached && cached.expiresAt > now) {
|
||||
return cached.members;
|
||||
}
|
||||
}
|
||||
|
||||
const guild = await client.guilds.fetch(guildId).catch(() => null);
|
||||
if (!guild) return new Collection<string, GuildMember>();
|
||||
|
||||
const members = await guild.members.fetch().catch(() => new Collection<string, GuildMember>());
|
||||
|
||||
this.memberCache.set(guildId, { members, expiresAt: now + useTTL });
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
invalidateGuildMembers(guildId: string) {
|
||||
this.memberCache.delete(guildId);
|
||||
}
|
||||
|
||||
|
||||
async getMessages(channel: any, options: { reverseArray?: boolean; userOnly?: boolean; botOnly?: boolean; pinnedOnly?: boolean; limitPages?: number; since?: Date } = {}): Promise<Message[]> {
|
||||
const { reverseArray, userOnly, botOnly, pinnedOnly, limitPages, since } = options;
|
||||
const messages: Message[] = [];
|
||||
let lastID: string | undefined = undefined;
|
||||
const maxPages = typeof limitPages === 'number' ? Math.max(1, limitPages) : 20;
|
||||
|
||||
for (let page = 0; page < maxPages; page++) {
|
||||
const fetched: Collection<string, Message> = await channel.messages.fetch({ limit: 100, ...(lastID ? { before: lastID } : {}) }).catch(() => new Collection<string, Message>());
|
||||
if (!fetched || fetched.size === 0) {
|
||||
if (reverseArray) messages.reverse();
|
||||
let out = messages as Message[];
|
||||
if (userOnly) out = out.filter(m => !m.author.bot);
|
||||
if (botOnly) out = out.filter(m => m.author.bot);
|
||||
if (pinnedOnly) out = out.filter(m => m.pinned);
|
||||
return out;
|
||||
}
|
||||
|
||||
let stopEarly = false;
|
||||
for (const msg of fetched.values()) {
|
||||
if (since && msg.createdAt < since) {
|
||||
stopEarly = true;
|
||||
break;
|
||||
}
|
||||
messages.push(msg);
|
||||
}
|
||||
|
||||
if (stopEarly) {
|
||||
if (reverseArray) messages.reverse();
|
||||
let out = messages as Message[];
|
||||
if (userOnly) out = out.filter(m => !m.author.bot);
|
||||
if (botOnly) out = out.filter(m => m.author.bot);
|
||||
if (pinnedOnly) out = out.filter(m => m.pinned);
|
||||
return out;
|
||||
}
|
||||
|
||||
lastID = fetched.last()?.id;
|
||||
if (!lastID) break;
|
||||
}
|
||||
|
||||
if (reverseArray) messages.reverse();
|
||||
let out = messages as Message[];
|
||||
if (userOnly) out = out.filter(m => !m.author.bot);
|
||||
if (botOnly) out = out.filter(m => m.author.bot);
|
||||
if (pinnedOnly) out = out.filter(m => m.pinned);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
115
src/libs/Typst.ts
Normal file
115
src/libs/Typst.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export default class Typst {
|
||||
messages: Map<string, { replyId: string; ownerId?: string }>;
|
||||
constructor() {
|
||||
this.messages = new Map();
|
||||
}
|
||||
|
||||
addMessage(userMessage: string, responseMessage: string, ownerId?: string) { this.messages.set(userMessage, { replyId: responseMessage, ownerId }); }
|
||||
removeMessage(userMessage: string) { this.messages.delete(userMessage); }
|
||||
hasMessage(userMessage: string) { return this.messages.has(userMessage); }
|
||||
getResponse(userMessage: string) { return this.messages.get(userMessage)?.replyId; }
|
||||
getOwner(userMessage: string) { return this.messages.get(userMessage)?.ownerId; }
|
||||
findByReply(replyId: string): { userMessage?: string; ownerId?: string } | null {
|
||||
for (const [userMessage, data] of this.messages.entries()) {
|
||||
if (data.replyId === replyId) return { userMessage, ownerId: data.ownerId };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async compile(code: string, options?: { cleanup?: boolean }): Promise<{ buffers?: Buffer[]; error?: string; files?: string[]; typPath?: string }> {
|
||||
const storageDir = path.resolve('./storage/typst');
|
||||
await fs.promises.mkdir(storageDir, { recursive: true }).catch(() => {});
|
||||
|
||||
const uid = `${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||||
const baseName = `typ_${uid}`;
|
||||
const typPath = path.join(storageDir, `${baseName}.typ`);
|
||||
const pngTemplate = path.join(storageDir, `${baseName}{p}.png`);
|
||||
const pngSingle = path.join(storageDir, `${baseName}.png`);
|
||||
const cleanup = options?.cleanup !== undefined ? options!.cleanup : true;
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile(typPath, code, 'utf8');
|
||||
} catch (writeErr: any) {
|
||||
return { error: `Failed to write typ file: ${String(writeErr)}` };
|
||||
}
|
||||
|
||||
let compiledToTemplate = false;
|
||||
try {
|
||||
await execFileAsync('typst', ['compile', '--format', 'png', '--ppi', '300', typPath, pngTemplate], { timeout: 30000 });
|
||||
compiledToTemplate = true;
|
||||
} catch (multiErr: any) {
|
||||
try {
|
||||
await execFileAsync('typst', ['compile', '--format', 'png', '--ppi', '300', typPath, pngSingle], { timeout: 30000 });
|
||||
compiledToTemplate = false;
|
||||
} catch (singleErr: any) {
|
||||
if (cleanup) await fs.promises.unlink(typPath).catch(() => {});
|
||||
const msg = (multiErr && (multiErr.stderr || multiErr.message)) || (singleErr && (singleErr.stderr || singleErr.message)) || 'Unknown typst error';
|
||||
return { error: `Typst compilation failed: ${String(msg)}` };
|
||||
}
|
||||
}
|
||||
|
||||
let pngFiles: string[] = [];
|
||||
try {
|
||||
const all = await fs.promises.readdir(storageDir);
|
||||
if (compiledToTemplate) pngFiles = all.filter(f => f.startsWith(baseName) && f.endsWith('.png')).sort();
|
||||
else if (all.includes(`${baseName}.png`)) pngFiles = [`${baseName}.png`];
|
||||
} catch (readErr: any) {
|
||||
if (cleanup) await fs.promises.unlink(typPath).catch(() => {});
|
||||
return { error: `Failed to read output PNG files: ${String(readErr)}` };
|
||||
}
|
||||
|
||||
if (pngFiles.length === 0) {
|
||||
await fs.promises.unlink(typPath).catch(() => {});
|
||||
return { error: 'Compilation finished but no PNG files were produced' };
|
||||
}
|
||||
|
||||
const fullPaths = pngFiles.map(f => path.join(storageDir, f));
|
||||
const buffers: Buffer[] = await Promise.all(fullPaths.map(async p => {
|
||||
try { return await fs.promises.readFile(p); } catch (e) { return null as any; }
|
||||
}));
|
||||
|
||||
const validBuffers = buffers.filter(Boolean) as Buffer[];
|
||||
|
||||
if (cleanup) {
|
||||
await fs.promises.unlink(typPath).catch(() => {});
|
||||
await Promise.all(fullPaths.map(f => fs.promises.unlink(f).catch(() => {})));
|
||||
}
|
||||
|
||||
if (validBuffers.length === 0) return { error: 'No PNG buffers could be read from generated output' };
|
||||
|
||||
const result: { buffers?: Buffer[]; files?: string[]; typPath?: string } = { buffers: validBuffers };
|
||||
if (!cleanup) {
|
||||
result.files = fullPaths;
|
||||
result.typPath = typPath;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async renderToImage(typstCode: string): Promise<string | null> {
|
||||
const styledCode = `
|
||||
#set page(width: 210mm, height: auto, margin: (x: 20pt, y: 25pt), fill: white)
|
||||
#set text(size: 16pt, font: "New Computer Modern")
|
||||
#set par(justify: true, leading: 0.65em)
|
||||
#set block(spacing: 1.2em)
|
||||
|
||||
${typstCode}
|
||||
`;
|
||||
|
||||
const result = await this.compile(styledCode, { cleanup: false });
|
||||
|
||||
if (result.error || !result.files || result.files.length === 0) {
|
||||
console.error("Typst render error:", result.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.files[0];
|
||||
}
|
||||
|
||||
}
|
||||
33
src/models/Question.ts
Normal file
33
src/models/Question.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface IQuestion extends Document {
|
||||
id: string;
|
||||
subject: string;
|
||||
frequency: "daily" | "weekly";
|
||||
topic: string;
|
||||
difficulty_rating: string;
|
||||
typst_source: string;
|
||||
reference_answer: string;
|
||||
timestamp: Date;
|
||||
image_path?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const QuestionSchema: Schema = new Schema({
|
||||
id: { type: String, required: true, unique: true, index: true },
|
||||
subject: { type: String, required: true, index: true },
|
||||
frequency: { type: String, enum: ["daily", "weekly"], required: true, index: true },
|
||||
topic: { type: String, required: true },
|
||||
difficulty_rating: { type: String, required: true },
|
||||
typst_source: { type: String, required: true },
|
||||
reference_answer: { type: String, required: true },
|
||||
timestamp: { type: Date, required: true },
|
||||
image_path: { type: String },
|
||||
createdAt: { type: Date, default: Date.now, index: true }
|
||||
});
|
||||
|
||||
// Index for retrieving recent questions by subject and frequency
|
||||
QuestionSchema.index({ subject: 1, frequency: 1, createdAt: -1 });
|
||||
QuestionSchema.index({ createdAt: -1 });
|
||||
|
||||
export default mongoose.model<IQuestion>("Question", QuestionSchema);
|
||||
24
src/models/ScheduledQuestion.ts
Normal file
24
src/models/ScheduledQuestion.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface IScheduledQuestion extends Document {
|
||||
cacheKey: string;
|
||||
questionId: string;
|
||||
subject: string;
|
||||
period: "daily" | "weekly";
|
||||
periodKey: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const ScheduledQuestionSchema: Schema = new Schema({
|
||||
cacheKey: { type: String, required: true, unique: true, index: true },
|
||||
questionId: { type: String, required: true, ref: "Question" },
|
||||
subject: { type: String, required: true, index: true },
|
||||
period: { type: String, enum: ["daily", "weekly"], required: true, index: true },
|
||||
periodKey: { type: String, required: true, index: true },
|
||||
createdAt: { type: Date, default: Date.now }
|
||||
});
|
||||
|
||||
// Compound index for efficient lookups
|
||||
ScheduledQuestionSchema.index({ subject: 1, period: 1, periodKey: 1 });
|
||||
|
||||
export default mongoose.model<IScheduledQuestion>("ScheduledQuestion", ScheduledQuestionSchema);
|
||||
35
src/models/Submission.ts
Normal file
35
src/models/Submission.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface ISubmission extends Document {
|
||||
userId: string;
|
||||
username: string;
|
||||
questionId: string;
|
||||
answer: string;
|
||||
gradeResult: {
|
||||
comparison_analysis?: string;
|
||||
is_correct: boolean;
|
||||
score?: number;
|
||||
feedback?: string;
|
||||
};
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
const SubmissionSchema: Schema = new Schema({
|
||||
userId: { type: String, required: true, index: true },
|
||||
username: { type: String, required: true },
|
||||
questionId: { type: String, required: true, ref: "Question", index: true },
|
||||
answer: { type: String, required: true },
|
||||
gradeResult: {
|
||||
comparison_analysis: { type: String },
|
||||
is_correct: { type: Boolean, required: true },
|
||||
score: { type: Number },
|
||||
feedback: { type: String }
|
||||
},
|
||||
timestamp: { type: Date, default: Date.now, index: true }
|
||||
});
|
||||
|
||||
// Compound indexes for efficient queries
|
||||
SubmissionSchema.index({ userId: 1, questionId: 1 });
|
||||
SubmissionSchema.index({ userId: 1, timestamp: -1 });
|
||||
|
||||
export default mongoose.model<ISubmission>("Submission", SubmissionSchema);
|
||||
43
src/models/Team.ts
Normal file
43
src/models/Team.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface ITeam extends Document {
|
||||
name: string;
|
||||
description?: string;
|
||||
leaderId: string;
|
||||
points: number;
|
||||
memberCount: number;
|
||||
createdAt: Date;
|
||||
adjustedPoints: number;
|
||||
}
|
||||
|
||||
const TeamSchema = new Schema<ITeam>({
|
||||
name: { type: String, required: true, unique: true, index: true },
|
||||
description: { type: String, default: "" },
|
||||
leaderId: { type: String, required: true },
|
||||
points: { type: Number, default: 0 },
|
||||
memberCount: { type: Number, default: 0 },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
adjustedPoints: { type: Number, default: 0 }
|
||||
});
|
||||
|
||||
TeamSchema.index({ points: -1 });
|
||||
TeamSchema.index({ adjustedPoints: -1 });
|
||||
|
||||
TeamSchema.methods.calculateAdjustedPoints = function() {
|
||||
if (this.memberCount === 0) {
|
||||
this.adjustedPoints = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Adjustment factor: smaller teams get bonus multiplier
|
||||
// 1-5 members: 1.5x, 6-10: 1.3x, 11-15: 1.1x, 16+: 1.0x
|
||||
let multiplier = 1.0;
|
||||
if (this.memberCount <= 5) multiplier = 1.5;
|
||||
else if (this.memberCount <= 10) multiplier = 1.3;
|
||||
else if (this.memberCount <= 15) multiplier = 1.1;
|
||||
|
||||
this.adjustedPoints = Math.round(this.points * multiplier);
|
||||
return this.adjustedPoints;
|
||||
};
|
||||
|
||||
export default mongoose.model<ITeam>("Team", TeamSchema);
|
||||
28
src/models/User.ts
Normal file
28
src/models/User.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface IUser extends Document {
|
||||
userId: string;
|
||||
username: string;
|
||||
points: number;
|
||||
teamId?: mongoose.Types.ObjectId;
|
||||
dailyQuestionsCompleted: number;
|
||||
weeklyQuestionsCompleted: number;
|
||||
lastActive: Date;
|
||||
joinedAt: Date;
|
||||
}
|
||||
|
||||
const UserSchema = new Schema<IUser>({
|
||||
userId: { type: String, required: true, unique: true, index: true },
|
||||
username: { type: String, required: true },
|
||||
points: { type: Number, default: 0 },
|
||||
teamId: { type: Schema.Types.ObjectId, ref: "Team", default: null },
|
||||
dailyQuestionsCompleted: { type: Number, default: 0 },
|
||||
weeklyQuestionsCompleted: { type: Number, default: 0 },
|
||||
lastActive: { type: Date, default: Date.now },
|
||||
joinedAt: { type: Date, default: Date.now }
|
||||
});
|
||||
|
||||
UserSchema.index({ points: -1 });
|
||||
UserSchema.index({ lastActive: -1 });
|
||||
|
||||
export default mongoose.model<IUser>("User", UserSchema);
|
||||
0
src/test.ts
Normal file
0
src/test.ts
Normal file
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user