diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..72a8aae --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +FROM mcr.microsoft.com/devcontainers/javascript-node:0-18 +ARG MONGO_TOOLS_VERSION=6.0 +RUN . /etc/os-release \ + && curl -sSL "https://www.mongodb.org/static/pgp/server-${MONGO_TOOLS_VERSION}.asc" | gpg --dearmor > /usr/share/keyrings/mongodb-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/debian ${VERSION_CODENAME}/mongodb-org/${MONGO_TOOLS_VERSION} main" | tee /etc/apt/sources.list.d/mongodb-org-${MONGO_TOOLS_VERSION}.list \ + && apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get install -y mongodb-mongosh \ + && if [ "$(dpkg --print-architecture)" = "amd64" ]; then apt-get install -y mongodb-database-tools; fi \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b45b7b6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,15 @@ +{ + "name": "Node.js & Mongo DB", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "customizations": { + "vscode": { + "extensions": [ + "mongodb.mongodb-vscode" + ] + } + }, + "forwardPorts": [3000, 3500, 8081], + "postCreateCommand": "npm install" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..9643ab4 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: local-app + volumes: + - ../..:/workspaces:cached + command: sleep infinity + network_mode: service:mongo + + mongo: + image: mongo:latest + container_name: local-mongo + restart: unless-stopped + + express: + image: mongo-express:latest + container_name: local-express + restart: unless-stopped + environment: + ME_CONFIG_BASICAUTH_USERNAME: developer + ME_CONFIG_BASICAUTH_PASSWORD: developer + ME_CONFIG_MONGODB_SERVER: local-mongo + network_mode: service:mongo diff --git a/.env.sample b/.env.sample index bec9142..e16fad7 100644 --- a/.env.sample +++ b/.env.sample @@ -1,13 +1,19 @@ +# node env ('production' or 'development') +NODE_ENV="development" + +# general +MONGODB_URI="mongodb://local-mongo:27017/app" +GOOGLE_AI_API_KEY="your_google_ai_api_key" + +# discord integration DISCORD_APP_ID="00000" DISCORD_BOT_TOKEN="your_discord_bot_token" - DISCORD_GUILD_ID="00000" -DISCORD_ROLE_ID_ADMIN="00000" -DISCORD_CHANNEL_ID_TERMINAL="00000" +DISCORD_GUILD_ROLE_ID_ADMIN="00000" +DISCORD_GUILD_CHANNEL_ID_TERMINAL="00000" +# matrix integration MATRIX_HOMESERVER="https://matrix.org" MATRIX_USERNAME="your_matrix_username" # e.g. @nymph57021218:matrix.org MATRIX_PASSWORD="your_matrix_password" MATRIX_ACCESS_TOKEN="your_matrix_access_token" - -GOOGLE_AI_API_KEY="your_google_ai_api_key" diff --git a/.gitignore b/.gitignore index 08482d8..4a2fab2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ prompts.json relay.json roles.json +# App data files +data/ + # Finder config file .DS_Store diff --git a/app.js b/app.js index 9bdc1bd..1c0966d 100644 --- a/app.js +++ b/app.js @@ -1,20 +1,30 @@ "use strict"; -require("dotenv").config(); +const { + runLoader, + getMust, +} = require("./src/config"); +const { + prepare, +} = require("./src/clients/database"); -const runners = []; +runLoader(); -if (process.env.MATRIX_ACCESS_TOKEN) { +const runners = []; +if (getMust("MATRIX_ACCESS_TOKEN")) { runners.push(require("./src/matrix")); } -if (process.env.DISCORD_BOT_TOKEN) { +if (getMust("DISCORD_BOT_TOKEN")) { runners.push(require("./src/discord")); } -Promise.all(runners.map( - (runner) => runner(), -)).then(() => { - console.info("Nymph 系統 成功啟動"); -}).catch((e) => { - console.error("Nymph 系統 啟動失敗:", e); -}); +(async () => { + await prepare(); + await Promise.all(runners.map( + (runner) => runner(), + )).then(() => { + console.info("Nymph 系統 成功啟動"); + }).catch((e) => { + console.error("Nymph 系統 啟動失敗:", e); + }); +})(); diff --git a/matrix_access.js b/matrix_access.js index 725794c..e69de29 100644 --- a/matrix_access.js +++ b/matrix_access.js @@ -1,33 +0,0 @@ -"use strict"; - -require("dotenv").config(); - -const { - createClient, -} = require("matrix-js-sdk"); - -const { - MATRIX_HOMESERVER: homeserverUrl, - MATRIX_USERNAME: username, - MATRIX_PASSWORD: password, -} = process.env; - -console.info( - "Get the AccessToken from Matrix", - `(homeserver: ${homeserverUrl || "undefined"})`, - "\n", -); - -const client = createClient({baseUrl: homeserverUrl}); -client.login("m.login.password", { - identifier: { - type: "m.id.user", - user: username, - }, - password, -}).then((response) => { - console.info("UserId:", response.user_id); - console.info("AccessToken:", response.access_token); -}).catch((e) => { - console.error("Unauthorized", e); -}); diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..171b378 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,5 @@ +{ + "ignore": [ + "data" + ] +} diff --git a/package.json b/package.json index c3631ef..964e15b 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,15 @@ "dependencies": { "@discordjs/rest": "^2.2.0", "@google/generative-ai": "^0.2.1", + "@matrix-org/matrix-sdk-crypto-nodejs": "^0.1.0-beta.12", "discord-api-types": "^0.37.65", "discord.js": "^14.14.1", "discordjs-reaction-role": "^3.1.0", "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "matrix-js-sdk": "^32.1.0" + "matrix-bot-sdk": "^0.7.1", + "mongoose": "^8.3.4" }, "devDependencies": { "@commitlint/cli": "^17.6.5", diff --git a/register_commands.js b/register_commands.js index 1e5a46f..8ae0abd 100644 --- a/register_commands.js +++ b/register_commands.js @@ -1,6 +1,8 @@ "use strict"; -require("dotenv").config(); +const { + getMust, +} = require("./src/config"); const {useRestClient} = require("./src/clients/discord"); const {Routes} = require("discord-api-types/v9"); @@ -30,8 +32,8 @@ console.info(commands); await client.put( Routes.applicationGuildCommands( - process.env.DISCORD_APP_ID, - process.env.GUILD_ID, + getMust("DISCORD_APP_ID"), + getMust("DISCORD_GUILD_ID"), ), {body: commands}, ); diff --git a/relay.sample.json b/relay.sample.json index 74dbeff..0a3fff2 100644 --- a/relay.sample.json +++ b/relay.sample.json @@ -2,6 +2,6 @@ { "name": "Arona", "discordChannelId": "996069746514612235", - "matrixRoomId": "#line:matrix.org" + "matrixRoomId": "!yTRzUDNyviBhrMwYGG:matrix.org" } ] \ No newline at end of file diff --git a/src/bridges/discord_matrix.js b/src/bridges/discord_matrix.js index ea5b35e..4c8222c 100644 --- a/src/bridges/discord_matrix.js +++ b/src/bridges/discord_matrix.js @@ -3,10 +3,6 @@ const discord = require("discord.js"); -const { - MsgType, -} = require("matrix-js-sdk"); - const { useClient, } = require("../clients/matrix"); @@ -15,8 +11,6 @@ const { find, } = require("./utils"); -const client = useClient(); - /** * This function is called when a message is created in Discord. * @@ -24,6 +18,8 @@ const client = useClient(); * @return {void} */ module.exports = async (message) => { + const client = await useClient(); + const {channelId} = message; const relayTarget = find("discordChannelId", channelId); @@ -38,7 +34,7 @@ module.exports = async (message) => { const {content: text} = message; await client.sendMessage(roomId, { - msgtype: MsgType.Text, + msgtype: "m.text", format: "plain/text", body: `${username} ⬗ Discord\n${text}`, }); diff --git a/src/bridges/matrix_discord.js b/src/bridges/matrix_discord.js index 4e0e05b..3f1ff7d 100644 --- a/src/bridges/matrix_discord.js +++ b/src/bridges/matrix_discord.js @@ -1,11 +1,6 @@ "use strict"; // Trasnfer messages from Discord to Matrix. -const { - RoomEvent, - Room, -} = require("matrix-js-sdk"); - const { useClient, } = require("../clients/discord"); @@ -14,18 +9,15 @@ const { find, } = require("./utils"); -const client = useClient(); - /** * This function is called when a new event is added to the timeline of a room. * - * @param {RoomEvent} roomEvent - The event that triggered this function. - * @param {Room} room - The room that the event was sent in. - * @param {Date} _toStartOfTimeline - The date of the first event. + * @param {string} roomId - The room that the event was sent in. + * @param {any} event - The event that triggered this function. * @return {void} */ -module.exports = async (roomEvent, room, _toStartOfTimeline) => { - const {roomId} = room; +module.exports = async (roomId, event) => { + const client = await useClient(); const relayTarget = find("matrixRoomId", roomId); if (!relayTarget) { @@ -35,8 +27,8 @@ module.exports = async (roomEvent, room, _toStartOfTimeline) => { discordChannelId: channelId, } = relayTarget; - const {sender: username} = roomEvent.event; - const {body: text} = roomEvent.event.content; + const {sender: username, content} = event; + const {body: text} = content; const channel = await client.channels.fetch(channelId); await channel.send(`\`${username}\` ⬗ Matrix\n${text}`); diff --git a/src/clients/database.js b/src/clients/database.js new file mode 100644 index 0000000..4ef6494 --- /dev/null +++ b/src/clients/database.js @@ -0,0 +1,18 @@ +"use strict"; +// mongoose is an ODM library for MongoDB. + +// Import config +const {getMust} = require("../config"); + +// Import mongoose +const database = require("mongoose"); + +// Configure mongose +database.set("strictQuery", true); + +// Connect to MongoDB +exports.prepare = () => + database.connect(getMust("MONGODB_URI")); + +// Export as useFunction +exports.useDatabase = () => database; diff --git a/src/clients/discord.js b/src/clients/discord.js index 5bd433f..a10472a 100644 --- a/src/clients/discord.js +++ b/src/clients/discord.js @@ -1,6 +1,10 @@ "use strict"; // Discord is a proprietary instant messaging platform. +const { + getMust, +} = require("../config"); + const { REST, } = require("@discordjs/rest"); @@ -11,7 +15,9 @@ const { GatewayIntentBits, } = require("discord.js"); -const newClient = () => { +const botToken = getMust("DISCORD_BOT_TOKEN"); + +const newClient = async () => { const client = new Client({ partials: [ Partials.Channel, @@ -28,10 +34,14 @@ const newClient = () => { GatewayIntentBits.MessageContent, ], }); - client.login(process.env.DISCORD_BOT_TOKEN); + client.login(botToken); return client; }; +/** + * The cached client. + * @type {Client|undefined} + */ let client; /** * Use Discord client @@ -39,17 +49,17 @@ let client; * @param {boolean} cached - Use the cached client * @return {Client} - The client */ -exports.useClient = (cached = true) => { +exports.useClient = async (cached = true) => { if (cached && client) { return client; } - client = newClient(); + client = await newClient(); return client; }; exports.useRestClient = () => { const restClient = new REST({version: "10"}); - restClient.setToken(process.env.DISCORD_BOT_TOKEN); + restClient.setToken(botToken); return restClient; }; diff --git a/src/clients/gemini.js b/src/clients/gemini.js index 9e94835..2c8da40 100644 --- a/src/clients/gemini.js +++ b/src/clients/gemini.js @@ -1,9 +1,10 @@ "use strict"; // Gemini is a generative AI model developed by Google. +const {getMust} = require("../config"); const {GoogleGenerativeAI} = require("@google/generative-ai"); -const client = new GoogleGenerativeAI(process.env.GOOGLE_AI_API_KEY); +const client = new GoogleGenerativeAI(getMust("GOOGLE_AI_API_KEY")); const model = client.getGenerativeModel({model: "gemini-pro"}); const chatSessions = {}; diff --git a/src/clients/matrix.js b/src/clients/matrix.js index 431eb9e..1faae1c 100644 --- a/src/clients/matrix.js +++ b/src/clients/matrix.js @@ -2,36 +2,94 @@ // Matrix is an opensource instant messaging platform. const { - createClient, + getMust, +} = require("../config"); + +const { + AutojoinRoomsMixin, + MatrixAuth, MatrixClient, -} = require("matrix-js-sdk"); + RustSdkCryptoStorageProvider, + SimpleFsStorageProvider, +} = require("matrix-bot-sdk"); const { - MATRIX_HOMESERVER: homeserverUrl, - MATRIX_USERNAME: username, - MATRIX_ACCESS_TOKEN: accessToken, -} = process.env; - -const newClient = () => { - const client = createClient({ - baseUrl: homeserverUrl, - userId: username, + StoreType, +} = require("@matrix-org/matrix-sdk-crypto-nodejs"); + +const MatrixAccess = require("../models/matrix_access"); + +const homeserverUrl = getMust("MATRIX_HOMESERVER"); +const username = getMust("MATRIX_USERNAME"); +const password = getMust("MATRIX_PASSWORD"); + +const deviceName = "Nymph"; + +const storage = new SimpleFsStorageProvider( + "data/storage.json", +); +const crypto = new RustSdkCryptoStorageProvider( + "data/crypto", + StoreType.Sled, +); + +const newClient = async () => { + const matrixData = await MatrixAccess.findOne({username}); + if (matrixData) { + const {accessToken} = matrixData; + return new MatrixClient( + homeserverUrl, + accessToken, + storage, + crypto, + ); + } + + const auth = new MatrixAuth(homeserverUrl); + const { + accessToken, + } = await auth.passwordLogin( + username, + password, + deviceName, + ); + + const newAccess = new MatrixAccess({ + username, accessToken, }); - return client; + await newAccess.save(); + + return new MatrixClient( + homeserverUrl, + accessToken, + storage, + crypto, + ); }; +/** + * The cached client. + * @type {MatrixClient|undefined} + */ let client; /** * Use Matrix client * * @param {boolean} cached - Use the cached client - * @return {MatrixClient} - The client + * @return {Promise} - The client */ -exports.useClient = (cached = true) => { +exports.useClient = async (cached = true) => { if (cached && client) { return client; } - client = newClient(); + client = await newClient(); return client; }; + +exports.startSync = async () => { + const joinedRooms = await client.getJoinedRooms(); + await client.crypto.prepare(joinedRooms); + AutojoinRoomsMixin.setupOnClient(client); + await client.start(); +}; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..12230a8 --- /dev/null +++ b/src/config.js @@ -0,0 +1,125 @@ +"use strict"; + +// Import modules +const {join: pathJoin} = require("node:path"); +const {existsSync} = require("node:fs"); + +/** + * Load configs from system environment variables. + */ +function runLoader() { + const dotenvPath = pathJoin(__dirname, "..", ".env"); + + const isDotEnvFileExists = existsSync(dotenvPath); + const isCustomDefined = get("APP_CONFIGURED") === "1"; + + if (!isDotEnvFileExists && !isCustomDefined) { + console.error( + "No '.env' file detected in app root.", + "If you're not using dotenv file,", + "set 'APP_CONFIGURED=1' into environment variables.", + "\n", + ); + throw new Error(".env not exists"); + } + + require("dotenv").config(); +} + +/** + * Check is production mode. + * @module config + * @function + * @return {boolean} true if production + */ +function isProduction() { + return getMust("NODE_ENV") === "production"; +} + +/** + * Get environment overview. + * @module config + * @function + * @return {object} + */ +function getEnvironmentOverview() { + return { + node: getFallback("NODE_ENV", "development"), + runtime: getFallback("RUNTIME_ENV", "native"), + }; +} + +/** + * Shortcut to get config value. + * @module config + * @function + * @param {string} key the key + * @return {string} the value + */ +function get(key) { + return process.env[key]; +} + +/** + * Get the bool value from config, if yes, returns true. + * @module config + * @function + * @param {string} key the key + * @return {boolean} the bool value + */ +function getEnabled(key) { + return getMust(key) === "yes"; +} + +/** + * Get the array value from config. + * @module config + * @function + * @param {string} key the key + * @param {string} separator [separator=,] the separator. + * @return {string[]} the array value + */ +function getSplited(key, separator=",") { + return getMust(key). + split(separator). + map((s) => s.trim()); +} + +/** + * Get the value from config with error thrown. + * @module config + * @function + * @param {string} key the key + * @return {string} the expected value + * @throws {Error} if value is undefined, throw an error + */ +function getMust(key) { + const value = get(key); + if (value === undefined) { + throw new Error(`config key ${key} is undefined`); + } + return value; +} + +/** + * Get the value from config with fallback. + * @module config + * @function + * @param {string} key the key + * @param {string} fallback the fallback value + * @return {string} the expected value + */ +function getFallback(key, fallback) { + return get(key) || fallback; +} + +module.exports = { + runLoader, + isProduction, + getEnvironmentOverview, + get, + getEnabled, + getSplited, + getMust, + getFallback, +}; diff --git a/src/discord.js b/src/discord.js index 482d8db..4a940de 100644 --- a/src/discord.js +++ b/src/discord.js @@ -15,7 +15,7 @@ const { } = require("./triggers/discord"); module.exports = async () => { - const client = useClient(); + const client = await useClient(); client.on(Events.ClientReady, () => { const showStartupMessage = async () => { diff --git a/src/matrix.js b/src/matrix.js index df0a17f..3830129 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -2,6 +2,7 @@ const { useClient, + startSync, } = require("./clients/matrix"); const { @@ -9,14 +10,14 @@ const { } = require("./triggers/matrix"); module.exports = async () => { - const client = useClient(); - await client.startClient({initialSyncLimit: 0}); + const client = await useClient(); - const showStartupMessage = () => { - const userId = client.getUserId(); + const showStartupMessage = async () => { + const userId = await client.getUserId(); console.info(`Matrix 身份:${userId}`); }; - showStartupMessage(); - startListen(); + await showStartupMessage(); + await startListen(); + await startSync(); }; diff --git a/src/models/matrix_access.js b/src/models/matrix_access.js new file mode 100644 index 0000000..08f9b9d --- /dev/null +++ b/src/models/matrix_access.js @@ -0,0 +1,7 @@ +"use strict"; + +const {useDatabase} = require("../clients/database"); +const database = useDatabase(); + +const schemaMatrixData = require("../schemas/matrix_access"); +module.exports = database.model("MatrixAccess", schemaMatrixData); diff --git a/src/schemas/matrix_access.js b/src/schemas/matrix_access.js new file mode 100644 index 0000000..09dedf7 --- /dev/null +++ b/src/schemas/matrix_access.js @@ -0,0 +1,15 @@ +"use strict"; + +const mongoose = require("mongoose"); +const {Schema} = mongoose; + +module.exports = new Schema({ + username: { + type: String, + required: true, + }, + accessToken: { + type: String, + required: true, + }, +}); diff --git a/src/triggers/discord/index.js b/src/triggers/discord/index.js index 7fb159c..af901f0 100644 --- a/src/triggers/discord/index.js +++ b/src/triggers/discord/index.js @@ -4,8 +4,8 @@ const { useClient, } = require("../../clients/discord"); -exports.startListen = () => { - const client = useClient(); +exports.startListen = async () => { + const client = await useClient(); const triggers = { interactionCreate: require("./interaction_create"), diff --git a/src/triggers/discord/interaction_create/terminal.js b/src/triggers/discord/interaction_create/commands.js similarity index 100% rename from src/triggers/discord/interaction_create/terminal.js rename to src/triggers/discord/interaction_create/commands.js diff --git a/src/triggers/discord/interaction_create/index.js b/src/triggers/discord/interaction_create/index.js index dc44616..fa491c1 100644 --- a/src/triggers/discord/interaction_create/index.js +++ b/src/triggers/discord/interaction_create/index.js @@ -17,10 +17,7 @@ const snakeToCamelCase = (str) => module.exports = async (interaction) => { if (!interaction.isCommand()) return; - let commands = {}; - if (interaction.channel.id === process.env.CHANNEL_ID_TERMINAL) { - commands = {...commands, ...require("./terminal")}; - } + const commands = require("./commands"); const actionName = snakeToCamelCase(interaction.commandName); if (actionName in commands) { diff --git a/src/triggers/discord/message_create/index.js b/src/triggers/discord/message_create/index.js index eb0f59c..b3138b6 100644 --- a/src/triggers/discord/message_create/index.js +++ b/src/triggers/discord/message_create/index.js @@ -2,14 +2,16 @@ const discord = require("discord.js"); -const discordToMatrix = require("../../../bridges/discord_matrix"); +const { + useClient, +} = require("../../../clients/discord"); +const { + usePrompts, +} = require("../../../clients/gemini"); -const {useClient} = require("../../../clients/discord"); -const {usePrompts} = require("../../../clients/gemini"); +const discordToMatrix = require("../../../bridges/discord_matrix"); const prompts = require("../../../../prompts.json"); - -const client = useClient(); const useChatSession = usePrompts(prompts); /** @@ -17,16 +19,15 @@ const useChatSession = usePrompts(prompts); * @return {void} */ module.exports = async (message) => { + const client = await useClient(); + if (message.author.bot) { return; } discordToMatrix(message); - if ( - message.channel.id !== process.env.CHANNEL_ID_TERMINAL && - !message.mentions.users.has(client.user.id) - ) { + if (!message.mentions.users.has(client.user.id)) { return; } diff --git a/src/triggers/matrix/index.js b/src/triggers/matrix/index.js index 3437b61..d05f3f4 100644 --- a/src/triggers/matrix/index.js +++ b/src/triggers/matrix/index.js @@ -1,18 +1,15 @@ "use strict"; -const { - RoomEvent, -} = require("matrix-js-sdk"); - const { useClient, } = require("../../clients/matrix"); -exports.startListen = () => { - const client = useClient(); +exports.startListen = async () => { + const client = await useClient(); const triggers = { - [RoomEvent.Timeline]: require("./room/timeline"), + "room.failed_decryption": require("./room/failed_decryption"), + "room.message": require("./room/message"), }; for (const [key, trigger] of Object.entries(triggers)) { client.on(key, trigger); diff --git a/src/triggers/matrix/room/failed_decryption.js b/src/triggers/matrix/room/failed_decryption.js new file mode 100644 index 0000000..3ed8843 --- /dev/null +++ b/src/triggers/matrix/room/failed_decryption.js @@ -0,0 +1,16 @@ +"use strict"; + +/** + * This function is called when a new event is added to the timeline of a room. + * + * @param {string} roomId - The room that the event was sent in. + * @param {any} event - The event that triggered this function. + * @param {Error} error - The error that triggered this function. + * @return {void} + */ +module.exports = async (roomId, event, error) => { + console.error( + `Failed to decrypt ${roomId} ${event["event_id"]} because `, + error, + ); +}; diff --git a/src/triggers/matrix/room/timeline.js b/src/triggers/matrix/room/message.js similarity index 57% rename from src/triggers/matrix/room/timeline.js rename to src/triggers/matrix/room/message.js index 21e232a..c171a61 100644 --- a/src/triggers/matrix/room/timeline.js +++ b/src/triggers/matrix/room/message.js @@ -1,7 +1,5 @@ "use strict"; -const {Room, RoomEvent, MsgType} = require("matrix-js-sdk"); - const matrixToDiscord = require("../../../bridges/matrix_discord"); const {useClient} = require("../../../clients/matrix"); @@ -9,29 +7,33 @@ const {usePrompts} = require("../../../clients/gemini"); const prompts = require("../../../../prompts.json"); -const client = useClient(); const useChatSession = usePrompts(prompts); const prefix = "Nymph "; /** * This function is called when a new event is added to the timeline of a room. * - * @param {RoomEvent} roomEvent - The event that triggered this function. - * @param {Room} room - The room that the event was sent in. - * @param {Date} toStartOfTimeline - The date of the first event. + * @param {string} roomId - The room that the event was sent in. + * @param {any} event - The event that triggered this function. * @return {void} */ -module.exports = async (roomEvent, room, toStartOfTimeline) => { - if ( - roomEvent.getType() !== "m.room.message" || - roomEvent.event.sender === client.getUserId() - ) { +module.exports = async (roomId, event) => { + const client = await useClient(); + + const { + event_id: eventId, + sender: senderId, + } = event; + + if (senderId === await client.getUserId()) { return; } - matrixToDiscord(roomEvent, room, toStartOfTimeline); + await client.sendReadReceipt(roomId, eventId); + + matrixToDiscord(roomId, event); - let requestContent = roomEvent.event.content.body; + let requestContent = event.content.body; if (!requestContent.startsWith(prefix)) { return; @@ -39,22 +41,22 @@ module.exports = async (roomEvent, room, toStartOfTimeline) => { requestContent = requestContent.slice(prefix.length).trim(); if (!requestContent) { - await client.sendMessage(room.roomId, { - msgtype: MsgType.Text, + await client.sendMessage(roomId, { + msgtype: "m.text", format: "plain/text", body: "所收到的訊息意圖不明。", }); return; } - const chatSession = useChatSession(room.roomId); + const chatSession = useChatSession(roomId); let result; try { result = await chatSession.sendMessage(requestContent); } catch (error) { console.error(error); - await client.sendMessage(room.roomId, { - msgtype: MsgType.Text, + await client.sendMessage(roomId, { + msgtype: "m.text", format: "plain/text", body: "思緒混亂,無法回覆。", }); @@ -63,16 +65,16 @@ module.exports = async (roomEvent, room, toStartOfTimeline) => { const responseContent = result.response.text().trim(); if (!responseContent) { - await client.sendMessage(room.roomId, { - msgtype: MsgType.Text, + await client.sendMessage(roomId, { + msgtype: "m.text", format: "plain/text", body: "無法正常回覆,請換個說法試試。", }); return; } - await client.sendMessage(room.roomId, { - msgtype: MsgType.Text, + await client.sendMessage(roomId, { + msgtype: "m.text", format: "plain/text", body: responseContent, });