Add v19 bigbot rest

This commit is contained in:
Fleny
2024-06-09 17:52:55 +02:00
parent 04de1c8b92
commit 7e8c20378d
11 changed files with 3111 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
#
# General Configurations
#
# For Prisma use, it should be a postgres connection url
# Template: postgres://[username]:[password]@[host]:[port]/[db]
# Replate the [...] from the template with your values
# TEMPLATE-SETUP: Add a postgres connection string
DATABASE_URL=
# Whether or not this process is a local development version
# In production this value should be set to false
# TEMPLATE-SETUP: When deploying, set this value to false
DEVELOPMENT=true
# The server id where you develop/test the bot
# TEMPLATE-SETUP: Add the id to a server where you develop the bot
DEV_SERVER_ID=
# The discord bot token
# NOTE: It should not be prefixed with Bot
# TEMPLATE-SETUP: Add the bot token here.
DISCORD_TOKEN=
#
# Bot Configuration
#
# NOTE: With "bot code" we refer to the code that will handle the events
# The secret passcode that the bot code is checking for
# This is used to prevent someone else from trying to send malicious events to your bot
# TEMPLATE-SETUP: Add a secret passcode here. It can be whatever you want
EVENT_HANDLER_AUTHORIZATION=
# The host where the event handler will run
# Will be used together with EVENT_HANDLER_PORT to compose the HTTP url to send the events to
# TEMPLATE-SETUP: Set the event handler's host here
EVENT_HANDLER_HOST=localhost
# The port where the event handler will listening for events
# TEMPLATE-SETUP: Set the port where events will be sent
EVENT_HANDLER_PORT=8081
# The full discord webhook url where the bot can send errors to alert you that the bot is missing translations
# TEMPLATE-SETUP: Add the full discord webhook url
MISSING_TRANSLATION_WEBHOOK=
# The full webhook url where the bot can send errors to alert you that the bot is throwing errors.
# TEMPLATE-SETUP: Add the full discord webhook url
BUGS_ERRORS_REPORT_WEBHOOK=
#
# Rest Proxy Configurations
#
# The passcode that the REST proxy is checking for
# This is used to prevent someone else from trying to send malicious API requests from your bot
# TEMPLATE-SETUP: Add a secret passcode here. It can be whatever you want
REST_AUTHORIZATION=
# The host where the REST proxy will run
# Will be used together with REST_PORT to compose the HTTP url to send the API requests to
# TEMPLATE-SETUP: Set the REST proxy's host here
REST_HOST=localhost
# The port where the REST proxy will listen for API requests
# TEMPLATE-SETUP: Set the port where API requests will be sent
REST_PORT=8000
#
# Gateway Proxy Configurations
#
# The amount of shards to start
# Useful with multiple servers where each server is handling a portion of your bot
# OPTIONAL: You can leave this value unspecified if you want this server to manage all shards
# TEMPLATE-SETUP: If you have separate servers, add the number of shards this process should handle
TOTAL_SHARDS=
# The amount of shards to start per worker.
# NOTE: If you are not sure just stick to 16
# TEMPLATE-SETUP: Set how many shards to start per worker
SHARDS_PER_WORKER=16
# The total amount of workers to start.
# NOTE: Generally this should be equal to the number of cores your server has
# TEMPLATE-SETUP: Choose how many workers to start up
TOTAL_WORKERS=4
# The passcode that the gateway is checking for
# This is used to prevent someone else from trying to send malicious messages to your bot
# TEMPLATE-SETUP: Set a secret passcode here. It can be whatever you want
GATEWAY_AUTHORIZATION=
# The host where the gateway will run
# Will be used together with GATEWAY_PORT to compose the HTTP url to send the gateway messages to
# TEMPLATE-SETUP: Set the gateway's host here
GATEWAY_HOST=localhost
# The port where the gateway will listen for gateway messages
# TEMPLATE-SETUP: Set the port where gateway messages will be sent
GATEWAY_PORT=8080
#
# Message queue (RabbitMQ configuration)
#
# Whatever to queue messages from the gateway to bot
# NOTE: If this is set to true, all other configuration in this section are requried
# NOTE: if this is set to false, gateway messages will be sent directly to the bot code, and will fail if the bot code is not running
MESSAGEQUEUE_ENABLE=false
# The url of the RabbitMQ instance
MESSAGEQUEUE_URL=localhost:5672
# Username for the authentication against the RabbitMQ instance
MESSAGEQUEUE_USERNAME=
# Password for the authentication against the RabbitMQ instance
MESSAGEQUEUE_PASSWORD=
#
# Analytics (InfluxDB configuration)
#
# NOTE: This entire section is optional
# TEMPLATE-SETUP: If you want to enable analytics, add the the following values
# The InfluxDB organization
INFLUX_ORG=
# The InfluxDB bucket
INFLUX_BUCKET=
# The InfluxDB secret API token
# NOTE: this may need to be in quotes ("...") if it contains the = sign
INFLUX_TOKEN=
# The InfluxDB Instance url
INFLUX_URL=http://localhost:8086
#
# Docker InfluxDB
#
DOCKER_INFLUXDB_INIT_MODE=setup
DOCKER_INFLUXDB_INIT_USERNAME=discordeno
DOCKER_INFLUXDB_INIT_PASSWORD=discordeno
DOCKER_INFLUXDB_INIT_ORG=discordeno
DOCKER_INFLUXDB_INIT_BUCKET=discordeno
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=discordeno

