Adjust the directory structure to prepare for merging into Master.
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
*.log
|
||||
.env.local
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"semi": false,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM node:19-alpine
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN npm install
|
||||
CMD ["sh", "-c", "npm run start:bot"]
|
||||
@@ -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
|
||||
```
|
||||
Generated
+2647
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user