Skip to content

Commit

Permalink
yay
Browse files Browse the repository at this point in the history
Akhil Pillai committed Nov 18, 2024
1 parent 0428ccf commit b5118dd
Showing 32 changed files with 5,018 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .commitlintrc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
extends: ['@commitlint/config-conventional']
};
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.commitlintrc.ts
.eslint*
.git*
.npm*
.prettier*
knip.ts
README.md
5 changes: 5 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
botfiles
.devcontainer
dist
**/*.js
58 changes: 58 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"root": true,
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"camelcase": "error",
"capitalized-comments": "off",
"comma-dangle": "error",
"eol-last": "error",
"func-call-spacing": "error",
"getter-return": "error",
"indent": "off",
"new-parens": "error",
"no-alert": "error",
"no-console": "error",
"no-const-assign": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-args": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
"no-eq-null": "error",
"no-eval": "error",
"no-fallthrough": "error",
"no-implied-eval": "error",
"no-invalid-regexp": "error",
"no-invalid-this": "error",
"no-irregular-whitespace": "error",
"no-lonely-if": "error",
"no-multi-str": "error",
"no-param-reassign": "error",
"no-redeclare": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-trailing-spaces": "error",
"no-unneeded-ternary": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-unused-vars": "off",
"no-var": "error",
"no-with": "error",
"object-curly-newline": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-promise-reject-errors": "error",
"prefer-template": "error",
"semi-spacing": "error",
"strict": "error",
"use-isnan": "error"
}
}
26 changes: 26 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Build and push Docker image

on:
workflow_dispatch:
push:
branches:
- 'gitmaster'

jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: akpi816218/transconlang-discord:latest
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules

*.env
*..env.local

*.db.json
*.tmp.json
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.tmp.db.json
*.cache.json
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"singleQuote": true,
"useTabs": true,
"tabWidth": 2,
"trailingComma": "none",
"arrowParens": "avoid",
"bracketSpacing": true,
"semi": true
}
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Start your image with a node base image
FROM node:latest

# The /app directory should act as the main application directory
WORKDIR /app

# Copy the app package and package-lock.json file
COPY package*.json ./

# Copy local directories to the current local directory of our docker image (/app)
COPY ./src ./src
COPY ./botfiles ./botfiles
COPY ./scripts ./scripts

# Install node packages, install serve, build the app, and remove dependencies at the end
RUN npm ci

EXPOSE 8000

# Start the app using serve command
CMD [ "npm", "start" ]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# dictionary-discord-bot

The official dictionary Discord bot for Kumilinwa, the trans constructed language! :3
8 changes: 8 additions & 0 deletions knip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { KnipConfig } from 'knip';

const config: KnipConfig = {
entry: ['src/index.ts', 'src/{commands,events}/*.ts', 'scripts/*.ts'],
project: ['src/**/*.ts']
};

export default config;
4,135 changes: 4,135 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"author": "Akhil Pillai",
"dependencies": {
"discord.js": "^14.16.3",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"helmet": "^8.0.0",
"jsoning": "^1.0.1",
"node-schedule": "^2.1.1",
"pino": "^9.5.0",
"pino-pretty": "^13.0.0"
},
"devDependencies": {
"@types/dotenv": "^8.2.3",
"@types/express": "^5.0.0",
"@types/node-schedule": "^2.1.7",
"@typescript-eslint/eslint-plugin": "^8.14.0",
"@typescript-eslint/parser": "^8.14.0",
"knip": "^5.37.1",
"npm-check-updates": "^17.1.11",
"prettier": "^3.3.3",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
},
"main": "src/index.ts",
"type": "module",
"private": true,
"scripts": {
"build-commands": "tsx scripts/buildCommands.ts",
"build-image": "docker build -t akpi816218/transconlang-discord . && docker push akpi816218/transconlang-discord",
"cache-langspec": "tsx scripts/cacheLangSpec.ts",
"check": "tsc",
"deploy": "npm ci && npm start",
"deploy-full": "npm ci && npm urn build-commands && npm urn cache-langspec && npm start",
"fmt": "prettier -w .",
"knip": "knip",
"lint": "eslint .",
"start": "tsx src/index.ts",
"up": "ncu -u && npm i"
}
}
31 changes: 31 additions & 0 deletions scripts/buildCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-disable no-console */
import 'dotenv/config';
import { commandsPath, registerCommands } from './registerCommands';
import { argv } from 'process';
import { readdir } from 'fs/promises';

argv.shift();
argv.shift();

let commandFiles: string[];
if (argv.length == 0) {
commandFiles = (await readdir(commandsPath)).filter(file =>
file.endsWith('.ts')
);
} else {
commandFiles = (await readdir(commandsPath)).filter(
file => file.endsWith('.ts') && argv.includes(file.replace('.ts', ''))
);
}
if (commandFiles.length == 0) {
console.log('No commands found');
process.exit(1);
}

