diff --git a/README.md b/README.md
index d5c61359..03a71709 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,10 @@ You can follow the instructions outlined [in this document](docs/SETUP.md).
You can follow the steps [in this document](docs/CONTRIBUTING.md).
+## Command Wiki
+
+Check out our [Command Wiki](docs/COMMAND-WIKI.md) for more details on CodeyBot commands!
+
## License
All rights reserved for images.
diff --git a/docker-compose.yml b/docker-compose.yml
index 1134b407..68fb4dd9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,3 +10,4 @@ services:
- ./src:/usr/app/src
- ./logs:/usr/app/logs
- ./db:/usr/app/db
+ - ./docs:/usr/app/docs
diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md
new file mode 100644
index 00000000..787770ec
--- /dev/null
+++ b/docs/COMMAND-WIKI.md
@@ -0,0 +1,327 @@
+# ADMIN
+## ban
+- **Aliases:** None
+- **Description:** Ban a user.
+- **Examples:**
`.ban @jeff spam`
+- **Options:**
+ - ``user``: The user to ban.
+ - ``reason``: The reason why we are banning the user.
+- **Subcommands:** None
+
+# COIN
+## coin
+- **Aliases:** None
+- **Description:** Handle coin functions.
+- **Examples:**
`.coin adjust @Codey 100`
`.coin adjust @Codey -100 Codey broke.`
`.coin`
`.coin check @Codey`
`.coin c @Codey`
`.coin info`
`.coin i`
`.coin update @Codey 100`
`.coin update @Codey 0 Reset Codey's balance.`
`.coin transfer @Codey 10`
`.coin transfer @Codey 15 Lost a bet to Codey `
+- **Options:** None
+- **Subcommands:** `adjust`, `check`, `info`, `update`, `leaderboard`, `transfer`
+
+## coin adjust
+- **Aliases:** `a`
+- **Description:** Adjust the coin balance of a user.
+- **Examples:**
`.coin adjust @Codey 100`
`.coin adjust @Codey -100 Codey broke.`
+- **Options:**
+ - ``user``: The user to adjust the balance of.
+ - ``amount``: The amount to adjust the balance of the specified user by.
+ - ``reason``: The reason why we are adjusting the balance.
+- **Subcommands:** None
+
+## coin check
+- **Aliases:** `c`, `b`, `balance`, `bal`
+- **Description:** The user to check the balance of.
+- **Examples:**
`.coin check @Codey`
`.coin c @Codey`
+- **Options:**
+ - ``user``: The user to check the balance of.
+- **Subcommands:** None
+
+## coin info
+- **Aliases:** `information`, `i`
+- **Description:** Get info about Codey coin.
+- **Examples:**
`.coin info`
`.coin information`
`.coin i`
+- **Options:** None
+- **Subcommands:** None
+
+## coin leaderboard
+- **Aliases:** `lb`
+- **Description:** Get the current coin leaderboard.
+- **Examples:**
`.coin lb`
`.coin leaderboard`
+- **Options:** None
+- **Subcommands:** None
+
+## coin transfer
+- **Aliases:** `t`
+- **Description:** Transfer coins from your balance to another user.
+- **Examples:**
`.coin transfer @Codey 10`
`.coin transfer @Codey 10 Lost a bet to @Codey`
+- **Options:**
+ - ``user``: The user to transfer coins to.
+ - ``amount``: The amount to transfer to the specified user.
+ - ``reason``: The reason for transferring.
+- **Subcommands:** None
+
+## coin update
+- **Aliases:** `u`
+- **Description:** Update the coin balance of a user.
+- **Examples:**
`.coin update @Codey 100`
+- **Options:**
+ - ``user``: The user to update the balance of.
+ - ``amount``: The amount to update the balance of the specified user to.
+ - ``reason``: The reason why we are updating the balance.
+- **Subcommands:** None
+
+# COMPANY
+## company
+- **Aliases:** None
+- **Description:** None
+- **Examples:**
`.company add coinbase SRE`
`.company find coinbase`
+- **Options:** None
+- **Subcommands:** `enroll`, `add`, `remove`, `find`, `profile`
+
+## company add
+- **Aliases:** `a`
+- **Description:** Add a company to your profile
+- **Examples:**
`.company add https://www.crunchbase.com/organization/microsoft`
`.company a microsoft `
+- **Options:**
+- **Subcommands:** None
+
+## company enroll
+- **Aliases:** `e`
+- **Description:** None
+- **Examples:**
`.company enroll https://www.crunchbase.com/organization/microsoft`
`.company enroll microsoft`
+- **Options:**
+- **Subcommands:** None
+
+## company find
+- **Aliases:** `f`
+- **Description:** Find all individuals that work at the company.
+- **Examples:**
`.company find https://www.crunchbase.com/organization/microsoft`
`.company f microsoft`
+- **Options:**
+- **Subcommands:** None
+
+## company profile
+- **Aliases:** `p`
+- **Description:** List all the companies you are associated with
+- **Examples:**
`.company profile`
`.company p`
+- **Options:** None
+- **Subcommands:** None
+
+## company remove
+- **Aliases:** `r`
+- **Description:** Remove a company to your profile
+- **Examples:**
`.company remove https://www.crunchbase.com/organization/microsoft`
`.company r microsoft `
+- **Options:**
+- **Subcommands:** None
+
+# FUN
+## flipcoin
+- **Aliases:** `fc`, `flip`, `flip-coin`, `coin-flip`, `coinflip`
+- **Description:** None
+- **Examples:**
`.flip-coin`
`.fc`
`.flip`
`.coin-flip`
`.coinflip`
`.flipcoin`
+- **Options:** None
+- **Subcommands:** None
+
+## rolldice
+- **Aliases:** `rd`, `roll`, `roll-dice`, `dice-roll`, `diceroll`, `dice`
+- **Description:** Roll a dice! :game_die:
+- **Examples:**
`.roll-dice 6`
`.dice-roll 30`
`.roll 100`
`.rd 4`
`.diceroll 2`
`.dice 1`
`.rolldice 10`
+- **Options:**
+ - ``sides``: The number of sides on the die.
+- **Subcommands:** None
+
+# GAMES
+## bj
+- **Aliases:** `blj`, `blackjack`, `21`
+- **Description:** Play a Blackjack game to win some Codey coins!
+- **Examples:**
`.bj 100`
`.blj 100`
+- **Options:**
+ - ``bet``: A valid bet amount
+- **Subcommands:** None
+
+## connect4
+- **Aliases:** None
+- **Description:** Play Connect 4!
+- **Examples:**
`.connect4`
`.connect 4 @user`
+- **Options:** None
+- **Subcommands:** None
+
+## rps
+- **Aliases:** None
+- **Description:** Play Rock, Paper, Scissors!
+- **Examples:**
`.rps`
`.rps 10`
+- **Options:**
+ - ``bet``: How much to bet - default is 10.
+- **Subcommands:** None
+
+# INTERVIEWER
+## interviewers
+- **Aliases:** `int`, `interviewer`
+- **Description:** Handle interviewer functions.
+- **Examples:**
`.interviewer`
`.interviewer frontend`
+- **Options:** None
+- **Subcommands:** `clear`, `domain`, `pause`, `profile`, `resume`, `signup`, `list`
+
+## interviewer clear
+- **Aliases:** `clr`
+- **Description:** Clear all your interviewer data
+- **Examples:**
`.interviewer clear`
+- **Options:** None
+- **Subcommands:** None
+
+## interviewer domain
+- **Aliases:** `domain`
+- **Description:** Add/remove a domain of your choice
+- **Examples:**
`.interviewer domain frontend`
+- **Options:**
+ - ``domain_name``: A valid domain name
+- **Subcommands:** None
+
+## interviewer list
+- **Aliases:** `ls`
+- **Description:** List all interviewers or those under a specific domain
+- **Examples:**
`.interviewer list`
`.interviewer list backend`
+- **Options:**
+ - ``domain``: The domain to be examined
+- **Subcommands:** None
+
+## interviewer pause
+- **Aliases:** `ps`
+- **Description:** Put your interviewer profile on pause
+- **Examples:**
`.interviewer pause`
+- **Options:** None
+- **Subcommands:** None
+
+## interviewer profile
+- **Aliases:** `pf`
+- **Description:** Display your interviewer profile data
+- **Examples:**
`.interviewer profile`
+- **Options:** None
+- **Subcommands:** None
+
+## interviewer resume
+- **Aliases:** `resume`
+- **Description:** Resume your interviewer profile
+- **Examples:**
`.interviewer resume`
+- **Options:** None
+- **Subcommands:** None
+
+## interviewer signup
+- **Aliases:** `signup`
+- **Description:** Sign yourself up to be an interviewer!
+- **Examples:**
`.interviewer signup www.calendly.com`
+- **Options:**
+ - ``calendar_url``: A valid calendly.com or x.ai calendar link
+- **Subcommands:** None
+
+# LEETCODE
+## leetcode
+- **Aliases:** None
+- **Description:** Handle LeetCode functions.
+-
+- **Options:** None
+- **Subcommands:** `random`, `specific`
+
+## leetcode random
+- **Aliases:** `r`
+- **Description:** Get a random LeetCode problem.
+- **Examples:**
`.leetcode`n
`.leetcode random`
+- **Options:**
+ - ``difficulty``: The difficulty of the problem.
+- **Subcommands:** None
+
+## leetcode specific
+- **Aliases:** `spec`, `s`
+- **Description:** Get a LeetCode problem with specified problem ID.
+- **Examples:**
`.leetcode specific 1`
+- **Options:**
+ - ``problem-id``: The problem ID.
+- **Subcommands:** None
+
+# MISCELLANEOUS
+## help
+- **Aliases:** `wiki`
+- **Description:** Get the URL to the wiki page.
+- **Examples:**
`.help`
`.wiki`
+- **Options:** None
+- **Subcommands:** None
+
+## info
+- **Aliases:** None
+- **Description:** Get Codey information - app version, repository link and issue templates.
+- **Examples:**
`.info`
+- **Options:** None
+- **Subcommands:** None
+
+## member
+- **Aliases:** None
+- **Description:** Get CSC membership information of a user.
+- **Examples:**
`.member [id]`
+- **Options:**
+ - ``uwid``: The Quest ID of the user.
+- **Subcommands:** None
+
+## ping
+- **Aliases:** `pong`
+- **Description:** Ping the bot to see if it is alive. :ping_pong:
+- **Examples:**
`.ping`
`.pong`
+- **Options:** None
+- **Subcommands:** None
+
+## uptime
+- **Aliases:** `up`, `timeup`
+- **Description:** None
+- **Examples:**
`.uptime`
`.up`
`.timeup`
+- **Options:** None
+- **Subcommands:** None
+
+# PROFILE
+## profile
+- **Aliases:** `userprofile`, `aboutme`
+- **Description:** Handle user profile functions.
+- **Examples:**
`.profile @Codey`
+- **Options:** None
+- **Subcommands:** `about`, `grad`, `set`
+
+## profile about
+- **Aliases:** `a`
+- **Description:** Display user profile.
+- **Examples:**
`.profile about @Codey`
`.profile a @Codey`
+- **Options:**
+ - ``user``: The user to give profile of.
+- **Subcommands:** None
+
+## profile grad
+- **Aliases:** `g`
+- **Description:** Update Grad Roles.
+- **Examples:**
`.profile grad`
`.profile g`
+- **Options:** None
+- **Subcommands:** None
+
+## profile set
+- **Aliases:** `s`
+- **Description:** Set parameters of user profile.
+- **Examples:**
`.profile set @Codey`
`.profile a @Codey`
+- **Options:**
+ - ``customization``: The customization to be set for the user.
+ - ``description``: The description of the customization to be set for the user.
+- **Subcommands:** None
+
+# SUGGESTION
+## suggestion
+- **Aliases:** ``suggest``
+- **Description:** Handle suggestion functions.
+- This command will forward a suggestion to the CSC Discord Mods. Please note that your suggestion is not anonymous, your Discord username and ID will be recorded. If you don't want to make a suggestion in public, you could use this command via a DM to Codey instead.
+ **Examples:**
+ ``.suggestion I want a new Discord channel named #hobbies.``
+- **Options:**
+ - ``details``: Details of your suggestion
+- **Subcommands:** ``list``, ``update``, ``create``
+
+# COFFEE CHAT
+## coffee
+- **Aliases:** None
+- **Description:** Handle coffee chat functions.
+- **Examples:**
+ ``.coffee match``
+ ``.coffee test 10``
+- **Options:** None
+- **Subcommands:** ``match``, ``test``
+
diff --git a/src/commandDetails/miscellaneous/help.ts b/src/commandDetails/miscellaneous/help.ts
index aa9a90d1..a0e6effd 100644
--- a/src/commandDetails/miscellaneous/help.ts
+++ b/src/commandDetails/miscellaneous/help.ts
@@ -5,7 +5,7 @@ import {
SapphireMessageResponse,
} from '../../codeyCommand';
-const wikiLink = 'https://github.com/uwcsc/codeybot/wiki/Command-Help';
+const wikiLink = 'https://github.com/uwcsc/codeybot/blob/main/docs/COMMAND-WIKI.md';
const helpExecuteCommand: SapphireMessageExecuteType = (
_client,
diff --git a/src/events/ready.ts b/src/events/ready.ts
index 105277f7..2b7aba1c 100644
--- a/src/events/ready.ts
+++ b/src/events/ready.ts
@@ -5,6 +5,7 @@ import { initEmojis } from '../components/emojis';
import { vars } from '../config';
import { logger } from '../logger/default';
import { getRepositoryReleases } from '../utils/github';
+import { updateWiki } from '../utils/updateWiki';
const dev = process.env.NODE_ENV !== 'production';
@@ -45,4 +46,5 @@ export const initReady = (client: Client): void => {
sendReady(client);
initCrons(client);
initEmojis(client);
+ updateWiki();
};
diff --git a/src/utils/updateWiki.ts b/src/utils/updateWiki.ts
new file mode 100644
index 00000000..8ced3243
--- /dev/null
+++ b/src/utils/updateWiki.ts
@@ -0,0 +1,258 @@
+import { statSync, readdirSync } from 'fs';
+import { appendFile, writeFile, readdir, readFile } from 'fs/promises';
+import { logger } from '../logger/default';
+
+// File Paths
+const wikiPath = 'docs/COMMAND-WIKI.md';
+const commandsDir = 'src/commands';
+const commandDetailsDir = 'src/commandDetails';
+
+// Commands with subcommands, to be handled separately
+const retrieveCompoundCommands = (): string[] => {
+ const compoundCommands: string[] = [];
+
+ const filesAndDirs = readdirSync(commandsDir);
+ const directories = filesAndDirs.filter((file) =>
+ statSync(`${commandsDir}/${file}`).isDirectory(),
+ );
+
+ for (const dir of directories) {
+ const subDir = `${commandsDir}/${dir}`;
+ const files = readdirSync(subDir);
+ if (files.length === 1 && files[0] === `${dir}.ts`) {
+ compoundCommands.push(dir);
+ }
+ }
+
+ return compoundCommands;
+};
+
+// As of Feb 8 2024, this should be ['coin', 'company', 'interviewer', 'leetcode', 'profile'];
+const compoundCommands: string[] = retrieveCompoundCommands();
+
+// RegEx patterns to extract info
+const commandDetailsPattern = /.*const .+CommandDetails: CodeyCommandDetails = {([\s\S]*?)\n}/;
+const namePattern = /name: '(.*?)'/;
+const aliasesPattern = /aliases: \[([\s\S]*?)\]/;
+const descriptionPattern = /description: '(.*?)'/;
+const detailedDescriptionPattern = /detailedDescription: `([\s\S]*?)`,/;
+const optionsPattern = /options: \[([\s\S]*?)\]/;
+const subcommandDetailsPattern = /subcommandDetails: {([\s\S]*?)},/;
+
+// ----------------------------------- START OF UTILITY FUNCTIONS ---------------------------- //
+
+const formatCommandName = async (
+ name: string | undefined,
+ baseCmd: string | undefined = undefined,
+) => {
+ let formattedName = '';
+ if (name === undefined) {
+ formattedName = 'None';
+ } else {
+ formattedName = name;
+ }
+
+ if (baseCmd === undefined) {
+ await appendFile(wikiPath, `## ${formattedName}\n`);
+ } else {
+ await appendFile(wikiPath, `## ${baseCmd} ${formattedName}\n`);
+ }
+};
+
+const formatAliases = async (aliases: string | undefined) => {
+ let formattedAliases = '';
+ if (aliases === undefined || aliases === '') {
+ formattedAliases = 'None';
+ } else {
+ formattedAliases = aliases
+ .replace(/'/g, '')
+ .split(', ')
+ .map((alias) => `\`${alias}\``)
+ .join(', ');
+ }
+ await appendFile(wikiPath, `- **Aliases:** ${formattedAliases}\n`);
+};
+
+const formatDescription = async (descr: string | undefined) => {
+ let formattedDescription = '';
+ if (descr === undefined) {
+ formattedDescription = 'None';
+ } else {
+ formattedDescription = descr;
+ }
+ await appendFile(wikiPath, `- **Description:** ${formattedDescription}\n`);
+};
+
+const formatDetailedDescription = async (listStr: string | undefined) => {
+ let formattedDetailedDescription = '';
+ if (listStr === undefined) {
+ formattedDetailedDescription = 'None';
+ } else {
+ const newlineList = listStr.replace(/\n/g, '
');
+ const botPrefixList = newlineList.replace(new RegExp('\\${container.botPrefix}', 'g'), '.');
+ const bulletList = botPrefixList.replace(/\\/g, '');
+ formattedDetailedDescription = bulletList;
+ }
+ await appendFile(wikiPath, `- ${formattedDetailedDescription}\n`);
+};
+
+const formatOptions = async (options: string[] | undefined) => {
+ if (options === undefined || options[0] === '') {
+ await appendFile(wikiPath, `- **Options:** None\n`);
+ } else {
+ let fieldMatch;
+ const extractedOptions = [];
+ const optionFieldsPattern = /\{\s*name: '(.*?)',\s*description: '(.*?)'/g;
+ while ((fieldMatch = optionFieldsPattern.exec(options![0])) !== null) {
+ const name = fieldMatch[1];
+ const description = fieldMatch[2];
+ extractedOptions.push({ name, description });
+ }
+ await appendFile(wikiPath, `- **Options:** \n`);
+ for (const desc of extractedOptions) {
+ await appendFile(wikiPath, ` - \`\`${desc.name}\`\`: ${desc.description}\n`);
+ }
+ }
+};
+
+const formatSubcommandDetails = async (subcommandDetails: string | undefined) => {
+ let formattedSubcommandDetails = '';
+ if (subcommandDetails === undefined || subcommandDetails === '') {
+ formattedSubcommandDetails = 'None';
+ } else {
+ const regex = /\b(\w+)\s*:/g;
+ const matches = subcommandDetails.match(regex);
+ if (matches) {
+ const subcommandList = matches.map((match) => match.trim().replace(/:$/, '')).join(', ');
+ formattedSubcommandDetails = subcommandList
+ .replace(/'/g, '')
+ .split(', ')
+ .map((subcmd) => `\`${subcmd}\``)
+ .join(', ');
+ }
+ }
+ await appendFile(wikiPath, `- **Subcommands:** ${formattedSubcommandDetails}\n\n`);
+};
+
+const extractAndFormat = async (
+ codeSection: string,
+ baseCmd: string | undefined = undefined,
+): Promise => {
+ // Extract info pieces
+ const name = codeSection.match(namePattern)?.[1];
+ const aliases = codeSection.match(aliasesPattern)?.[1];
+ const description = codeSection.match(descriptionPattern)?.[1];
+ const detailedDescription = codeSection.match(detailedDescriptionPattern)?.[1];
+ const options = codeSection.match(optionsPattern)?.[1].split(', ');
+ const subcommandDetails = codeSection.match(subcommandDetailsPattern)?.[1];
+
+ // Just in case command name cannot be extracted, which is not to be expected
+ if (name === undefined) {
+ logger.error({
+ message: `Could not find command name from ${codeSection}`,
+ });
+ return;
+ }
+
+ // Format the info
+ if (baseCmd === undefined) {
+ await formatCommandName(name);
+ } else {
+ await formatCommandName(name, baseCmd);
+ }
+ await formatAliases(aliases);
+ await formatDescription(description);
+ await formatDetailedDescription(detailedDescription);
+ await formatOptions(options);
+ await formatSubcommandDetails(subcommandDetails);
+};
+
+// ----------------------------------- END OF UTILITY FUNCTIONS ---------------------------- //
+
+export const updateWiki = async (): Promise => {
+ logger.info({
+ message: 'Updating wiki...',
+ });
+
+ // Refresh COMMAND-WIKI.md
+ await writeFile(wikiPath, '');
+
+ const filesAndDirs = await readdir(commandDetailsDir);
+ const directories = filesAndDirs.filter((file) =>
+ statSync(`${commandDetailsDir}/${file}`).isDirectory(),
+ );
+
+ for (const dir of directories) {
+ if (!compoundCommands.includes(dir)) {
+ await appendFile(wikiPath, `# ${dir.toUpperCase()}\n`);
+ const subDir = `${commandDetailsDir}/${dir}`;
+ const files = await readdir(subDir);
+ for (const file of files) {
+ const filePath = `${subDir}/${file}`;
+ const content = await readFile(filePath, 'utf-8');
+ const match = content.match(commandDetailsPattern);
+ if (match) {
+ const codeSection = match[1];
+ await extractAndFormat(codeSection);
+ }
+ }
+ } else {
+ await appendFile(wikiPath, `# ${dir.toUpperCase()}\n`);
+ const mainCommandDir = `${commandsDir}/${dir}`;
+ const subCommandsdir = `${commandDetailsDir}/${dir}`;
+
+ // Retrieve overview info
+ const mainCommandFiles = await readdir(mainCommandDir);
+ for (const file of mainCommandFiles) {
+ const filePath = `${mainCommandDir}/${file}`;
+ const content = await readFile(filePath, 'utf-8');
+ const match = content.match(commandDetailsPattern);
+ if (match) {
+ const codeSection = match[1];
+ await extractAndFormat(codeSection);
+ }
+ }
+
+ // Retrieve subcommand infos
+ const subCommandFiles = await readdir(subCommandsdir);
+ for (const file of subCommandFiles) {
+ const filePath = `${subCommandsdir}/${file}`;
+ const content = await readFile(filePath, 'utf-8');
+ const match = content.match(commandDetailsPattern);
+ if (match) {
+ const codeSection = match[1];
+ await extractAndFormat(codeSection, dir);
+ }
+ }
+ }
+ }
+
+ // Harcoding info for suggestion until it can be migrated to CodeyCommand framework
+ const suggestionContents = `# SUGGESTION
+## suggestion
+- **Aliases:** \`\`suggest\`\`
+- **Description:** Handle suggestion functions.
+- This command will forward a suggestion to the CSC Discord Mods. Please note that your suggestion is not anonymous, your Discord username and ID will be recorded. If you don't want to make a suggestion in public, you could use this command via a DM to Codey instead.
+ **Examples:**
+ \`\`.suggestion I want a new Discord channel named #hobbies.\`\`
+- **Options:**
+ - \`\`details\`\`: Details of your suggestion
+- **Subcommands:** \`\`list\`\`, \`\`update\`\`, \`\`create\`\``;
+ await appendFile(wikiPath, `${suggestionContents}\n\n`);
+
+ // Harcoding info for coffechat until it can be migrated to CodeyCommand framework
+ const coffeeContents = `# COFFEE CHAT
+## coffee
+- **Aliases:** None
+- **Description:** Handle coffee chat functions.
+- **Examples:**
+ \`\`.coffee match\`\`
+ \`\`.coffee test 10\`\`
+- **Options:** None
+- **Subcommands:** \`\`match\`\`, \`\`test\`\``;
+ await appendFile(wikiPath, `${coffeeContents}\n\n`);
+
+ logger.info({
+ message: 'Wiki successfully updated.',
+ });
+};