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
+7
View File
@@ -0,0 +1,7 @@
# bot
CLIENT_ID='your discord bot client id'
BOT_TOKEN='your discord bot token'
# websocket
# the same as JARVIS_SERVER_MODE_PORT in agent_jarvis .env file
WEBSOCKET_PORT=10000
+3
View File
@@ -0,0 +1,3 @@
node_modules
*.log
.env.local
View File
+6
View File
@@ -0,0 +1,6 @@
{
"arrowParens": "avoid",
"semi": false,
"useTabs": false,
"tabWidth": 2
}
+5
View File
@@ -0,0 +1,5 @@
FROM node:19-alpine
COPY . /app
WORKDIR /app
RUN npm install
CMD ["sh", "-c", "npm run start:bot"]
+53
View File
@@ -0,0 +1,53 @@
# Jarvis Discord Bot
## What is Jarvis Discord Bot?
Using Jarvis Discord Bot you can interact with Jarvis through Discord app on various devices and share him with your friends. In the early versions of Jarvis, you could only interact with Jarvis through a simple web page. This limited your interaction with Jarvis to computers and browsers, and you had to be on the same local network.
## How to setup
### Creating a Discord App and Bot Account
- Register a Discord account if you don't have one yet.
- Login to the Discord website https://discord.com/.
- Go to the Discord Developer Portal https://discord.com/developers/applications/.
- Click the `New Application` button in the top right corner.
- Give your application a name, and select `Create`.
- From the left-hand menu, select the `Bot` option, then configure your bot's name and icon.
- Turn the `Public Bot` switch to `On`.
- Turn the `PRESENCE INTENT` switch to `On`.
- Turn the `MESSAGE CONTENT INTENT` switch to `On`.
- Copy the `Token` string, you will need it later.
### Adding the Bot to a Server
- From the left-hand menu, select `OAuth2`.
- Copy the `CLIENT ID` string, you will need it later.
- Click on `URL Generator`, Under `Scopes`, check the `bot` box.
- Configure the `Bot Permissions` as required, ensuring that at least `Send Messages` and `Use Slash Commands` are checked.
- Copy and paste the auto-generated URL into your web browser.
- Select the server you want to add the bot to.
- Verify yourself with `I am not a robot`.
- Click `Authorize` to complete the process of adding the bot to the server.
### Running the bot
- cd to the root directory of the bot(which contains this README.md file).
- Before the first running, you need to configure some parameters:
- copy the `.env.template` file to `.env.local`.
- Copy and paste the previously obtained `Token` and `CLIENT ID` into the `.env.local` file.
- run `npm install`.
- run `npm run start:bot`.
NOTE: You need to set `JARVIS_SERVER_MODE=false` and `JARVIS_BOT_SERVER_URL="http://localhost:10000"` in the Jarvis's `.env` file
### Running the bot in docker
```
# build docker image
docker build -t jarvis-discord-bot .
docker run -d --name jarvis-discord-bot \
-e CLIENT_ID='Your Client ID' \
-e BOT_TOKEN='Your Bot Token' \
-e WEBSOCKET_PORT=10000 \
jarvis-discord-bot:latest
```
+2647
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "opendan-jarvis-discord-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start:bot": "cross-env ts-node --esm ./src/bot/index.mts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.17",
"@types/lodash": "^4.14.195",
"cross-env": "^7.0.3",
"ts-node": "^10.9.1"
},
"dependencies": {
"base64-js": "^1.5.1",
"discord.js": "^14.11.0",
"dotenv": "^16.1.4",
"express": "^4.18.2",
"lodash": "^4.17.21",
"nanoid": "^4.0.2",
"socket.io": "^4.6.2"
}
}
+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
}
}
+24
View File
@@ -0,0 +1,24 @@
{
// This is an alias to @tsconfig/node16: https://github.com/tsconfig/bases
"extends": "ts-node/node16/tsconfig.json",
// Most ts-node options can be specified here using their programmatic names.
"ts-node": {
// It is faster to skip typechecking.
// Remove if you want ts-node to do typechecking.
"transpileOnly": true,
"files": true,
"compilerOptions": {
// compilerOptions specified here will override those declared below,
// but *only* in ts-node. Useful if you want ts-node and tsc to use
// different options with a single tsconfig.json.
"module": "ES6",
"target": "ES2022"
}
},
"compilerOptions": {
// typescript options here
}
}