console.log(`Registering ${commandFiles.length} commands...`);

console.log(
await (
await registerCommands(process.env.DISCORD_TOKEN as string, commandFiles)
).getCommands()
);
16 changes: 16 additions & 0 deletions scripts/cacheLangSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LangSpecURL } from '@/config';
import { writeFile } from 'fs/promises';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

await writeFile(
join(
dirname(fileURLToPath(import.meta.url)),
'..',
'src',
'lib',
'kumilinwa',
'langspec.cache.json'
),
JSON.stringify(await fetch(LangSpecURL).then(res => res.json()))
);
41 changes: 41 additions & 0 deletions scripts/registerCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { REST, Routes } from 'discord.js';
import { dirname, join } from 'path';
import { clientId } from '../src/config';
import { fileURLToPath } from 'url';
import { readdir } from 'fs/promises';
import Jsoning, { JSONValue } from 'jsoning';
import { Command } from '../src/lib/struct/discord/types';

const Dirname = dirname(fileURLToPath(import.meta.url));

export const commandsPath = join(Dirname, '..', 'src', 'commands');

export async function registerCommands(
token: string,
commandFiles?: string[]
): Promise<{
data: unknown;
getCommands: () => Promise<unknown>;
rest: REST;
}> {
// eslint-disable-next-line no-param-reassign
commandFiles =
commandFiles ??
(await readdir(commandsPath)).filter(file => file.endsWith('.ts'));
const commands = [];
for (const file of commandFiles)
commands.push(
((await import(join(commandsPath, file))) as Command).data.toJSON()
);
let data: unknown;
const rest = new REST().setToken(token);
await rest.put(Routes.applicationCommands(clientId), {
body: commands
});
return {
data,
getCommands: async () =>
await rest.get(Routes.applicationCommands(clientId)),
rest
};
}
45 changes: 45 additions & 0 deletions src/commands/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MatchType, searchLangSpec } from '@/lib';
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';

export const data = new SlashCommandBuilder()
.setName('search')
.setDescription('Search for an entry in the dictionary')
.addStringOption(option =>
option.setName('query').setDescription('The search query').setRequired(true)
)
.addStringOption(option =>
option
.setName('match')
.setDescription('The type of match to use (default: any)')
.setRequired(false)
.setChoices(
{ name: 'Word', value: 'word' },
{ name: 'Meaning', value: 'meaning' }
)
)
.addBooleanOption(option =>
option
.setName('ephemeral')
.setDescription('Whether to send an ephemeral response')
.setRequired(false)
);

export async function execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply({
ephemeral: interaction.options.getBoolean('ephemeral', false) ?? false
});
const query = interaction.options.getString('query', true),
match = interaction.options.getString('match', false);
const results = await searchLangSpec(
query,
(match as MatchType) ?? undefined
);
const response = results.length
? results.map(({ word, meaning }) => `${word}: ${meaning}`).join('\n')
: 'No results found.';
await interaction.editReply(
response.length > 2000
? `Whoa, that's a lot of results! Discord's char limit is set to 2000 per message. Try narrowing down your search.\nYour search query: ${query}`
: response
);
}
10 changes: 10 additions & 0 deletions src/commands/z.command.tstemplate
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {
ChatInputCommandInteraction,
SlashCommandBuilder
} from 'discord.js';

export const data = new SlashCommandBuilder()
.setName('name')
.setDescription('description');

export async function execute(interaction: ChatInputCommandInteraction) {}
20 changes: 20 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PermissionFlagsBits, PermissionsBitField } from 'discord.js';

export const clientId = '1307953747741245460';

export const DevIds = ['817214551740776479'];

export const permissionsBits = new PermissionsBitField().add(
PermissionFlagsBits.AddReactions,
PermissionFlagsBits.EmbedLinks,
PermissionFlagsBits.ReadMessageHistory,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.SendMessagesInThreads,
PermissionFlagsBits.ViewChannel
).bitfield;

export const PORT = 8000;

// ! change branch to main later
export const LangSpecURL =
'https://raw.githubusercontent.com/Transconlang/translang/refs/heads/rawspec/rawspec/0-complete.json';
266 changes: 266 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import 'dotenv/config';
import {
ActivityType,
Colors,
EmbedBuilder,
Events,
OAuth2Scopes,
PresenceUpdateStatus,
TimestampStyles,
codeBlock,
time
} from 'discord.js';
import { argv, stdout } from 'process';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { readdir, rm, writeFile } from 'fs/promises';
import { Jsoning } from 'jsoning';
import { DevIds, LangSpecURL, permissionsBits, PORT } from './config';
import {
Command,
CommandClient,
createServer,
FullEntry,
logger,
Methods
} from './lib';
import { scheduleJob } from 'node-schedule';

const Dirname = dirname(fileURLToPath(import.meta.url));

argv.shift();
argv.shift();
if (argv.includes('-d')) {
logger.level = 'debug';
logger.debug('Debug mode enabled.');
}

