diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b816ab1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# npm stuff +/node_modules/ +/package-lock.json +/dist diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e69de29 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c30e44c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## v1.0.0 + +- Initial + diff --git a/README.md b/README.md index 39c9f8c..ec47f0e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# ClusterChatSync -Clusterio plugin - One way discord chat sync +Cluster Chat Sync +======================== + +This is a custom chat sync, one-way only at this time. diff --git a/controller.js b/controller.js new file mode 100644 index 0000000..83cfb0c --- /dev/null +++ b/controller.js @@ -0,0 +1,102 @@ +"use strict"; +const Discord = require("discord.js"); +const { BaseControllerPlugin } = require("@clusterio/controller"); +const { InstanceActionEvent } = require("./info.js"); + +class ControllerPlugin extends BaseControllerPlugin { + async init() { + this.controller.config.on("fieldChanged", (field, curr, prev) => { + if (field === "chat_sync.discord_bot_token") { + this.connect().catch(err => { this.logger.error(`Unexpected error:\n${err.stack}`); }); + } + }); + + this.controller.handle(InstanceActionEvent, this.handleInstanceAction.bind(this)); + this.client = null; + await this.connect(); + } + + async connect() { + if (this.client) { + this.client.destroy(); + this.client = null; + } + + let token = this.controller.config.get("chat_sync.discord_bot_token"); + + if (!token) { + this.logger.warn("chat sync bot token not configured, so chat is offline"); + return; + } + + this.client = new Discord.Client({ + intents: [ + Discord.GatewayIntentBits.Guilds, + Discord.GatewayIntentBits.GuildMessages, + Discord.GatewayIntentBits.MessageContent, + ], + }); + + this.logger.info("chat sync is logging in to Discord"); + + try { + await this.client.login(this.controller.config.get("chat_sync.discord_bot_token")); + } catch (err) { + this.logger.error(`chat sync have error logging in to discord, chat is offline:\n${err.stack}`); + this.client.destroy(); + this.client = null; + return; + } + + this.logger.info("chat sync have successfully logged in"); + } + + async onShutdown() { + if (this.client) { + this.client.destroy(); + this.client = null; + } + } + + async handleInstanceAction(request, src) { + if (request.action === "CHAT" || request.action === "SHOUT") { + const channel_id = this.controller.config.get("chat_sync.discord_channel_mapping")[this.controller.instances.get(src.id) ?? ""]; + let channel = null; + + if (!channel_id) { + return; + } + + try { + channel = await this.client.channels.fetch(channel_id); + } catch (err) { + if (err.code !== 10003) { + throw err; + } + } + + if (channel === null) { + this.logger.error(`chat sync discord hannel ID ${channel_id} was not found`); + return; + } + + const nrc = request.content.replace(/\[special-item=.*?\]/g, '').replace(/<@/g, '<@\u200c>'); + const nrc_index = nrc.indexOf(":"); + const nrc_username = nrc.substring(0, nrc_index); + const nrc_message = nrc.substring(nrc_index + 1).trim(); + const nrc_msg = `**\`${nrc_username}\`** ${nrc_message}` + + if (this.controller.config.get("chat_sync.datetime_on_message")) { + let now = new Date(); + let dt = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`; + await channel.send(`${dt} ${nrc_msg}`, { allowedMentions: { parse: [] }}); + } else { + await channel.send(nrc_msg, { allowedMentions: { parse: [] }}); + } + } + } +} + +module.exports = { + ControllerPlugin +}; diff --git a/info.js b/info.js new file mode 100644 index 0000000..0fe6719 --- /dev/null +++ b/info.js @@ -0,0 +1,70 @@ +"use strict"; +const lib = require("@clusterio/lib"); + +class InstanceActionEvent { + static type = "event"; + static src = "instance"; + static dst = "controller"; + static plugin = "chat_sync"; + + constructor(instanceName, action, content) { + this.instanceName = instanceName; + this.action = action; + this.content = content; + } + + static jsonSchema = { + type: "object", + required: ["instanceName", "action", "content"], + properties: { + "instanceName": { type: "string" }, + "action": { type: "string" }, + "content": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceName, json.action, json.content); + } +} + +const plugin = { + name: "chat_sync", + title: "Chat Sync", + description: "One way chat sync.", + instanceEntrypoint: "instance", + controllerEntrypoint: "controller", + controllerConfigFields: { + "chat_sync.discord_bot_token": { + title: "Discord Bot Token", + description: "API Token", + type: "string", + optional: true, + }, + "chat_sync.datetime_on_message": { + title: "Message Datetime", + description: "Append datetime in front", + type: "boolean", + initialValue: true, + optional: true, + }, + "chat_sync.discord_channel_mapping": { + title: "Channels", + description: "Putting the discord channel id and instance relations here", + type: "object", + initialValue: { + "S1": "123", + "S2": "123", + }, + }, + }, + + messages: [ + InstanceActionEvent + ], +}; + +module.exports = { + plugin, + InstanceActionEvent +}; diff --git a/instance.js b/instance.js new file mode 100644 index 0000000..93f6749 --- /dev/null +++ b/instance.js @@ -0,0 +1,19 @@ +"use strict"; +const lib = require("@clusterio/lib"); +const { BaseInstancePlugin } = require("@clusterio/host"); + +class InstancePlugin extends BaseInstancePlugin { + async init() { + this.messageQueue = []; + } + + async onOutput(output) { + if (output.type == "action") { + this.messageQueue.push([output.action, output.message]); + } + } +} + +module.exports = { + InstancePlugin +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0b75cfe --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "@phidias0303/chat_sync", + "version": "1.0.0", + "description": "One way chat sync", + "main": "info.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "prepare": "webpack-cli --env production" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/PHIDIAS0303" + }, + "keywords": [ + "clusterio", + "factorio", + "discord" + ], + "author": "PHIDIAS ", + "license": "MIT", + "bugs": { + "url": "https://github.com/PHIDIAS0303/issues" + }, + "homepage": "https://github.com/PHIDIAS0303#readme", + "peerDependencies": { + "@clusterio/lib": "^2.0.0-alpha.14" + }, + "devDependencies": { + "@clusterio/lib": "^2.0.0-alpha.14", + "@clusterio/web_ui": "^2.0.0-alpha.14", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-merge": "^5.9.0" + }, + "dependencies": { + "discord.js": "^14.14.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/web/index.jsx b/web/index.jsx new file mode 100644 index 0000000..e69de29 diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..d0c5063 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,28 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/web_ui/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + context: __dirname, + entry: "./web/index.jsx", + output: { + path: path.resolve(__dirname, "dist", "web"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "chat_sync", + library: { type: "var", name: "plugin_chat_sync" }, + exposes: { + "./": "./info.js", + "./package.json": "./package.json", + }, + shared: { + "@clusterio/lib": { import: false }, + "@clusterio/web_ui": { import: false }, + }, + }), + ], +});