32
examples/bigbot-19/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# build
dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

24
examples/bigbot-19/.swcrc Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2022",
"keepClassNames": true,
"loose": true
},
"module": {
"type": "es6",
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
}
}

View File

@@ -0,0 +1,33 @@
{
"name": "dd-bigbot",
"version": "1.0.0",
"description": "A scalable bot for big bot developers.",
"main": "dist/index.js",
"type": "module",
"license": "ISC",
"private": true,
"packageManager": "yarn@4.0.2",
"scripts": {
"start:bot": "echo TODO: fill this up",
"start:rest": "node dist/rest/index.js",
"start:gatewa": "echo TODO: fill this up",
"build": "swc src --strip-leading-paths --delete-dir-on-start --out-dir dist",
"build:watch": "swc src --strip-leading-paths --delete-dir-on-start --watch --out-dir dist",
"dev:bot": "echo TODO: fill this up",
"dev:rest": "node --watch --watch-preserve-output dist/rest/index.js",
"dev:gatewa": "echo TODO: fill this up",
"setup-dd": ""
},
"dependencies": {
"@discordeno/bot": "19.0.0-next.ad7e74c",
"@fastify/multipart": "^8.3.0",
"@influxdata/influxdb-client": "^1.33.2",
"fastify": "^4.27.0"
},
"devDependencies": {
"@swc/cli": "^0.3.12",
"@swc/core": "^1.5.25",
"@types/node": "^20.14.2",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,60 @@
import { Intents } from '@discordeno/bot'
import 'dotenv/config'
// #region Mapping of environment variables to javascript variables
// General Configurations
export const DATABASE_URL = process.env.DATABASE_URL
export const DEVELOPMENT = process.env.DEVELOPMENT
export const DEV_SERVER_ID = process.env.DEV_SERVER_ID
export const DISCORD_TOKEN = process.env.DISCORD_TOKEN
// Bot Configuration
export const EVENT_HANDLER_AUTHORIZATION = process.env.EVENT_HANDLER_AUTHORIZATION
export const EVENT_HANDLER_HOST = process.env.EVENT_HANDLER_HOST
export const EVENT_HANDLER_PORT = process.env.EVENT_HANDLER_PORT
export const MISSING_TRANSLATION_WEBHOOK = process.env.MISSING_TRANSLATION_WEBHOOK
export const BUGS_ERRORS_REPORT_WEBHOOK = process.env.BUGS_ERRORS_REPORT_WEBHOOK
// Rest Proxy Configurations
export const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION
export const REST_HOST = process.env.REST_HOST
export const REST_PORT = process.env.REST_PORT
// Gateway Proxy Configurations
export const TOTAL_SHARDS = process.env.TOTAL_SHARDS
export const SHARDS_PER_WORKER = process.env.SHARDS_PER_WORKER
export const TOTAL_WORKERS = process.env.TOTAL_WORKERS
export const GATEWAY_AUTHORIZATION = process.env.GATEWAY_AUTHORIZATION
export const GATEWAY_HOST = process.env.GATEWAY_HOST
export const GATEWAY_PORT = process.env.GATEWAY_PORT
// Message queue (RabbitMQ configuration)
export const MESSAGEQUEUE_ENABLE = process.env.MESSAGEQUEUE_ENABLE
export const MESSAGEQUEUE_URL = process.env.MESSAGEQUEUE_URL
export const MESSAGEQUEUE_USERNAME = process.env.MESSAGEQUEUE_USERNAME
export const MESSAGEQUEUE_PASSWORD = process.env.MESSAGEQUEUE_PASSWORD
// Analytics (InfluxDB configuration)
export const INFLUX_ORG = process.env.INFLUX_ORG
export const INFLUX_BUCKET = process.env.INFLUX_BUCKET
export const INFLUX_TOKEN = process.env.INFLUX_TOKEN
export const INFLUX_URL = process.env.INFLUX_URL
// #endregion
export const EVENT_HANDLER_URL = `http://${EVENT_HANDLER_HOST}:${EVENT_HANDLER_PORT}`
export const REST_URL = `http://${REST_HOST}:${REST_PORT}`
export const GATEWAY_URL = `http://${GATEWAY_HOST}:${GATEWAY_PORT}`
// TEMPLATE-SETUP: Add/Remove the intents you need/don't need
export const GATEWAY_INTENTS = Intents.Guilds | Intents.GuildMessages

View File

@@ -0,0 +1,51 @@
import type { RestManager } from '@discordeno/bot'
import { InfluxDB, Point } from '@influxdata/influxdb-client'
import { INFLUX_BUCKET, INFLUX_ORG, INFLUX_TOKEN, INFLUX_URL } from './config.js'
const isInfluxConfigured = INFLUX_URL && INFLUX_TOKEN && INFLUX_ORG && INFLUX_BUCKET
// @ts-expect-error INFLUX_URL is not undefined if isInfluxConfigured is true, however this kinda of stuff confuses typescript. This will get fixed in TS 5.5
export const influxDB = isInfluxConfigured ? new InfluxDB({ url: INFLUX_URL, token: INFLUX_TOKEN }) : undefined
// @ts-expect-error INFLUX_ORG is not undefined if isInfluxConfigured is true, however this kinda of stuff confuses typescript. This will get fixed in TS 5.5
export const influx = isInfluxConfigured && influxDB ? influxDB.getWriteApi(INFLUX_ORG, INFLUX_BUCKET) : undefined
export const setupRestAnalyticsHooks = (rest: RestManager, logger: RestManager['logger']): void => {
// If influxdb data is provided, enable analytics in this proxy.
if (!influx) return
const originalSendRequest = rest.sendRequest
rest.sendRequest = async (options) => {
const fetchingPoint = new Point('restEvents')
.timestamp(new Date())
.stringField('type', 'REQUEST_FETCHING')
.tag('method', options.method)
.tag('route', options.route)
.tag('bucket', options.bucketId ?? 'NA')
influx.writePoint(fetchingPoint)
await originalSendRequest(options)
const fetchedPoint = new Point('restEvents')
.timestamp(new Date())
.stringField('type', 'REQUEST_FETCHED')
.tag('method', options.method)
.tag('route', options.route)
.tag('bucket', options.bucketId ?? 'NA')
// FIXME: rest.sendRequest returns Promise<void>, so there is no way currently to get the response status
// .intField('status', response.status)
influx.writePoint(fetchedPoint)
}
setInterval(async () => {
logger.info('Influx - Saving events...')
try {
await influx.flush()
logger.info('Influx - events saved!')
} catch (error) {
logger.error('Influx - error saving events!', error)
}
}, 30_000 /* 30s */)
}

View File

@@ -0,0 +1,39 @@
import fastifyMultipart, { type MultipartFile, type MultipartValue } from '@fastify/multipart'
import fastify, { type FastifyInstance } from 'fastify'
import { REST_AUTHORIZATION } from '../config.js'
export function buildFastifyApp(): FastifyInstance {
const app = fastify()
app.register(fastifyMultipart, { attachFieldsToBody: true })
// Authorization check
app.addHook('onRequest', async (request, reply) => {
if (request.headers.authorization !== REST_AUTHORIZATION) {
reply.status(401).send({
message: 'Credentials not valid.',
})
}
})
return app
}
export async function parseMultiformBody(body: unknown): Promise<FormData> {
const form = new FormData()
if (typeof body !== 'object' || !body) return form
for (const objectValue of Object.values(body)) {
const value = objectValue as MultipartFile | MultipartValue
if (value.type === 'file') {
form.append(value.fieldname, new Blob([await value.toBuffer()]), value.filename)
}
if (value.type === 'field' && typeof value.value === 'string') {
form.append(value.fieldname, value.value)
}
}
return form
}

View File

@@ -0,0 +1,56 @@
import { type RequestMethods } from '@discordeno/bot'
import { REST_AUTHORIZATION, REST_HOST, REST_PORT } from '../config.js'
import { buildFastifyApp, parseMultiformBody } from './fastify.js'
import restManager, { logger } from './restManager.js'
if (!REST_AUTHORIZATION) throw new Error('The REST_AUTHORIZATION environment variable is missing')
if (!REST_HOST) throw new Error('The REST_HOST environment variable is missing')
if (!REST_PORT) throw new Error('The REST_PORT environment variable is missing')
const portNumber = Number.parseInt(REST_PORT)
if (Number.isNaN(portNumber)) throw new Error('The REST_PORT environment variable should be a valid number')
const app = buildFastifyApp()
app.get('/timecheck', async (_req, res) => {
res.status(200).send({ message: Date.now() })
})
app.all('/*', async (req, res) => {
let url = req.originalUrl
if (url.startsWith('/v')) {
url = url.slice(url.indexOf('/', 2))
}
const isMultipart = req.headers['content-type']?.startsWith('multipart/form-data')
const hasBody = req.method !== 'GET' && req.method !== 'DELETE'
const body = hasBody ? (isMultipart ? await parseMultiformBody(req.body) : req.body) : undefined
try {
const result = await restManager.makeRequest(req.method as RequestMethods, url, {
body,
})
if (result) {
res.status(200).send(result)
return
}
res.status(204).send({})
} catch (error) {
logger.error(error)
res.status(500).send({
message: error,
})
}
})
await app.listen({
host: REST_HOST,
port: portNumber,
})
logger.info(`REST Proxy listening on port ${portNumber}`)

View File

@@ -0,0 +1,16 @@
import { createLogger, createRestManager, LogDepth } from '@discordeno/bot'
import { DISCORD_TOKEN } from '../config.js'
import { setupRestAnalyticsHooks } from '../influx.js'
if (!DISCORD_TOKEN) throw new Error('The DISCORD_TOKEN environment variable is missing')
const manager = createRestManager({
token: DISCORD_TOKEN,
})
export const logger = createLogger({ name: 'REST' })
logger.setDepth(LogDepth.Full)
setupRestAnalyticsHooks(manager, logger)
export default manager

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"moduleResolution": "node",
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"strict": true,
"incremental": true
}
}

2634
examples/bigbot-19/yarn.lock Normal file

File diff suppressed because it is too large Load Diff