const StatsDB = new Jsoning('stats.tmp.db.json');
logger.debug('Created stats database.');

const client = new CommandClient({
intents: [],
presence: {
activities: [
{
name: 'Kumilinwa',
type: ActivityType.Listening
}
],
afk: false,
status: PresenceUpdateStatus.Online
}
});
logger.debug('Created client instance.');

const server = createServer(
{
handler: (_req, res) =>
res.redirect(
client.generateInvite({
permissions: permissionsBits,
scopes: [OAuth2Scopes.Bot]
})
),
method: Methods.GET,
route: '/invite'
},
{
handler: (_req, res) => res.redirect('/status'),
method: Methods.GET,
route: '/'
},
{
handler: (_req, res) => res.sendStatus(client.isReady() ? 200 : 503),
method: Methods.GET,
route: '/status'
},
{
handler: (req, res) => {
if (
req.headers['content-type'] !== 'application/json' &&
req.headers['content-type'] != undefined
)
res.status(415).end();
else if (client.isReady())
res
.status(200)
.contentType('application/json')
.send({
clientPing: client.ws.ping,
clientReady: client.isReady(),
commandCount: client.application!.commands.cache.size,
guildCount: client.application!.approximateGuildCount,
lastReady: client.readyAt.valueOf(),
uptime: client.uptime
})
.end();
else res.status(503).end();
},
method: Methods.GET,
route: '/info'
},
{
handler: (req, res) => {
if (
req.headers['content-type'] !== 'application/json' &&
req.headers['content-type'] != undefined
)
res.status(415).end();
else if (client.isReady())
res
.status(200)
.contentType('application/json')
.send({
commands: client.commands.map(command => ({
data: command.data.toJSON(),
help: command.help?.toJSON()
})),
timestamp: Date.now()
})
.end();
else res.status(503).end();
},
method: Methods.GET,
route: '/commands'
}
);
logger.debug('Created server instance.');

const commandsPath = join(Dirname, 'commands');
const commandFiles = (await readdir(commandsPath)).filter(file =>
file.endsWith('.ts')
);
logger.debug('Loaded command files.');
const cmndb = new Jsoning('cmnds.tmp.db.json');
for (const file of commandFiles) {
const filePath = join(commandsPath, file);
logger.debug(`Loading command ${filePath}`);
const command: Command = await import(filePath);
client.commands.set(command.data.name, command);
if (command.help)
await cmndb.set(
command.data.name,
// @ts-expect-error types
command.help.toJSON()
);
}
client.commands.freeze();
logger.info('Loaded commands.');

/**
const eventsPath = join(Dirname, 'events');
const eventFiles = readdirSync(eventsPath).filter(file => file.endsWith('.ts'));
for (const file of eventFiles) {
const filePath = join(eventsPath, file);
const event: Event = await import(filePath);
if (event.once)
client.once(event.name, async (...args) => await event.execute(...args));
else client.on(event.name, async (...args) => await event.execute(...args));
}
logger.debug('Loaded events.');
*/

client
.on(Events.ClientReady, () => logger.info('Client#ready'))
.on(Events.InteractionCreate, async interaction => {
if (interaction.user.bot) return;
if (interaction.isChatInputCommand()) {
const command = client.commands.get(interaction.commandName);
if (!command) {
await interaction.reply('Internal error: Command not found');
return;
}
try {
await command.execute(interaction);
} catch (e) {
logger.error(e);
if (interaction.replied || interaction.deferred) {
await interaction.editReply(
'There was an error while running this command.'
);
} else {
await interaction.reply({
content: 'There was an error while running this command.',
ephemeral: true
});
}
}
}
})
.on(Events.Debug, m => logger.debug(m))
.on(Events.Error, m => {
logger.error(m);
sendError(m);
})
.on(Events.Warn, m => logger.warn(m));
logger.debug('Set up client events.');

await refreshCachedLangSpec();
await StatsDB.set('langSpecCacheAge', Date.now());
logger.debug('Cached language specification.');

await client
.login(process.env.DISCORD_TOKEN)
.then(() => logger.info('Logged in.'));

process.on('SIGINT', async () => {
sendError(new Error('SIGINT received.'));
await client.destroy();
stdout.write('\n');
logger.info('Destroyed Client.');
await rm(join(Dirname, '..', 'stats.tmp.db.json'));
await rm(join(Dirname, '..', 'cmnds.tmp.db.json'));
logger.info('Removed temporary databases.');
process.exit(0);
});

server.listen(process.env.PORT ?? PORT);
logger.info(`Listening to HTTP server on port ${process.env.PORT ?? PORT}.`);

process.on('uncaughtException', sendError);
process.on('unhandledRejection', sendError);
logger.debug('Set up error handling.');

// refresh cached kumilinwa spec every hour
scheduleJob('0 * * * *', refreshCachedLangSpec);

