commit e4c0d7a901139d4ce90be3b579683b94438549ba Author: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Mon Dec 9 00:59:16 2024 +0100 feat: initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0d6b50f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.env +.git +.sern +db \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a942cbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/dist +.env +.sern/ +/db \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5324000 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "useTabs": false, + "printWidth": 800, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "es5", + "semi": true +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cc9d4f3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.tabSize": 2, + "editor.detectIndentation": false, + "editor.insertSpaces": true, +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..caf4cd6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM oven/bun:1-alpine + +WORKDIR /app + +COPY . . + +RUN bun install + +RUN bun run docker:build + +CMD ["bun", "start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..872c0ab --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# How to use + +1.) Build +``` +npm run build +``` +2.) Run +``` +node . +``` +3.) Publish +``` +npm run commands:publish +``` + diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..102c9e9 Binary files /dev/null and b/bun.lockb differ diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..50517ba --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'sqlite', // 'mysql' | 'sqlite' | 'turso' + dbCredentials: { + url: './db/sqlite.db', + }, + schema: './src/db/schema.ts', + out: './src/db/migrations', +}); diff --git a/drizzle/0000_overconfident_rattler.sql b/drizzle/0000_overconfident_rattler.sql new file mode 100644 index 0000000..5494d16 --- /dev/null +++ b/drizzle/0000_overconfident_rattler.sql @@ -0,0 +1,6 @@ +CREATE TABLE `birthday` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `date` text NOT NULL, + `userId` text NOT NULL +); diff --git a/drizzle/0001_steep_liz_osborn.sql b/drizzle/0001_steep_liz_osborn.sql new file mode 100644 index 0000000..9eba625 --- /dev/null +++ b/drizzle/0001_steep_liz_osborn.sql @@ -0,0 +1 @@ +ALTER TABLE `birthday` RENAME COLUMN "name" TO "authorId"; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..70e799d --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,56 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "27b25008-8dd3-4ef6-992b-8586dd7fb4eb", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "birthday": { + "name": "birthday", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..d07f605 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,58 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "afeb51df-0d38-42d3-a4f4-2f33045ecf7e", + "prevId": "27b25008-8dd3-4ef6-992b-8586dd7fb4eb", + "tables": { + "birthday": { + "name": "birthday", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorId": { + "name": "authorId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": { + "\"birthday\".\"name\"": "\"birthday\".\"authorId\"" + } + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..dc5fb7e --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1733584433042, + "tag": "0000_overconfident_rattler", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1733585283432, + "tag": "0001_steep_liz_osborn", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..fd7817c --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "ezbd", + "version": "1.0.0", + "private": true, + "description": "", + "main": "dist/index.js", + "scripts": { + "build": "sern build && bun copy:json", + "docker:build": "mkdir -p db/kv && bun run build && bun db:migrate", + "copy:json": "copyfiles -u 1 src/**/*.json dist/", + "start": "bun run ./src/index.ts", + "dev": "bun run build && bun run ./src/index.ts", + "install": "sern build", + "commands:publish": "sern commands publish", + "db:generate": "bun drizzle-kit generate --dialect sqlite --schema ./src/db/schema.ts", + "db:migrate": "bun run ./src/db/migrate.ts", + "db:all": "bun db:generate && bun db:migrate" + }, + "keywords": [ + "typescript", + "sern", + "discord.js" + ], + "dependencies": { + "@sern/cli": "^1.3.3", + "@sern/handler": "^4.0.0", + "@sern/publisher": "^1.1.1", + "discord.js": "latest", + "dotenv": "^16.3.1", + "drizzle-orm": "^0.37.0", + "unstorage": "^1.13.1" + }, + "devDependencies": { + "@types/bun": "^1.1.14", + "@types/node": "^17.0.25", + "copyfiles": "^2.4.1", + "drizzle-kit": "^0.29.1", + "typescript": "^5.0" + }, + "type": "module", + "trustedDependencies": [ + "@parcel/watcher" + ] +} diff --git a/sern.config.json b/sern.config.json new file mode 100644 index 0000000..267655b --- /dev/null +++ b/sern.config.json @@ -0,0 +1,7 @@ +{ + "language": "typescript", + "paths": { + "base": "src", + "commands": "commands" + } +} \ No newline at end of file diff --git a/src/commands/add.ts b/src/commands/add.ts new file mode 100644 index 0000000..edb46c2 --- /dev/null +++ b/src/commands/add.ts @@ -0,0 +1,70 @@ +import { commandModule, CommandType } from '@sern/handler'; +import { publishConfig } from '@sern/publisher'; +import { ApplicationCommandOptionType } from 'discord.js'; +import { birthdayTable } from '../db/schema'; +import getDates from '../util/getDates'; +import { and, eq } from 'drizzle-orm'; + +export default commandModule({ + type: CommandType.Slash, + plugins: [ + publishConfig({ + contexts: [0, 1, 2], + integrationTypes: ['Guild', 'User'], + }), + ], + description: 'Add a user to your database', + options: [ + { + name: 'user', + type: ApplicationCommandOptionType.User, + description: 'The user you want to add', + required: true, + }, + { + name: 'birthday', + type: ApplicationCommandOptionType.String, + description: 'Day and month', + required: true, + autocomplete: true, + command: { + onEvent: [], + execute: async (ctx) => { + const autocomplete = ctx.options.getFocused(); + const choices = getDates() + .filter((d) => d.startsWith(autocomplete)) + .slice(0, 25); + ctx.respond(choices.map((c) => ({ name: c, value: c }))); + }, + }, + }, + ], + //alias : [], + execute: async (ctx, args) => { + try { + const db = args.deps['drizzle']; + const user = ctx.interaction.options.getUser('user', true); + const birthday = ctx.interaction.options.getString('birthday', true); + + if (!getDates().includes(birthday)) { + ctx.reply({ content: 'Invalid date. Please choose a correct one from the autocomplete!', ephemeral: true }); + return; + } + if ( + ( + await db + .select() + .from(birthdayTable) + .where(and(eq(birthdayTable.authorId, ctx.userId), eq(birthdayTable.userId, user.id))) + ).length > 0 + ) { + return ctx.reply({ content: 'User already exists!', ephemeral: true }); + } + + await db.insert(birthdayTable).values({ authorId: ctx.userId, userId: user.id, date: birthday }).execute(); + ctx.reply({ content: `Added ${user.username} to the database. Date will be ${birthday}`, ephemeral: true }); + } catch (e) { + console.error(e); + } + }, +}); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..2a95529 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,13 @@ +//CONFIG FILE: export only data here and do not cause side effects. Feel free to add your own configuration to this file. + +//commands directory. REQUIRED +export const commands = './dist/commands' +// events directory. +// export const events = './dist/events' + +// schedule tasks and declare them here +export const tasks = './dist/tasks' + +// defaultPrefix: if omitted, sern will disable all text/prefix commands +// export const defaultPrefix = '?' + diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..e9497d8 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,8 @@ +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { Database } from 'bun:sqlite'; +import * as schema from './schema' + +const client = new Database('./db/sqlite.db'); +const db = drizzle({ client, schema }); + +export { db }; \ No newline at end of file diff --git a/src/db/kv.ts b/src/db/kv.ts new file mode 100644 index 0000000..f0cd98d --- /dev/null +++ b/src/db/kv.ts @@ -0,0 +1,6 @@ +import { createStorage } from "unstorage"; +import fsDriver from "unstorage/drivers/fs"; + +export const kv = createStorage({ + driver: fsDriver({ base: "./db/kv" }), +}); \ No newline at end of file diff --git a/src/db/migrate.ts b/src/db/migrate.ts new file mode 100644 index 0000000..9700654 --- /dev/null +++ b/src/db/migrate.ts @@ -0,0 +1,8 @@ +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; + +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { Database } from "bun:sqlite"; + +const sqlite = new Database("./db/sqlite.db"); +const db = drizzle(sqlite); +await migrate(db, { migrationsFolder: "./drizzle" }); \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..7bc1a4b --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,8 @@ +import { int, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const birthdayTable = sqliteTable("birthday", { + id: int().primaryKey({ autoIncrement: true }), + userId: text().notNull(), + date: text().notNull(), + authorId: text().notNull(), +}); diff --git a/src/dependencies.d.ts b/src/dependencies.d.ts new file mode 100644 index 0000000..1ed9ff8 --- /dev/null +++ b/src/dependencies.d.ts @@ -0,0 +1,25 @@ +/** + * This file serves as intellisense for sern projects. + * Types are declared here for dependencies to function properly + * Service(s) api rely on this file to provide a better developer experience. + */ + +import type { CoreDependencies } from '@sern/handler'; +import type { Client } from 'discord.js'; +import type { Publisher } from '@sern/publisher'; +import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite'; +import type { Storage, StorageValue } from 'unstorage'; +/** + * Note: You usually would not need to modify this unless there is an urgent need to break the contracts provided. + * You would need to modify this to add your custom Services, however. + */ +declare global { + interface Dependencies extends CoreDependencies { + '@sern/client': Client; + publisher: Publisher; + 'drizzle': BunSQLiteDatabase; + 'cache': Storage; + } +} + +export {}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..122cc91 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,31 @@ +import 'dotenv/config'; +import * as config from './config.js'; +import { Client, GatewayIntentBits } from 'discord.js'; +import { Sern, makeDependencies } from '@sern/handler'; +import { Publisher } from '@sern/publisher'; +import { db } from './db/index.js'; +import { kv } from './db/kv.js'; +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + ], +}); + +/** + * Where all of your dependencies are composed. + * '@sern/client' is usually your Discord Client. + * Use this function to access all of your dependencies. + * This is used for external event modules as well + */ +await makeDependencies(({ add }) => { + add('@sern/client', client); + add('publisher', (deps) => new Publisher(deps['@sern/modules'], deps['@sern/emitter'], deps['@sern/logger']!)); + add('drizzle', db); + add('cache', kv); +}); + +//View docs for all options +Sern.init(config); + +await client.login(); diff --git a/src/tasks/bdDm.ts b/src/tasks/bdDm.ts new file mode 100644 index 0000000..2c00f11 --- /dev/null +++ b/src/tasks/bdDm.ts @@ -0,0 +1,35 @@ +import { scheduledTask } from '@sern/handler'; +import { birthdayTable } from '../db/schema'; +import currentDate from '../util/currentDate'; +import { eq } from 'drizzle-orm'; + +// TODO: optimize +export default scheduledTask({ + // on production, run every 2 hours + // on development, run every minute + trigger: process.env.NODE_ENV === 'production' ? '0 */2 * * *' : '* * * * *', + async execute(ctx, sdt) { + const client = sdt.deps['@sern/client']; + const db = sdt.deps['drizzle']; + const kv = sdt.deps['cache']; + const getAllSent = await kv.getKeys(`sent`); + for (const sent of getAllSent) { + const get = (await kv.get(sent))?.toString(); + if (get !== currentDate()) { + await kv.del(sent); + } + } + + const bdList = await db.select().from(birthdayTable).where(eq(birthdayTable.date, currentDate())); + for (const bd of bdList) { + const kvKey = `sent:${bd.authorId}:${bd.userId}`; + if (await kv.hasItem(kvKey)) continue; + + const author = await client.users.fetch(bd.authorId); + const user = await client.users.fetch(bd.userId); + + await kv.set(`sent:${bd.authorId}:${bd.userId}`, currentDate()); + await author.send(`Today is ${user.username}'s birthday! (<@${user.id}>)`); + } + }, +}); diff --git a/src/util/currentDate.ts b/src/util/currentDate.ts new file mode 100644 index 0000000..2487c33 --- /dev/null +++ b/src/util/currentDate.ts @@ -0,0 +1,4 @@ +export default function currentDate() { + const date = new Date(); + return `${date.getDate()}-${date.getMonth() + 1}`; +} \ No newline at end of file diff --git a/src/util/dates.json b/src/util/dates.json new file mode 100644 index 0000000..d0f17c6 --- /dev/null +++ b/src/util/dates.json @@ -0,0 +1 @@ +["1-1","2-1","3-1","4-1","5-1","6-1","7-1","8-1","9-1","10-1","11-1","12-1","13-1","14-1","15-1","16-1","17-1","18-1","19-1","20-1","21-1","22-1","23-1","24-1","25-1","26-1","27-1","28-1","29-1","30-1","31-1","1-2","2-2","3-2","4-2","5-2","6-2","7-2","8-2","9-2","10-2","11-2","12-2","13-2","14-2","15-2","16-2","17-2","18-2","19-2","20-2","21-2","22-2","23-2","24-2","25-2","26-2","27-2","28-2","1-3","2-3","3-3","4-3","5-3","6-3","7-3","8-3","9-3","10-3","11-3","12-3","13-3","14-3","15-3","16-3","17-3","18-3","19-3","20-3","21-3","22-3","23-3","24-3","25-3","26-3","27-3","28-3","29-3","30-3","31-3","1-4","2-4","3-4","4-4","5-4","6-4","7-4","8-4","9-4","10-4","11-4","12-4","13-4","14-4","15-4","16-4","17-4","18-4","19-4","20-4","21-4","22-4","23-4","24-4","25-4","26-4","27-4","28-4","29-4","30-4","1-5","2-5","3-5","4-5","5-5","6-5","7-5","8-5","9-5","10-5","11-5","12-5","13-5","14-5","15-5","16-5","17-5","18-5","19-5","20-5","21-5","22-5","23-5","24-5","25-5","26-5","27-5","28-5","29-5","30-5","31-5","1-6","2-6","3-6","4-6","5-6","6-6","7-6","8-6","9-6","10-6","11-6","12-6","13-6","14-6","15-6","16-6","17-6","18-6","19-6","20-6","21-6","22-6","23-6","24-6","25-6","26-6","27-6","28-6","29-6","30-6","1-7","2-7","3-7","4-7","5-7","6-7","7-7","8-7","9-7","10-7","11-7","12-7","13-7","14-7","15-7","16-7","17-7","18-7","19-7","20-7","21-7","22-7","23-7","24-7","25-7","26-7","27-7","28-7","29-7","30-7","31-7","1-8","2-8","3-8","4-8","5-8","6-8","7-8","8-8","9-8","10-8","11-8","12-8","13-8","14-8","15-8","16-8","17-8","18-8","19-8","20-8","21-8","22-8","23-8","24-8","25-8","26-8","27-8","28-8","29-8","30-8","31-8","1-9","2-9","3-9","4-9","5-9","6-9","7-9","8-9","9-9","10-9","11-9","12-9","13-9","14-9","15-9","16-9","17-9","18-9","19-9","20-9","21-9","22-9","23-9","24-9","25-9","26-9","27-9","28-9","29-9","30-9","1-10","2-10","3-10","4-10","5-10","6-10","7-10","8-10","9-10","10-10","11-10","12-10","13-10","14-10","15-10","16-10","17-10","18-10","19-10","20-10","21-10","22-10","23-10","24-10","25-10","26-10","27-10","28-10","29-10","30-10","31-10","1-11","2-11","3-11","4-11","5-11","6-11","7-11","8-11","9-11","10-11","11-11","12-11","13-11","14-11","15-11","16-11","17-11","18-11","19-11","20-11","21-11","22-11","23-11","24-11","25-11","26-11","27-11","28-11","29-11","30-11","1-12","2-12","3-12","4-12","5-12","6-12","7-12","8-12","9-12","10-12","11-12","12-12","13-12","14-12","15-12","16-12","17-12","18-12","19-12","20-12","21-12","22-12","23-12","24-12","25-12","26-12","27-12","28-12","29-12","30-12","31-12"] \ No newline at end of file diff --git a/src/util/getDates.ts b/src/util/getDates.ts new file mode 100644 index 0000000..7b2a583 --- /dev/null +++ b/src/util/getDates.ts @@ -0,0 +1,12 @@ +import Dates from './dates.json'; + +export default function getDates(isAmerican = false) { + if (isAmerican) { + return Dates.map(d => { + const [day, month] = d.split('-'); + return `${month}-${day}`; + }) + } else { + return Dates; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5690f3a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./.sern/tsconfig.json", + "compilerOptions": { + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + }, + "include": ["src/**/*.json"], +}