Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(discord-bot): Implement Discord Bot Prototype #468

Open
wants to merge 14 commits into
base: beta
Choose a base branch
from
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ Thumbs.db
# Next.js
.next

# SQLite
**/*.sqlite

# Google Analytics API
google_application_credentials.json

# cgr-api log
log/

# Scraper data source (local)
data/
9 changes: 9 additions & 0 deletions apps/discord-bot/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
DISCORD_TOKEN=
DISCORD_CLIENT_ID=

# GOOGLE API
GA4_PROPERTY_ID=
GOOGLE_APPLICATION_CREDENTIALS=./google_application_credentials.json

# Database
DATABASE_CONNECTION_URL=database.sqlite
18 changes: 18 additions & 0 deletions apps/discord-bot/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
Comment on lines +1 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this file here for? doesn't this literally ignore every error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is copy from apps/api/.eslintrc.json

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you recommend how I should lint this apps?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saenyakorn did u add this in api?

1 change: 1 addition & 0 deletions apps/discord-bot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
16 changes: 16 additions & 0 deletions apps/discord-bot/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable */
export default {
displayName: 'discord-bot',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/discord-bot',
}
72 changes: 72 additions & 0 deletions apps/discord-bot/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "discord-bot",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/discord-bot/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"target": "node",
"compiler": "tsc",
"outputPath": "dist/apps/discord-bot",
"main": "apps/discord-bot/src/main.ts",
"tsConfig": "apps/discord-bot/tsconfig.app.json"
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false,
"fileReplacements": [
{
"replace": "apps/discord-bot/src/environments/environment.ts",
"with": "apps/discord-bot/src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"executor": "@nrwl/js:node",
"options": {
"buildTarget": "discord-bot:build"
},
"configurations": {
"production": {
"buildTarget": "discord-bot:build:production"
}
}
},
"deploy-commands": {
"executor": "nx:run-commands",
"options": {
"command": "ts-node --project ./apps/discord-bot/tsconfig.app.json ./apps/discord-bot/src/deploy-commands.ts"
}
},
"prisma-migrate-dev": {
"executor": "nx:run-commands",
"options": {
"command": "cd {args.db} && npx prisma migrate dev",
"cwd": "apps/discord-bot/src/database"
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/discord-bot/**/*.ts"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/discord-bot/jest.config.ts",
"passWithNoTests": true
}
}
},
"tags": []
}
9 changes: 9 additions & 0 deletions apps/discord-bot/src/command/ICommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChatInputApplicationCommandData, ChatInputCommandInteraction, Client } from 'discord.js'

import { CUGetReg } from '../core/CUGetReg'

export interface ICommand extends ChatInputApplicationCommandData {
readonly name: string
readonly description: string
execute: (client: CUGetReg, interaction: ChatInputCommandInteraction) => void
}
5 changes: 5 additions & 0 deletions apps/discord-bot/src/command/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ICommand } from './ICommand'
import { PingCommand } from './ping'
import { RegisterReportChannelCommand } from './registerReportChannel'

export const commands: ICommand[] = [PingCommand, RegisterReportChannelCommand]
12 changes: 12 additions & 0 deletions apps/discord-bot/src/command/ping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ChatInputCommandInteraction, Client } from 'discord.js'

import { CUGetReg } from '../core/CUGetReg'
import { ICommand } from './ICommand'

export const PingCommand: ICommand = {
name: 'ping',
description: 'Replies with Pong!',
execute: (client: CUGetReg, interaction: ChatInputCommandInteraction): void => {
interaction.reply('Pong!')
},
}
14 changes: 14 additions & 0 deletions apps/discord-bot/src/command/registerReportChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChatInputCommandInteraction } from 'discord.js'

import { CUGetReg } from '../core/CUGetReg'
import { ICommand } from './ICommand'

export const RegisterReportChannelCommand: ICommand = {
name: 'register_report_channel',
description: 'channel to receive reports',
execute: async (client: CUGetReg, interaction: ChatInputCommandInteraction): Promise<void> => {
await client.db.saveRegisterReportChannel(interaction.guildId, interaction.channelId)

interaction.reply('Channel Registered!')
},
}
25 changes: 25 additions & 0 deletions apps/discord-bot/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface Configuration {
discord: {
token: string
clientID: string
}
googleAnalytics: {
GA4_PROPERTY_ID: string
}
database: {
connectionURL: string
}
}

export const configuration: Configuration = {
discord: {
token: process.env.DISCORD_TOKEN,
clientID: process.env.DISCORD_CLIENT_ID,
},
googleAnalytics: {
GA4_PROPERTY_ID: process.env.GA4_PROPERTY_ID,
},
database: {
connectionURL: process.env.DATABASE_CONNECTION_URL,
},
}
61 changes: 61 additions & 0 deletions apps/discord-bot/src/core/CUGetReg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { AttachmentBuilder, Client, GatewayIntentBits, TextChannel } from 'discord.js'
import * as cron from 'node-cron'