logger.info('Process setup complete.');

async function sendError(e: Error) {
for (const devId of DevIds) {
client.users.fetch(devId).then(user => {
const date = new Date();
user.send({
embeds: [
new EmbedBuilder()
.setTitle('Error Log')
.setDescription(e.message)
.addFields({ name: 'Stack Trace', value: codeBlock(e.stack ?? '') })
.addFields({
name: 'ISO 8601 Timestamp',
value: date.toISOString()
})
.addFields({
name: 'Localized DateTime',
value: time(date, TimestampStyles.LongDateTime)
})
.setColor(Colors.Red)
.setTimestamp()
]
});
});
}
}

async function refreshCachedLangSpec() {
const data = (await fetch(LangSpecURL)
.then(res => res.json())
.catch(e => sendError)) as FullEntry[];
await writeFile(
join(Dirname, 'lib', 'kumilinwa', 'langspec.cache.json'),
JSON.stringify(data)
);
await StatsDB.set('langSpecCacheAge', Date.now());
logger.debug('Cached language specification.');
}
7 changes: 7 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './misc/logger';
export * from './misc/server';
export * from './struct/discord/types';
export * from './struct/discord/Extend';
export * from './struct/CommandHelpEntry';

export * from './kumilinwa';
5 changes: 5 additions & 0 deletions src/lib/kumilinwa/import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FullEntry } from './types';

