From 2d5be66c487d64799fb13b021bfa8e0bc382876c Mon Sep 17 00:00:00 2001 From: encode42 Date: Tue, 30 Apr 2024 04:56:05 -0400 Subject: [PATCH] Initial commit --- .env | 5 + .github/workflows/build.yml | 30 ++++++ .gitignore | 182 ++++++++++++++++++++++++++++++++ biome.json | 59 +++++++++++ build.ts | 55 ++++++++++ bun.lockb | Bin 0 -> 29340 bytes license.md | 21 ++++ package.json | 17 +++ src/database.ts | 92 ++++++++++++++++ src/discord/client.ts | 17 +++ src/discord/events/onMessage.ts | 68 ++++++++++++ src/discord/events/register.ts | 23 ++++ src/index.ts | 3 + src/log.ts | 11 ++ src/util/env.ts | 17 +++ tsconfig.json | 17 +++ 16 files changed, 617 insertions(+) create mode 100644 .env create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 biome.json create mode 100644 build.ts create mode 100755 bun.lockb create mode 100644 license.md create mode 100644 package.json create mode 100644 src/database.ts create mode 100644 src/discord/client.ts create mode 100644 src/discord/events/onMessage.ts create mode 100644 src/discord/events/register.ts create mode 100644 src/index.ts create mode 100644 src/log.ts create mode 100644 src/util/env.ts create mode 100644 tsconfig.json diff --git a/.env b/.env new file mode 100644 index 0000000..fb60787 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +DISCORD_TOKEN= +VERIFIED_ROLE= + +MINIMUM_MESSAGES=30 +MESSAGE_COOLDOWN=3 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c55f547 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Build + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + run: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Build + run: | + bun install --production --frozen-lockfile + bun build.ts + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: Binaries + path: build/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a991b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,182 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +# Runtime files +!/.env +/db.sqlite* + +# Compiled binaries +/build \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..0c90abd --- /dev/null +++ b/biome.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": true, + "ignore": ["./public", "./build", "./dist"], + "rules": { + "recommended": true, + "complexity": { + "useLiteralKeys": "off" + }, + "performance": { + "noDelete": "off" + }, + "style": { + "useBlockStatements": "error", + "useNamingConvention": { + "level": "error", + "options": { + "strictCase": false + } + }, + "useShorthandArrayType": "error", + "noImplicitBoolean": "error", + "noNegationElse": "error", + "useCollapsedElseIf": "error", + "useFilenamingConvention": { + "level": "error", + "options": { + "requireAscii": true, + "filenameCases": ["camelCase", "PascalCase"] + } + } + }, + "suspicious": { + "noConsoleLog": "warn" + }, + "correctness": { + "noUnusedImports": "error" + } + } + }, + "formatter": { + "enabled": true, + "ignore": ["./public", "./build", "./dist"], + "indentStyle": "tab" + }, + "javascript": { + "formatter": { + "quoteProperties": "preserve", + "trailingComma": "none", + "lineWidth": 192, + "indentStyle": "tab", + "bracketSameLine": true + } + } +} diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..3ca89a8 --- /dev/null +++ b/build.ts @@ -0,0 +1,55 @@ +import { $ } from "bun"; +import { log } from "./src/log"; +import meta from "./package.json"; + +interface BuildOptions { + "fileName": string; + "target": string; + "arch"?: string; +} + +const targets = { + "linux": { + "label": "Linux", + "arm": true + }, + "darwin": { + "label": "macOS", + "arm": true + }, + "windows": { + "label": "Windows", + "arm": false + } +}; + +async function build({ fileName, target, arch = "x64-modern" }: BuildOptions) { + if (arch === "arm64") { + fileName += "-arm"; + } + + const result = await $`bun build --compile --minify --sourcemap src/index.ts --target=bun-${target}-${arch} --outfile build/${fileName}`.text(); + log.debug(result); +} + +for (const [target, value] of Object.entries(targets)) { + log.info(`Building for ${value.label}...`); + + const options: BuildOptions = { + "fileName": `${meta.name}-${value.label.toLowerCase()}`, + "target": target + }; + + await build(options); + + if (value.arm) { + await build({ + ...options, + "arch": "arm64" + }); + + log.info("ARM binary built!"); + } + + log.info(`Finished building for ${value.label}!`); +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..52627423d5493338c3ce06fbc1eb8ca8c5888cf8 GIT binary patch literal 29340 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 literal 0 HcmV?d00001 diff --git a/license.md b/license.md new file mode 100644 index 0000000..992fa07 --- /dev/null +++ b/license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 encode42 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..2120e27 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "ohm", + "module": "src/index.ts", + "type": "module", + "devDependencies": { + "@biomejs/biome": "^1.7.1", + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "discord.js": "^14.14.1", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0" + } +} diff --git a/src/database.ts b/src/database.ts new file mode 100644 index 0000000..30feeac --- /dev/null +++ b/src/database.ts @@ -0,0 +1,92 @@ +import { Database } from "bun:sqlite"; +import { log } from "./log"; +import { number } from "./util/env"; + +interface User { + "id": number; + // biome-ignore lint/style/useNamingConvention: SQLite convention is in snake_case + "message_count": number; + // biome-ignore lint/style/useNamingConvention: SQLite convention is in snake_case + "last_seen": number; + "verified": boolean; +} + +const messageCooldown = number("MESSAGE_COOLDOWN"); + +const database = new Database("db.sqlite", { + "create": true +}); + +database.exec("PRAGMA journal_mode = WAL;"); +database + .query(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + message_count INTEGER DEFAULT 1, + last_seen INTEGER DEFAULT 0, + verified BOOLEAN DEFAULT FALSE + ); + `) + .run(); + +const query = { + "select": database.query("SELECT $id, message_count, last_seen, verified FROM users;"), + "update": database.query("UPDATE users SET last_seen = unixepoch(CURRENT_TIMESTAMP), message_count = $count WHERE ID = $id;"), + "verify": database.query("UPDATE users SET verified = true WHERE ID = $id;"), + "create": database.query(` + INSERT INTO users (id) + VALUES ($id); + `) +}; + +export function getUser(id: string) { + const result = query.select.get({ + "$id": id + }); + + return result as User | undefined; +} + +export function seeUser(id: string) { + const user = getUser(id); + + if (!user) { + query.create.run({ + "$id": id + }); + + return { + "count": 1, + "verified": false + }; + } + + if (user.verified) { + return { + "count": user.message_count, + "verified": true + }; + } + + let count = user.message_count; + if (Date.now() / 1000 - user.last_seen > messageCooldown) { + log.debug("Message cooldown satisfied, incrementing count..."); + count++; + } + + query.update.run({ + "$id": id, + "$count": count + }); + + return { + "count": count, + "verified": false + }; +} + +export function verifyUser(id: string) { + query.verify.run({ + "$id": id + }); +} diff --git a/src/discord/client.ts b/src/discord/client.ts new file mode 100644 index 0000000..dfdd961 --- /dev/null +++ b/src/discord/client.ts @@ -0,0 +1,17 @@ +import { Client, GatewayIntentBits } from "discord.js"; +import { log } from "../log"; +import { string } from "../util/env"; + +log.info("Creating Discord client..."); + +export const client = new Client({ + "intents": [GatewayIntentBits.MessageContent, GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] +}); + +const success = !!(await client.login(string("DISCORD_TOKEN"))); + +if (success) { + log.info("Logged in!"); +} else { + throw "Client unable to log in."; +} diff --git a/src/discord/events/onMessage.ts b/src/discord/events/onMessage.ts new file mode 100644 index 0000000..2d644db --- /dev/null +++ b/src/discord/events/onMessage.ts @@ -0,0 +1,68 @@ +import type { Role } from "discord.js"; +import { Events } from "discord.js"; +import { client } from "../client"; +import { seeUser, verifyUser } from "../../database"; +import { number, string } from "../../util/env"; +import { log } from "../../log"; + +const roleId = string("VERIFIED_ROLE"); +const minimumMessages = number("MINIMUM_MESSAGES"); + +let roleValid = false; +for (const [_, guild] of client.guilds.cache) { + const roles = await guild.roles.fetch(); + + let verifiedRole: Role | undefined; + let selfRole: Role | undefined; + for (const [_, role] of roles) { + if (role.id === roleId) { + verifiedRole = role; + continue; + } + + if (role.name === "Ohm" && role.managed) { + selfRole = role; + } + } + + log.debug(`Verified role name: ${verifiedRole?.name}`); + log.debug(`Managed role ID: ${selfRole?.id}`); + if (!verifiedRole || !selfRole) { + continue; + } + + if (verifiedRole.position > selfRole.position) { + throw "Verified role is below the bot's managed role!"; + } + + roleValid = true; + break; +} + +if (!roleValid) { + throw "Role setup is not valid."; +} + +client.on(Events.MessageCreate, (event) => { + log.debug(`New message creation event for user ${event.author.username}...`); + if (!event.member) { + return; + } + + const user = seeUser(event.author.id); + if (user.verified) { + log.debug("Member has already been verified!"); + return; + } + + log.debug(`Message count is ${user.count}.`); + if (user.count > minimumMessages) { + log.debug("Verifying member..."); + if (event.member.moderatable) { + event.member.roles.add(roleId); + } + + verifyUser(event.member.id); + log.debug("Verified member!"); + } +}); diff --git a/src/discord/events/register.ts b/src/discord/events/register.ts new file mode 100644 index 0000000..bfc7882 --- /dev/null +++ b/src/discord/events/register.ts @@ -0,0 +1,23 @@ +import { opendir } from "node:fs/promises"; +import { basename, resolve, join } from "node:path"; +import { log } from "../../log"; + +const self = basename(__filename); +const path = resolve(__dirname); + +export async function register() { + log.info("Registering all Discord listeners..."); + + const directory = await opendir(path); + for await (const entry of directory) { + if (entry.name === self) { + continue; + } + + log.debug(`Registering ${entry.name}...`); + await import(join(path, entry.name)); + log.debug("Done! Moving on..."); + } + + log.info("Finished registering!"); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..74134a0 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +import { register } from "./discord/events/register"; + +await register(); diff --git a/src/log.ts b/src/log.ts new file mode 100644 index 0000000..bebdeff --- /dev/null +++ b/src/log.ts @@ -0,0 +1,11 @@ +import { pino } from "pino"; +import pretty from "pino-pretty"; + +export const log = pino( + { + "level": process.env.VERBOSE ? "debug" : "info" + }, + pretty({ + "ignore": "pid,hostname" + }) +); diff --git a/src/util/env.ts b/src/util/env.ts new file mode 100644 index 0000000..013a103 --- /dev/null +++ b/src/util/env.ts @@ -0,0 +1,17 @@ +function env(key: string): string { + const value = process.env[key]; + + if (!value) { + throw `The environment variable "${key}" is undefined! Change it in the ".env" file.`; + } + + return value; +} + +export function string(key: string): string { + return env(key); +} + +export function number(key: string): number { + return Number.parseInt(env(key)); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d175cad --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true + } +}