diff options
| author | Andrew Lee <andrew@alee14.me> | 2026-04-20 16:19:09 -0400 |
|---|---|---|
| committer | Andrew Lee <andrew@alee14.me> | 2026-04-20 16:19:09 -0400 |
| commit | 98805430b78fa24a7d80808c50f3c38a3a748f07 (patch) | |
| tree | 6362690ecd724989867a044033bc84622f955a8a | |
| parent | f8b93cf133126b57d85d8a5fc424fc87ad2f4459 (diff) | |
| download | bnbmc-announcement-api-98805430b78fa24a7d80808c50f3c38a3a748f07.tar.gz bnbmc-announcement-api-98805430b78fa24a7d80808c50f3c38a3a748f07.tar.bz2 bnbmc-announcement-api-98805430b78fa24a7d80808c50f3c38a3a748f07.zip | |
attachment support; santitized input; docker support
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | api/Dockerfile | 12 | ||||
| -rwxr-xr-x | api/bun.lockb | bin | 30656 -> 31778 bytes | |||
| -rw-r--r-- | api/index.ts | 13 | ||||
| -rw-r--r-- | api/package.json | 2 | ||||
| -rw-r--r-- | bot/Dockerfile | 12 | ||||
| -rwxr-xr-x | bot/bun.lockb | bin | 11472 -> 12570 bytes | |||
| -rw-r--r-- | bot/index.ts | 91 | ||||
| -rw-r--r-- | bot/package.json | 7 | ||||
| -rw-r--r-- | compose.yaml | 16 |
11 files changed, 141 insertions, 14 deletions
@@ -1,3 +1,4 @@ node_modules/ database.sqlite .env +public/ @@ -2,6 +2,7 @@ Reads messages from the bits & Bytes Minecraft General Announcements, and displays it on the website. # Configuration +bot/.env ``` TOKEN=[token] ANNOUNCEMENT_CHANNEL=[channel_id] diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..a7a7d62 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,12 @@ +FROM bun:latest + +WORKDIR /api + +COPY bun.lockd . +COPY package.json . + +RUN bun install + +COPY . . + +ENTRYPOINT ["bun", "index.ts"] diff --git a/api/bun.lockb b/api/bun.lockb Binary files differindex 52c0b95..9d47e1d 100755 --- a/api/bun.lockb +++ b/api/bun.lockb diff --git a/api/index.ts b/api/index.ts index 2fb7a08..9435bb1 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,10 +1,13 @@ import express from 'express'; import { Database } from "bun:sqlite"; +import cors from 'cors'; const app = express(); const port = process.env.PORT || 3000; const db = new Database("../database.sqlite", { readonly: true }); +app.use(express.static('../public')) +app.use(cors()); app.get('/', (req, res) => { const query = db.prepare(`SELECT * FROM announcements ORDER BY created_at DESC`); @@ -12,6 +15,16 @@ app.get('/', (req, res) => { res.send(result); }); +app.get('/attachments/:slug', (req, res) => { + if (req.params.slug) { + const query = db.prepare(`SELECT * FROM announcements_attachments WHERE msg_id = (?)`); + const result = query.all(req.params.slug); + res.send(result); + } else { + res.send('Unknown message id.'); + } +}); + app.listen(port, () => { console.log(`Server is running on port ${port}`); }); diff --git a/api/package.json b/api/package.json index c9b6a1e..8fcc3ed 100644 --- a/api/package.json +++ b/api/package.json @@ -4,10 +4,12 @@ "main": "index.js", "license": "MIT", "dependencies": { + "cors": "^2.8.6", "express": "^5.2.1" }, "devDependencies": { "@types/bun": "^1.3.11", + "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^25.5.2" } diff --git a/bot/Dockerfile b/bot/Dockerfile new file mode 100644 index 0000000..e37f276 --- /dev/null +++ b/bot/Dockerfile @@ -0,0 +1,12 @@ +FROM bun:latest + +WORKDIR /bot + +COPY bun.lockd . +COPY package.json . + +RUN bun install + +COPY . . + +ENTRYPOINT ["bun", "index.ts"] diff --git a/bot/bun.lockb b/bot/bun.lockb Binary files differindex 28b13e5..3d6ad67 100755 --- a/bot/bun.lockb +++ b/bot/bun.lockb diff --git a/bot/index.ts b/bot/index.ts index e3a2c6a..2c8bfd2 100644 --- a/bot/index.ts +++ b/bot/index.ts @@ -1,13 +1,16 @@ import { Client, Events, GatewayIntentBits } from 'discord.js'; +import removeMd from 'remove-markdown'; +import { writeFileSync, mkdirSync, rmSync } from 'fs'; +import { marked } from 'marked'; import { Database } from "bun:sqlite"; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] }); -const db = new Database("../database.sqlite", { create: true }); +const db = new Database(process.env.DB_LOCATION, { create: true }); function parseMessage(content: string) { const lines = content.split('\n'); const authorMatch = content.match(/\*from(?: the)? ([^*]+)\*/i); - const titleLine = lines.slice(0, 2).find(l => /^#+\s+.+|^.+\*\*[^*]+\*\*$/.test(l)) ?? null; + const titleLine = lines.slice(0, 2).find(l => /^#+\s+.+|^\*\*.+\*\*|^.+\*\*[^*]+\*\*$/.test(l)) ?? null; const author = authorMatch ? authorMatch[1].trim() : null; const title = titleLine ? titleLine.replace(/^#+\s+/, '').replace(/\*\*/g, '').trim() : null; @@ -33,28 +36,67 @@ client.on(Events.ClientReady, bot => { message TEXT, created_at INTEGER )`); + + db.run(`CREATE TABLE IF NOT EXISTS announcements_attachments ( + id INTEGER PRIMARY KEY, + msg_id INTERGER, + file_name TEXT + )`); + + db.run(`CREATE TABLE IF NOT EXISTS exclude_person ( + id INTEGER PRIMARY KEY, + user_id INTERGER + )`); } catch (e) { console.error(e); } }); client.on(Events.MessageCreate, async (msg) => { + const query = db.prepare('SELECT * FROM exclude_person WHERE user_id = ?'); + const result = query.get(msg.author.id); + if (result) return; + if (msg.author.bot) return; - if (msg.author.id === client.user!.id) return; + if (msg.channel.id !== process.env.ANNOUNCEMENT_CHANNEL) return; - if (!(msg.content.startsWith('# ') || msg.content.startsWith('*') || msg.content.match(/^:[a-z0-9_]+:/i) || msg.content.match(/^\p{Emoji}/u))) return; + if (!(msg.content.startsWith('#') || msg.content.startsWith('*') || msg.content.match(/^:[a-z0-9_]+:/i) || msg.content.match(/^\p{Emoji}/u))) return; const { title, author, message } = parseMessage(msg.content); + if (!title || !author || !message) return; + try { const insert = db.prepare(`INSERT INTO announcements (title, msg_id, author, message, created_at) VALUES (($title), ($msg_id), ($author), ($message), ($created_at))`); insert.run({ - $title: title, + $title: removeMd(title), $msg_id: msg.id, - $author: author, - $message: message, + $author: removeMd(author), + $message: await marked(message), $created_at: msg.createdTimestamp.toString() }) + + if (msg.attachments.size > 0) { + try { + mkdirSync(`../public/images/${msg.id}`, { recursive: true }); + + for (const a of msg.attachments.values()) { + const response = await fetch(a.url); + const buffer = Buffer.from(await response.arrayBuffer()); + writeFileSync(`../public/images/${msg.id}/${a.name}`, buffer); + + const insert = db.prepare(`INSERT INTO announcements_attachments (msg_id, file_name) VALUES ($msg_id, $file_name)`); + insert.run({ + $msg_id: msg.id, + $file_name: a.name + }); + } + } catch (e) { + console.error(e); + return; + } + } + } catch (e) { console.error(e); } @@ -64,19 +106,34 @@ client.on(Events.MessageCreate, async (msg) => { client.on(Events.MessageUpdate, async (msg, msgnew) => { if (!msg.author) return; if (!msg.content) return; + if (msg.author.bot) return; - if (msg.author.id === client.user!.id) return; if (msg.channel.id !== process.env.ANNOUNCEMENT_CHANNEL) return; - if (!(msgnew.content.startsWith('# ') || msgnew.content.startsWith('*') || msgnew.content.match(/^:[a-z0-9_]+:/i) || msgnew.content.match(/^\p{Emoji}/u))) return; + if (!(msgnew.content.startsWith('#') || msgnew.content.startsWith('*') || msgnew.content.match(/^:[a-z0-9_]+:/i) || msgnew.content.match(/^\p{Emoji}/u))) return; const { title, author, message } = parseMessage(msgnew.content); + if (!title || !author || !message) return; + + try { + const removedAttachments = msg.attachments.filter(a => !msgnew.attachments.has(a.id)); + + for (const a of removedAttachments.values()) { + const deleteAttachment = db.prepare(`DELETE FROM announcements_attachments WHERE msg_id = ($msg_id) AND file_name = ($file_name)`); + deleteAttachment.run({ + $msg_id: msg.id, + $file_name: a.name + }); + } + } catch (e) { + console.error(e); + } try { const update = db.prepare(`UPDATE announcements SET title = ($title), author = ($author), message = ($message) WHERE msg_id = ($msg_id)`); update.run({ - $title: title, - $author: author, - $message: message, + $title: removeMd(title), + $author: removeMd(author), + $message: await marked(message), $msg_id: msg.id }) } catch (e) { @@ -89,6 +146,7 @@ client.on(Events.MessageDelete, async (msg) => { if (!msg.author) return; if (!msg.content) return; if (msg.author.bot) return; + if (msg.author.id === client.user!.id) return; if (msg.channel.id !== process.env.ANNOUNCEMENT_CHANNEL) return; @@ -97,6 +155,15 @@ client.on(Events.MessageDelete, async (msg) => { deleteAnnouncement.run({ $msg_id: msg.id }) + + if (msg.attachments.size > 0) { + const deleteAttachment = db.prepare(`DELETE FROM announcements_attachments WHERE msg_id = ($msg_id)`); + deleteAttachment.run({ + $msg_id: msg.id + }); + + rmSync(`../public/images/${msg.id}`, { recursive: true }); + } } catch (e) { console.error(e); } diff --git a/bot/package.json b/bot/package.json index 4ada057..546cb39 100644 --- a/bot/package.json +++ b/bot/package.json @@ -4,10 +4,13 @@ "main": "index.js", "license": "MIT", "dependencies": { - "discord.js": "^14.26.2" + "discord.js": "^14.26.2", + "marked": "^18.0.0", + "remove-markdown": "^0.6.3" }, "devDependencies": { "@types/bun": "^1.3.11", - "@types/node": "^25.5.2" + "@types/node": "^25.5.2", + "@types/remove-markdown": "^0.3.4" } } diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..0952f11 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,16 @@ +services: + bot: + image: bnbmc-announcement-bot + hostname: bot + restart: unless-stopped + volumes: + - ./database.db:/database.db + - ./.env:/bot/.env + web: + image: bnbmc-announcement-bot-api + hostname: api + restart: unless-stopped + volumes: + - ./database.db:/database.db + ports: + - "3000:3000" |