export const CompleteLangSpec = (await import(
'./langspec.cache.json'
)).default as FullEntry[];
3 changes: 3 additions & 0 deletions src/lib/kumilinwa/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './types';
export * from './import';
export * from './search';
1 change: 1 addition & 0 deletions src/lib/kumilinwa/langspec.cache.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"word":"mi","meaning":"cute","type":"adjective"},{"word":"wusita","meaning":"good","type":"adjective"},{"word":"disita","meaning":"bad","type":"adjective"},{"word":"nen","meaning":"not","type":"adjective"},{"word":"toransu","meaning":"different, other","type":"adjective"},{"word":"somo","meaning":"sleepy","type":"adjective"},{"word":"sekiso","meaning":"sexy, fuckable","type":"adjective"},{"word":"deja","meaning":"active","type":"adjective"},{"word":"gara","meaning":"all","type":"adjective"},{"word":"coko","meaning":"some","type":"adjective"},{"word":"boro","meaning":"any","type":"adjective"},{"word":"gurati","meaning":"negative freedom, free (price)","type":"adjective"},{"word":"lifeta","meaning":"positive freedom, liberated","type":"adjective"},{"word":"jala","meaning":"able, capable of","type":"adjective"},{"word":"mire","meaning":"mid, average","type":"adjective"},{"word":"misu","meaning":"liquid, fluid","type":"adjective"},{"word":"ten","meaning":"solid, rigid","type":"adjective"},{"word":"lufi","meaning":"gaseous","type":"adjective"},{"word":"nen","meaning":"no","type":"adverb"},{"word":"con","meaning":"soon","type":"adverb"},{"word":"huba","meaning":"overly","type":"adverb"},{"word":"sun","meaning":"why","type":"adverb"},{"word":"saci","meaning":"how","type":"adverb"},{"word":"tahi","meaning":"now","type":"adverb"},{"word":"kahi","meaning":"then","type":"adverb"},{"word":"sohi","meaning":"when","type":"adverb"},{"word":"tawa","meaning":"here","type":"adverb"},{"word":"kawa","meaning":"there","type":"adverb"},{"word":"sowa","meaning":"where","type":"adverb"},{"word":"pum","meaning":"and","type":"conjunction"},{"word":"ron","meaning":"or","type":"conjunction"},{"word":"no","meaning":"but","type":"conjunction"},{"word":"nen","meaning":"no","type":"interjection"},{"word":"henlo","meaning":"hello","type":"interjection"},{"word":"na","meaning":"hi","type":"interjection"},{"word":"konba","meaning":"goodbye","type":"interjection"},{"word":"3","meaning":"cute!","type":"interjection"},{"word":"ke","meaning":"ok","type":"interjection"},{"word":"biwe","meaning":"thanks","type":"interjection"},{"word":"hihi","meaning":"laughter","type":"interjection"},{"word":"kase","meaning":"cat","type":"noun"},{"word":"hun","meaning":"dog","type":"noun"},{"word":"luma","meaning":"person","type":"noun"},{"word":"mawa","meaning":"kiss","type":"noun"},{"word":"lanmi","meaning":"food","type":"noun"},{"word":"topi","meaning":"god, deity","type":"noun"},{"word":"wantu","meaning":"number","type":"noun"},{"word":"cosi","meaning":"choice, vote","type":"noun"},{"word":"falo","meaning":"speech, discussion, chat","type":"noun"},{"word":"linwa","meaning":"language","type":"noun"},{"word":"keso","meaning":"cheese","type":"noun"},{"word":"sekiso","meaning":"sex, act of fucking","type":"noun"},{"word":"lumaba","meaning":"friend","type":"noun"},{"word":"deja","meaning":"action","type":"noun"},{"word":"gade","meaning":"guess","type":"noun"},{"word":"tempu","meaning":"time","type":"noun"},{"word":"cikondu","meaning":"second","type":"noun"},{"word":"minedu","meaning":"minute","type":"noun"},{"word":"haweru","meaning":"hour","type":"noun"},{"word":"dawu","meaning":"day","type":"noun"},{"word":"natu","meaning":"night","type":"noun"},{"word":"murinu","meaning":"morning","type":"noun"},{"word":"ebeninku","meaning":"evening","type":"noun"},{"word":"monadu","meaning":"month","type":"noun"},{"word":"siconu","meaning":"season","type":"noun"},{"word":"waru","meaning":"year","type":"noun"},{"word":"diwaru","meaning":"decade","type":"noun"},{"word":"huwaru","meaning":"century","type":"noun"},{"word":"newaru","meaning":"millenium","type":"noun"},{"word":"nem","meaning":"mind","type":"noun"},{"word":"pen","meaning":"movement","type":"noun"},{"word":"nepen","meaning":"feeling","type":"noun"},{"word":"gurati","meaning":"negative freedom","type":"noun"},{"word":"lifeta","meaning":"positive freedom","type":"noun"},{"word":"kumi","meaning":"trans(gender)ness","type":"noun"},{"word":"jala","meaning":"ability","type":"noun"},{"word":"mire","meaning":"half, middle","type":"noun"},{"word":"misu","meaning":"liquid","type":"noun"},{"word":"lufi","meaning":"gas","type":"noun"},{"word":"ten","meaning":"solid","type":"noun"},{"word":"kin","meaning":"thing","type":"noun"},{"word":"meta","meaning":"measurment","type":"noun"},{"word":"core","meaning":"desire","type":"noun"},{"word":"jore","meaning":"need","type":"noun"},{"word":"lonki","meaning":"field of knowledge/study","type":"noun"},{"word":"tori","meaning":"kindness, love","type":"noun"},{"word":"toba","meaning":"word","type":"noun"},{"word":"Word","meaning":"Number","type":"number"},{"word":"**sero**","meaning":"0","type":"number"},{"word":"**wano**","meaning":"1","type":"number"},{"word":"**duwo**","meaning":"2","type":"number"},{"word":"**tero**","meaning":"3","type":"number"},{"word":"**karo**","meaning":"4","type":"number"},{"word":"**bimo**","meaning":"5","type":"number"},{"word":"**ciko**","meaning":"6","type":"number"},{"word":"**sebo**","meaning":"7","type":"number"},{"word":"**wonto**","meaning":"8","type":"number"},{"word":"**ninto**","meaning":"9","type":"number"},{"word":"**dinko**","meaning":"10","type":"number"},{"word":"dunko","meaning":"20","type":"number"},{"word":"tenko","meaning":"30","type":"number"},{"word":"kanko","meaning":"40","type":"number"},{"word":"binko","meaning":"50","type":"number"},{"word":"cinko","meaning":"60","type":"number"},{"word":"senko","meaning":"70","type":"number"},{"word":"wonko","meaning":"80","type":"number"},{"word":"ninko","meaning":"90","type":"number"},{"word":"**hundo**","meaning":"100","type":"number"},{"word":"dundo","meaning":"200","type":"number"},{"word":"tendo","meaning":"300","type":"number"},{"word":"kando","meaning":"400","type":"number"},{"word":"bindo","meaning":"500","type":"number"},{"word":"cindo","meaning":"600","type":"number"},{"word":"sendo","meaning":"700","type":"number"},{"word":"wondo","meaning":"800","type":"number"},{"word":"nindo","meaning":"900","type":"number"},{"word":"**neko**","meaning":"1000","type":"number"},{"word":"duko","meaning":"2000","type":"number"},{"word":"teko","meaning":"3000","type":"number"},{"word":"keko","meaning":"4000","type":"number"},{"word":"biko","meaning":"5000","type":"number"},{"word":"ciko","meaning":"6000","type":"number"},{"word":"seko","meaning":"7000","type":"number"},{"word":"woko","meaning":"8000","type":"number"},{"word":"niko","meaning":"9000","type":"number"},{"word":"diko","meaning":"10'000","type":"number"},{"word":"dudiko","meaning":"20'000","type":"number"},{"word":"tediko","meaning":"30'000","type":"number"},{"word":"kadiko","meaning":"40'000","type":"number"},{"word":"bidiko","meaning":"50'000","type":"number"},{"word":"cidiko","meaning":"60'000","type":"number"},{"word":"sediko","meaning":"70'000","type":"number"},{"word":"wodiko","meaning":"80'000","type":"number"},{"word":"nidiko","meaning":"90'000","type":"number"},{"word":"huko","meaning":"100'000","type":"number"},{"word":"duhuko","meaning":"200'000","type":"number"},{"word":"tehuko","meaning":"300'000","type":"number"},{"word":"kahuko","meaning":"400'000","type":"number"},{"word":"bihuko","meaning":"500'000","type":"number"},{"word":"cihuko","meaning":"600'000","type":"number"},{"word":"sehuko","meaning":"700'000","type":"number"},{"word":"wohuko","meaning":"800'000","type":"number"},{"word":"nihuko","meaning":"900'000","type":"number"},{"word":"**ranolono**","meaning":"1'000'000","type":"number"},{"word":"dunolono","meaning":"2'000'000","type":"number"},{"word":"tenolono","meaning":"3'000'000","type":"number"},{"word":"kenolono","meaning":"4'000'000","type":"number"},{"word":"binolono","meaning":"5'000'000","type":"number"},{"word":"cinolono","meaning":"6'000'000","type":"number"},{"word":"senolono","meaning":"7'000'000","type":"number"},{"word":"wonolono","meaning":"8'000'000","type":"number"},{"word":"ninolono","meaning":"9'000'000","type":"number"},{"word":"dinolono","meaning":"10'000'000","type":"number"},{"word":"dudinolono","meaning":"20'000'000","type":"number"},{"word":"tedinolono","meaning":"30'000'000","type":"number"},{"word":"kadinolono","meaning":"40'000'000","type":"number"},{"word":"bidinolono","meaning":"50'000'000","type":"number"},{"word":"cidinolono","meaning":"60'000'000","type":"number"},{"word":"sedinolono","meaning":"70'000'000","type":"number"},{"word":"wodinolono","meaning":"80'000'000","type":"number"},{"word":"nidinolono","meaning":"90'000'000","type":"number"},{"word":"hunolono","meaning":"100'000'000","type":"number"},{"word":"**rawolono**","meaning":"1'000'000'000","type":"number"},{"word":"diwolono","meaning":"10'000'000'000","type":"number"},{"word":"huwolono","meaning":"100'000'000'000","type":"number"},{"word":"**rerolono**","meaning":"1'000'000'000'000","type":"number"},{"word":"dirolono","meaning":"10'000'000'000'000","type":"number"},{"word":"hurolono","meaning":"100'000'000'000'000","type":"number"},{"word":"**ragolono**","meaning":"1'000'000'000'000'000","type":"number"},{"word":"digolono","meaning":"10'000'000'000'000'000","type":"number"},{"word":"hugolono","meaning":"100'000'000'000'000'000","type":"number"},{"word":"**rimolono**","meaning":"1'000'000'000'000'000'000","type":"number"},{"word":"dimolono","meaning":"10'000'000'000'000'000'000","type":"number"},{"word":"humolono","meaning":"100'000'000'000'000'000'000","type":"number"},{"word":"**rikolono**","meaning":"1'000'000'000'000'000'000'000","type":"number"},{"word":"dikolono","meaning":"10'000'000'000'000'000'000'000","type":"number"},{"word":"hukolono","meaning":"100'000'000'000'000'000'000'000","type":"number"},{"word":"**rebolono**","meaning":"1'000'000'000'000'000'000'000'000","type":"number"},{"word":"dibolono","meaning":"10'000'000'000'000'000'000'000'000","type":"number"},{"word":"hubolono","meaning":"100'000'000'000'000'000'000'000'000","type":"number"},{"word":"**rontolono**","meaning":"1'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"dintolono","meaning":"10'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"huntolono","meaning":"100'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"**rinolono**","meaning":"1'000'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"dimpolono","meaning":"10'000'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"humpolono","meaning":"100'000'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"**rinkolono**","meaning":"1'000'000'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"dinkolono","meaning":"100'000'000'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"hunkolono","meaning":"100'000'000'000'000'000'000'000'000'000'000'000","type":"number"},{"word":"mire-","meaning":"half, in the middle of","type":"prefix"},{"word":"gara-","meaning":"all/every","type":"prefix"},{"word":"coko-","meaning":"some","type":"prefix"},{"word":"boro-","meaning":"any","type":"prefix"},{"word":"ni-","meaning":"negation, other than","type":"prefix"},{"word":"mo-","meaning":"inversion, contrary meaning","type":"prefix"},{"word":"wu-","meaning":"denotes positive connotation","type":"prefix"},{"word":"di-","meaning":"denotes negative connotation","type":"prefix"},{"word":"ra","meaning":"in","type":"preposition"},{"word":"para","meaning":"for","type":"preposition"},{"word":"de","meaning":"of","type":"preposition"},{"word":"no","meaning":"but","type":"preposition"},{"word":"gon","meaning":"on","type":"preposition"},{"word":"huba","meaning":"over","type":"preposition"},{"word":"wuba","meaning":"under","type":"preposition"},{"word":"faba","meaning":"in front of","type":"preposition"},{"word":"caba","meaning":"behind","type":"preposition"},{"word":"tamba","meaning":"next to","type":"preposition"},{"word":"taba","meaning":"near","type":"preposition"},{"word":"kaba","meaning":"far","type":"preposition"},{"word":"—","meaning":"li","type":"pronoun"},{"word":"mid","meaning":"mida","type":"pronoun"},{"word":"—","meaning":"da","type":"pronoun"},{"word":"—","meaning":"dase","type":"pronoun"},{"word":"—","meaning":"dake","type":"pronoun"},{"word":"—","meaning":"daka","type":"pronoun"},{"word":"—","meaning":"daki","type":"pronoun"},{"word":"—","meaning":"daci","type":"pronoun"},{"word":"reda","meaning":"reflexive pronoun, referencing to the thing(s) already mentioned in the sentence","type":"pronoun"},{"word":"toransu","meaning":"other","type":"pronoun"},{"word":"gara","meaning":"all/every","type":"pronoun"},{"word":"coko","meaning":"some","type":"pronoun"},{"word":"boro","meaning":"any","type":"pronoun"},{"word":"mire","meaning":"half","type":"pronoun"},{"word":"Prefix for:","meaning":"—","type":"pronoun"},{"word":"-lu","meaning":"defines an agent noun","type":"suffix"},{"word":"-sa","meaning":"indicates plurality","type":"suffix"},{"word":"-ri","meaning":"establishes a place/event","type":"suffix"},{"word":"-dufen","meaning":"refers to an entity or construct that performs a specific action or function","type":"suffix"},{"word":"-fe","meaning":"connotes opposition","type":"suffix"},{"word":"-se","meaning":"establishes feminine characteristics/gender","type":"suffix"},{"word":"-ke","meaning":"establishes androfeminine characteristics/gender","type":"suffix"},{"word":"-ka","meaning":"establishes androgynous characteristics/gender","type":"suffix"},{"word":"-ki","meaning":"establishes andromasc characteristics/gender","type":"suffix"},{"word":"-ci","meaning":"establishes masculine characteristics/gender","type":"suffix"},{"word":"-pa","meaning":"marks word as possessive","type":"suffix"},{"word":"-lonki","meaning":"refers to a field of knowledge/study","type":"suffix"},{"word":"-falo","meaning":"indicates manner or characteristic of speech ","type":"suffix"},{"word":"-da","meaning":"refers to the past tense form of a verb","type":"suffix"},{"word":"-ne","meaning":"refers to the present tense form of a verb","type":"suffix"},{"word":"-lo","meaning":"refers to the future tense form of a verb","type":"suffix"},{"word":"-ku","meaning":"establishes verb as uncertain/questioning","type":"suffix"},{"word":"-do","meaning":"establishes verb as imperative","type":"suffix"},{"word":"-jala","meaning":"denotes ability or capacity","type":"suffix"},{"word":"mawa","meaning":"kiss","type":"verb"},{"word":"binta","meaning":"eat","type":"verb"},{"word":"sanu","meaning":"be","type":"verb"},{"word":"tenco","meaning":"have","type":"verb"},{"word":"somo","meaning":"sleep","type":"verb"},{"word":"cosi","meaning":"choose, vote","type":"verb"},{"word":"falo","meaning":"speak, say, tell","type":"verb"},{"word":"toransu","meaning":"differ","type":"verb"},{"word":"mahen","meaning":"make, create","type":"verb"},{"word":"sekiso","meaning":"have sex, fuck","type":"verb"},{"word":"deja","meaning":"do, act","type":"verb"},{"word":"gade","meaning":"guess","type":"verb"},{"word":"nem","meaning":"think","type":"verb"},{"word":"pen","meaning":"move","type":"verb"},{"word":"nepen","meaning":"feel","type":"verb"},{"word":"gurati","meaning":"induce negative freedom, reduce cost to none","type":"verb"},{"word":"lifeta","meaning":"induce positive freedom","type":"verb"},{"word":"jala","meaning":"can, be able to","type":"verb"},{"word":"meta","meaning":"measure","type":"verb"},{"word":"core","meaning":"want","type":"verb"},{"word":"jore","meaning":"need","type":"verb"}]
20 changes: 20 additions & 0 deletions src/lib/kumilinwa/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FullEntry } from './types';
import { CompleteLangSpec } from './import';