import { commands } from '../command'
import { ICommand } from '../command/ICommand'
import { database } from '../database'
import { IDatabase } from '../database/IDatabase'
import { hooks } from '../hook'
import { HookFunction } from '../hook/type'
import { scheduler } from '../scheduler'
import { IScheduler } from '../scheduler/IScheduler'

export class CUGetReg extends Client {
private _commandList = new Map<string, ICommand>()
private _database: IDatabase = database

constructor(token: string) {
super({ intents: [GatewayIntentBits.Guilds] })
this.login(token)
this.addHook(hooks)
this.registerCommand(commands)
this.registerScheduler(scheduler)
}

addHook(hookFunctions: HookFunction[]): void {
hookFunctions.forEach((hookFunction) => hookFunction(this))
}

registerCommand(commands: ICommand[]): void {
commands.forEach((command) => {
this._commandList.set(command.name, command)
})
}

registerScheduler(schedulers: IScheduler[]) {
schedulers.forEach((scheduler) => {
cron.schedule(scheduler.cronTime, () => scheduler.callbackFunction(this))
})
}

sendMessage(channelID: string, message: string): void {
const textChannel = this.channels.cache.get(channelID) as TextChannel

textChannel.send(message)
}

sendImage(channelID: string, imgBuffer: Buffer): void {
const textChannel = this.channels.cache.get(channelID) as TextChannel
const attachment = new AttachmentBuilder(imgBuffer)

textChannel.send({ files: [attachment] })
}

get commands(): Map<string, ICommand> {
return this._commandList
}

get db(): IDatabase {
return this._database
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same goes here

}
}
5 changes: 5 additions & 0 deletions apps/discord-bot/src/database/IDatabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IDatabase {
saveRegisterReportChannel(guildId: string, channelId: string): Promise<void>
getRegisterReportChannel(guildId: string): Promise<string[]>
getAllReportChannels(): Promise<Map<string, string>>
}
4 changes: 4 additions & 0 deletions apps/discord-bot/src/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { IDatabase } from './IDatabase'
import { SQLiteDatabaseDriver } from './sqlite/SQLiteDatabaseDriver'

export const database: IDatabase = new SQLiteDatabaseDriver()
55 changes: 55 additions & 0 deletions apps/discord-bot/src/database/sqlite/SQLiteDatabaseDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { open } from 'sqlite'
import * as sqlite3 from 'sqlite3'

import { configuration } from '../../config'
import { IDatabase } from '../IDatabase'

const DB_FILE_NAME = configuration.database.connectionURL

type ReportChannel = {
guild_id?: string
channel_id: string
}

export class SQLiteDatabaseDriver implements IDatabase {
constructor() {
new sqlite3.Database(DB_FILE_NAME).run(
'CREATE TABLE IF NOT EXISTS register_report_channel (channel_id TEXT PRIMARY KEY, guild_id TEXT)'
)
}

async openDB() {
return open({
driver: sqlite3.Database,
filename: DB_FILE_NAME,
})
}

async saveRegisterReportChannel(guildId: string, channelId: string): Promise<void> {
const database = await this.openDB()

await database.run(`INSERT INTO register_report_channel VALUES ('${channelId}', '${guildId}')`)
return
}

async getRegisterReportChannel(guildId: string): Promise<string[]> {
const database = await this.openDB()

const result: ReportChannel[] = await database.all(
`SELECT channel_id FROM register_report_channel WHERE guild_id = '${guildId}'`
)
return result.map((obj: ReportChannel) => {
return obj.channel_id
})
}

async getAllReportChannels(): Promise<Map<string, string>> {
const database = await this.openDB()

const result = await database.all(`SELECT * FROM register_report_channel`)
return result.reduce((map: Map<string, string>, obj: ReportChannel) => {
map.set(obj.guild_id, obj.channel_id)
return map
}, new Map<string, string>())
}
}
29 changes: 29 additions & 0 deletions apps/discord-bot/src/deploy-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { REST, Routes, SlashCommandBuilder } from 'discord.js'

import { commands as CUGetRegCommands } from './command'
import { configuration as config } from './config'

const commands = CUGetRegCommands.map((command) => {
return new SlashCommandBuilder()
.setName(command.name)
.setDescription(command.description)
.toJSON()
})

const rest = new REST({ version: '10' }).setToken(config.discord.token)

;(async () => {
try {
console.log(`Started refreshing ${commands.length} application (/) commands.`)

const data = Array(
await rest.put(Routes.applicationCommands(config.discord.clientID), {
body: commands,
})
)

console.log(`Successfully reloaded ${data.length} application (/) commands.`)
} catch (error) {
console.error(error)
}
})()
Loading