Adjust the directory structure to prepare for merging into Master.

This commit is contained in:
Liu Zhicong
2023-09-27 11:40:46 -07:00
parent 5146bc1871
commit 030e4c4f52
115 changed files with 413 additions and 160 deletions
+207
View File
@@ -0,0 +1,207 @@
import EventEmitter from "events"
import {
ChannelType,
ChatInputCommandInteraction,
Client,
GatewayIntentBits,
Interaction,
Message,
MessageReplyOptions,
Partials,
REST,
Routes,
} from "discord.js"
import { nanoid } from "nanoid"
import _ from "lodash"
import base64 from "base64-js"
import { ExchangeMessageData } from "../types/index.mjs"
interface BotAppEvents {
message_request: (data: ExchangeMessageData) => void
}
declare interface BotApp {
on<U extends keyof BotAppEvents>(event: U, cb: BotAppEvents[U]): this
off<U extends keyof BotAppEvents>(event: U, cb: BotAppEvents[U]): this
emit<U extends keyof BotAppEvents>(
event: U,
...args: Parameters<BotAppEvents[U]>
): boolean
}
class BotApp extends EventEmitter {
constructor(clientId: string, token: string) {
super()
this.clientId = clientId
this.token = token
this.init()
}
private clientId: string | undefined
private token: string | undefined
private client: Client | undefined
private cachedMessages: Map<string /* message id */, Message> = new Map()
private voiceSettings: Map<string /* chat id */, boolean> = new Map()
private typingTexts = _.range(1, 5).map(n => `_Typing${_.repeat(".", n)}_`)
private init = () => {
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Channel],
})
client.on("ready", () => {
console.log(`Logged in as ${client.user?.tag}!`)
})
client.on("messageCreate", this.handleMessageCreate)
client.on("interactionCreate", this.handleInteractionCreate)
this.client = client
}
private handleMessageCreate = async (message: Message) => {
console.log("handleMessageCreate", message.author.id)
const botId = this.client?.user?.id ?? ""
const isExpectedMessage =
message.author.id !== botId &&
(message.channel.type === ChannelType.DM ||
(message.channel.type === ChannelType.GuildText &&
message.mentions.has(botId) === true &&
message.mentions.everyone === false))
if (isExpectedMessage === false) {
return
}
const msgId = nanoid()
const chatId = message.channel.id
this.cachedMessages.set(msgId, message)
this.emit("message_request", {
user: {
id: `${message.author.id}`,
},
chat: {
id: chatId,
},
message: {
id: msgId,
type: "text",
content: message.content
.replaceAll(/@here/g, "")
.replaceAll(/@everyone/g, "")
.replaceAll(/\<@.*?\>/g, "")
.trim(),
},
options: {
voice: this.voiceSettings.get(chatId) ?? false,
},
})
}
private handleInteractionCreate = async (interaction: Interaction) => {
console.log("handleInteractionCreate", interaction.isChatInputCommand())
if (interaction.isChatInputCommand()) {
if (interaction.commandName === "reset") {
await this.handleCommandReset(interaction)
}
}
}
private handleCommandReset = async (
interaction: ChatInputCommandInteraction
) => {
const channelId = interaction.channel?.id
const useId = interaction.user?.id
if (channelId === undefined || useId === undefined) {
return
}
this.emit("message_request", {
user: {
id: useId,
},
chat: {
id: channelId,
},
message: {
id: nanoid(),
type: "clear",
content: "",
},
})
await interaction.reply("Done!")
}
replyMessage = async (data: ExchangeMessageData) => {
const cachedMessage = this.cachedMessages.get(data.message.id)
if (cachedMessage === undefined) {
return
}
const { type, content } = data.message
const replyOptions: MessageReplyOptions = {
content: "",
files: [],
}
if (type === "text") {
replyOptions.content = content
} else if (type === "image") {
replyOptions.files?.push(Buffer.from(base64.toByteArray(content)))
} else if (type === "voice") {
replyOptions.files?.push({
attachment: Buffer.from(base64.toByteArray(content)),
name: `${Date.now()}.wav`,
})
} else if (type === "end") {
}
await cachedMessage.reply(replyOptions)
}
start = async () => {
if (
this.token === undefined ||
this.clientId === undefined ||
this.client === undefined
) {
return
}
const commands = [
{
name: "reset",
description:
"Clear the history messages of the current session on the server.",
},
]
const rest = new REST({ version: "10" }).setToken(this.token)
await rest.put(Routes.applicationCommands(this.clientId), {
body: commands,
})
await this.client.login(this.token)
}
stop = () => {
this.client?.destroy()
}
}
export default BotApp
+57
View File
@@ -0,0 +1,57 @@
import dotenv from "dotenv"
import path from "path"
import { ExchangeMessageData } from "../types/index.mjs"
import BotApp from "./bot-app.mjs"
import WebsocketServer from "./websocket-server.mjs"
dotenv.config({
path: path.resolve(process.cwd(), ".env.local"),
})
class App {
constructor() {
// @ts-ignore
const botApp = new BotApp(process.env.CLIENT_ID, process.env.BOT_TOKEN)
const websocketServer = new WebsocketServer(
parseInt(process.env.WEBSOCKET_PORT as string, 10)
)
botApp.on("message_request", this.handleBotAppMessageRequest)
websocketServer.on("message_response", this.handleWebsocketMessageResponse)
this.botApp = botApp
this.websocketServer = websocketServer
}
private botApp: BotApp
private websocketServer: WebsocketServer
private handleBotAppMessageRequest = (data: ExchangeMessageData) => {
console.log("handleBotAppMessageRequest", data)
this.websocketServer.sendMessageRequest(data)
}
private handleWebsocketMessageResponse = (data: ExchangeMessageData) => {
console.log("handleWebsocketMessageResponse", data.message.type)
this.botApp.replyMessage(data)
}
start = () => {
this.botApp?.start()
this.websocketServer?.start()
}
stop = () => {
this.botApp?.stop()
this.websocketServer?.stop()
}
}
process.on("unhandledRejection", (error: Error) => {
console.log("unhandledRejection", error.message)
})
const app = new App()
app.start()
@@ -0,0 +1,78 @@
import { createServer, Server as HTTPServer } from "http"
import { Server as SocketIOServer, Socket } from "socket.io"
import express from "express"
import EventEmitter from "events"
import { ExchangeMessageData } from "../types/index.mjs"
interface WebsocketServerEvents {
message_response: (data: ExchangeMessageData) => void
}
declare interface WebsocketServer {
on<U extends keyof WebsocketServerEvents>(
event: U,
cb: WebsocketServerEvents[U]
): this
off<U extends keyof WebsocketServerEvents>(
event: U,
cb: WebsocketServerEvents[U]
): this
emit<U extends keyof WebsocketServerEvents>(
event: U,
...args: Parameters<WebsocketServerEvents[U]>
): boolean
}
class WebsocketServer extends EventEmitter {
constructor(port: number) {
super()
const app = express()
const httpServer = createServer(app)
const sioServer = new SocketIOServer(httpServer, {
maxHttpBufferSize: 50 * 1024 * 1024,
pingInterval: 25000,
pingTimeout: 20000,
})
sioServer.on("connect", this.handleConnect)
this.port = port
this.httpServer = httpServer
this.sioServer = sioServer
}
private port: number
private httpServer: HTTPServer
private sioServer: SocketIOServer
private handleConnect = (socket: Socket) => {
console.log("handleConnect", socket.id)
socket.on("disconnect", (reason: string) => {
console.log("socket disconnect", reason, Date.now())
})
socket.on("chat_message", (data: ExchangeMessageData) => {
this.emit("message_response", data)
})
}
sendMessageRequest = (data: ExchangeMessageData) => {
this.sioServer.emit("chat_message", data)
}
start = () => {
this.httpServer.listen(this.port, () => {
console.log(`listening on *:${this.port}`)
})
}
stop = () => {
this.sioServer.close()
}
}
export default WebsocketServer
+16
View File
@@ -0,0 +1,16 @@
export interface ExchangeMessageData {
user: {
id: string
}
chat: {
id: string
}
message: {
type: "text" | "image" | "voice" | "clear" | "end"
content: string
id: string
}
options?: {
voice: boolean
}
}