export async function searchLangSpec(
query: string,
matching?: MatchType
): Promise<FullEntry[]> {
const matchWord = matching === 'word' || !matching,
matchMeaning = matching === 'meaning' || !matching;
const results: FullEntry[] = [];
for (const entry of CompleteLangSpec)
if (
(matchWord && entry.word.includes(query)) ||
(matchMeaning && entry.meaning.includes(query))
)
results.push(entry);
return results;
}

export type MatchType = 'word' | 'meaning';
31 changes: 31 additions & 0 deletions src/lib/kumilinwa/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* The type of the entry in each separated file (i.e. verbs.json)
*/
export interface Entry {
word: string;
meaning: string;
}

/**
* The type of entry in the complete dictionary (0-complete.json)
* @extends Entry Also contains the properties from the Entry type
*/
export interface FullEntry extends Entry {
type: WordType;
}

/**
* The types of words there can be
*/
export type WordType =
| 'adjective'
| 'adverb'
| 'conjunction'
| 'interjection'
| 'noun'
| 'number'
| 'prefix'
| 'preposition'
| 'pronoun'
| 'suffix'
| 'verb';
8 changes: 8 additions & 0 deletions src/lib/misc/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { pino } from 'pino';
import pretty from 'pino-pretty';

export const logger = pino(
pretty({
colorize: true
})
);
33 changes: 33 additions & 0 deletions src/lib/misc/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import express, { Request, Response } from 'express';
import helmet from 'helmet';

export enum Methods {
DELETE = 'delete',
GET = 'get',
HEAD = 'head',
PATCH = 'patch',
POST = 'post',
PUT = 'put'
}

interface Route {
handler: (req: Request, res: Response) => void;
method: Methods;
route: string;
}

export function createServer(...routes: Route[]) {
const app = express();
for (const { handler, method, route } of routes) {
app[method](route, handler);
}
// cors
app.use(
helmet({
crossOriginResourcePolicy: {
policy: 'same-site'
}
})
);
return app;
}
99 changes: 99 additions & 0 deletions src/lib/struct/CommandHelpEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
APIEmbedField,
RestOrArray,
inlineCode,
normalizeArray
} from 'discord.js';

/**
* Represents a command entry in the help command
* @class
*/
export class CommandHelpEntry {
description: string;
name: string;
_usage: string[] | undefined;

/**
* Creates a new command entry
* @constructor
* @param {string} name the name of the command
* @param {string} description the description of the command
* @param {string[]} usage the usage of the command
*/
constructor(
name: string,
description: string,
...usage: RestOrArray<string>
) {
/**
* The name of the command
* @type {string}
* @private
* @readonly
*/
this.name = name;
/**
* The description of the command
* @type {string}
* @public
* @readonly
*/
this.description = description;
/**
* The usage of the command
* @type {string[]}
* @private
* @readonly
*/
this._usage = normalizeArray(usage);
}

/**
* The usage of the command
* @type {string[]}
* @readonly
*/
get usage(): string[] {
if (!this._usage) return [inlineCode(`/${this.name}`)];
return this._usage.map(val => inlineCode(`/${this.name} ${val}`));
}

/**
* Converts the command entry to a Discord API embed field
* @returns {APIEmbedField}
*/
toDiscordAPIEmbedField(): APIEmbedField {
return {
name: this.name,
value: `${this.description}\n${this.usage.join('\n')}`
};
}

/**
* Converts the command entry to a JSON object
* @returns {SerializedCommandHelpEntry}
*/
toJSON(): SerializedCommandHelpEntry {
return {
description: this.description,
name: this.name,
usage: this._usage
};
}

/**
* Creates a new command entry from a JSON object
* @param json {{description: string, name: string, usage?: string[]}
* @returns {CommandHelpEntry}
*/
static fromJSON(json: SerializedCommandHelpEntry): CommandHelpEntry {
return new CommandHelpEntry(json.name, json.description, json.usage ?? []);
}
}

interface SerializedCommandHelpEntry {
description: string;
name: string;
usage?: string[];
}
24 changes: 24 additions & 0 deletions src/lib/struct/discord/Extend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Client, ClientOptions } from 'discord.js';
import { Collection, ReadonlyCollection } from '@discordjs/collection';
import { Command } from './types';

/**
export
*/ class ExtendedCollection<K, V> extends Collection<K, V> {
constructor(entries?: readonly (readonly [K, V])[] | null) {
super(entries);
}
public freeze(): ReadonlyCollection<K, V> {
return Object.freeze(this);
}
}

export class CommandClient extends Client {
commands: ExtendedCollection<string, Command>;

constructor(options: ClientOptions) {
super(options);

this.commands = new ExtendedCollection<string, Command>();
}
}
14 changes: 14 additions & 0 deletions src/lib/struct/discord/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
import { CommandHelpEntry } from '../CommandHelpEntry';

export interface Event {
name: string;
once: boolean;
execute: (...args: unknown[]) => Promise<void>;
}

export interface Command {
data: SlashCommandBuilder;
help?: CommandHelpEntry;
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
}
21 changes: 21 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"baseUrl": "./",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"target": "ESNext",
"lib": ["ESNext", "ES2022", "ES5", "ES6"],
"resolveJsonModule": true,
"isolatedModules": true,
"alwaysStrict": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["./src/**/*", "./scripts/**/*", "./*.ts"],
"exclude": ["node_modules"]
}

0 comments on commit b5118dd

Please sign in to comment.