mirror of
https://github.com/PHIDIAS0303/ClusterChatSync.git
synced 2026-11-11 15:14:52 +09:00
.
This commit is contained in:
1 parent
ef5d18a171
commit
b56904e8ae
13 files changed
+296
-222
No files matched your search
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.3
|
||||
|
||||
- Changed to TS
|
||||
|
||||
## v1.0.0
|
||||
|
||||
- Initial
|
||||
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
'use strict';
|
||||
const Discord = require('discord.js');
|
||||
const lib = require('@clusterio/lib');
|
||||
const {BaseControllerPlugin} = require('@clusterio/controller');
|
||||
const {InstanceActionEvent} = require('./index.js');
|
||||
|
||||
const MAX_DISCORD_MESSAGE_LENGTH = 1950;
|
||||
|
||||
class ControllerPlugin extends BaseControllerPlugin {
|
||||
async init() {
|
||||
this.controller.config.on('fieldChanged', (field, curr, prev) => {
|
||||
if (field === 'ClusterChatSync.discord_bot_token') {
|
||||
this.connect().catch(err => {
|
||||
this.logger.error(`[Chat Sync] Discord bot token:\n${err.stack}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
this.controller.handle(InstanceActionEvent, this.handleInstanceAction.bind(this));
|
||||
this.client = null;
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
async clientDestroy() {
|
||||
if (this.client) {
|
||||
this.client.destroy();
|
||||
this.client = null;
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
await this.clientDestroy();
|
||||
|
||||
if (!this.controller.config.get('ClusterChatSync.discord_bot_token')) {
|
||||
this.logger.error('[Chat Sync] Discord bot token not configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.client = new Discord.Client({intents: [Discord.GatewayIntentBits.Guilds, Discord.GatewayIntentBits.GuildMessages, Discord.GatewayIntentBits.MessageContent]});
|
||||
this.logger.info('[Chat Sync] Logging into Discord.');
|
||||
|
||||
try {
|
||||
await this.client.login(this.controller.config.get('ClusterChatSync.discord_bot_token'));
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] Discord login error:\n${err.stack}`);
|
||||
await this.clientDestroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info('[Chat Sync] Logged in Discord successfully.');
|
||||
}
|
||||
|
||||
async onShutdown() {
|
||||
await this.clientDestroy();
|
||||
}
|
||||
|
||||
async sendMessage(request, nrc_msg) {
|
||||
const channel_id = this.controller.config.get('ClusterChatSync.discord_channel_mapping')[request.instanceName];
|
||||
if (!channel_id) return;
|
||||
let channel;
|
||||
|
||||
try {
|
||||
channel = await this.client.channels.fetch(channel_id);
|
||||
|
||||
if (channel === null) {
|
||||
this.logger.error(`[Chat Sync] Discord Channel ID ${channel_id} not found.`);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== 10003) {
|
||||
this.logger.error(`[Chat Sync] Discord channel fetch error:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.controller.config.get('ClusterChatSync.datetime_on_message')) {
|
||||
let now = new Date();
|
||||
nrc_msg = `${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')} ${nrc_msg}`
|
||||
}
|
||||
|
||||
if (nrc_msg.length <= MAX_DISCORD_MESSAGE_LENGTH) {
|
||||
await channel.send(nrc_msg, {allowedMentions: {parse: []}});
|
||||
} else {
|
||||
while (nrc_msg.length > 0) {
|
||||
let nrc_cmsg = nrc_msg.slice(0, MAX_DISCORD_MESSAGE_LENGTH);
|
||||
let nrc_lindex = nrc_cmsg.lastIndexOf(' ');
|
||||
|
||||
if (nrc_lindex !== -1) {
|
||||
nrc_cmsg = nrc_cmsg.slice(0, nrc_lindex);
|
||||
nrc_msg = nrc_msg.slice(nrc_lindex).trim();
|
||||
} else {
|
||||
nrc_msg = nrc_msg.slice(MAX_DISCORD_MESSAGE_LENGTH).trim();
|
||||
}
|
||||
|
||||
await channel.send(nrc_cmsg, {allowedMentions: {parse: []}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstanceAction(request, src) {
|
||||
if (request.action === 'CHAT' || request.action === 'SHOUT') {
|
||||
const nrc = request.content.replace(/\[special-item=.*?\]/g, '<blueprint>').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();
|
||||
await this.sendMessage(request, `**\`${nrc_username}\`**: ${nrc_message}`);
|
||||
} else if (request.action === 'JOIN') {
|
||||
await this.sendMessage(request, `**\`${nrc_username}\`** joined the game`);
|
||||
} else if (request.action === 'LEAVE') {
|
||||
await this.sendMessage(request, `**\`${nrc_username}\`** left the game`);
|
||||
} else if (request.action === 'KICK') {
|
||||
await this.sendMessage(request, `**\`${nrc_username}\`** was kicked from the game`);
|
||||
} else if (request.action === 'BAN') {
|
||||
await this.sendMessage(request, `**\`${nrc_username}\`** was banned from the game`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {ControllerPlugin};
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import type {ControllerPluginContext} from "@clusterio/controller";
|
||||
import {ChatEvent} from './index.js';
|
||||
import * as Discord from 'discord.js';
|
||||
|
||||
const MAX_DISCORD_MESSAGE_LENGTH = 1950;
|
||||
const ACTION_VERBS: Record<string, string> = {
|
||||
JOIN: 'joined the game',
|
||||
LEAVE: 'left the game',
|
||||
KICK: 'was kicked from the game',
|
||||
BAN: 'was banned from the game',
|
||||
};
|
||||
const escapeMentions = (content: string) => content
|
||||
.replace(/\[special-item=.*?\]/g, '<blueprint>')
|
||||
.replace(/<@/g, '<@\u200c>');
|
||||
|
||||
export default async function(context: ControllerPluginContext) {
|
||||
const {controller, plugin, logger} = context;
|
||||
|
||||
let client: Discord.Client | null = null;
|
||||
|
||||
const clientDestroy = async () => {
|
||||
const old = client;
|
||||
client = null;
|
||||
if (old) await old.destroy();
|
||||
};
|
||||
|
||||
const connect = async () => {
|
||||
await clientDestroy();
|
||||
|
||||
const token = controller.config.get('ClusterChatSync.discord_bot_token') as string | undefined;
|
||||
|
||||
if (!token) {
|
||||
logger.error('[Chat Sync] Discord bot token not configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
const discordClient = new Discord.Client({intents: [
|
||||
Discord.GatewayIntentBits.Guilds,
|
||||
Discord.GatewayIntentBits.GuildMessages,
|
||||
Discord.GatewayIntentBits.MessageContent,
|
||||
]});
|
||||
|
||||
client = discordClient;
|
||||
|
||||
logger.info('[Chat Sync] Logging into Discord.');
|
||||
|
||||
try {
|
||||
await discordClient.login(token);
|
||||
} catch (err) {
|
||||
logger.error(`[Chat Sync] Discord login error:\n${(err as Error).stack}`);
|
||||
await clientDestroy();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('[Chat Sync] Logged in Discord successfully.');
|
||||
};
|
||||
|
||||
const sendMessage = async (instanceName: string, message: string) => {
|
||||
const discordClient = client;
|
||||
if (!discordClient) return;
|
||||
|
||||
const mapping = controller.config.get('ClusterChatSync.discord_channel_mapping') as Record<string, string>;
|
||||
const channelId = mapping?.[instanceName];
|
||||
if (!channelId) return;
|
||||
|
||||
let channel: Discord.Channel | null;
|
||||
|
||||
try {
|
||||
channel = await discordClient.channels.fetch(channelId);
|
||||
} catch (err) {
|
||||
if ((err as {code?: number}).code !== 10003) {
|
||||
logger.error(`[Chat Sync] Discord channel fetch error:\n${(err as Error).stack}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel === null || !channel.isTextBased()) {
|
||||
logger.error(`[Chat Sync] Discord Channel ID ${channelId} is not a usable text channel.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const textChannel = channel;
|
||||
|
||||
let nrcMsg = message;
|
||||
|
||||
if (controller.config.get('ClusterChatSync.datetime_on_message')) {
|
||||
const now = new Date();
|
||||
const p = (n: number) => String(n).padStart(2, '0');
|
||||
const stamp = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}` + ` ${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
||||
nrcMsg = `${stamp} ${nrcMsg}`;
|
||||
}
|
||||
|
||||
const send = (content: string) => textChannel.send({content, allowedMentions: {parse: []}});
|
||||
|
||||
if (nrcMsg.length <= MAX_DISCORD_MESSAGE_LENGTH) {
|
||||
await send(nrcMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
let remaining = nrcMsg;
|
||||
while (remaining.length > 0) {
|
||||
let chunk = remaining.slice(0, MAX_DISCORD_MESSAGE_LENGTH);
|
||||
const lastSpace = chunk.lastIndexOf(' ');
|
||||
|
||||
if (lastSpace > 0) {
|
||||
chunk = chunk.slice(0, lastSpace);
|
||||
remaining = remaining.slice(lastSpace).trim();
|
||||
} else {
|
||||
remaining = remaining.slice(MAX_DISCORD_MESSAGE_LENGTH).trim();
|
||||
}
|
||||
|
||||
await send(chunk);
|
||||
}
|
||||
};
|
||||
|
||||
controller.handle(ChatEvent, async (request: ChatEvent) => {
|
||||
const {instanceName, action, content} = request;
|
||||
|
||||
if (action === 'CHAT' || action === 'SHOUT') {
|
||||
const nrc = escapeMentions(content);
|
||||
const index = nrc.indexOf(':');
|
||||
const username = index === -1 ? nrc.trim() : nrc.slice(0, index);
|
||||
const msg = index === -1 ? '' : nrc.slice(index + 1).trim();
|
||||
await sendMessage(instanceName, `**\`${username}\`**: ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const verb = ACTION_VERBS[action];
|
||||
|
||||
if (verb) {
|
||||
await sendMessage(instanceName, `**\`${content.trim()}\`** ${verb}`);
|
||||
}
|
||||
});
|
||||
|
||||
controller.config.on('fieldChanged', (field) => {
|
||||
if (field === 'ClusterChatSync.discord_bot_token') {
|
||||
connect().catch(err => {
|
||||
logger.error(`[Chat Sync] Discord bot token:\n${(err as Error).stack}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
controller.hooks.shutdown.attach(plugin.name, async () => {
|
||||
await clientDestroy();
|
||||
});
|
||||
|
||||
await connect();
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
'use strict';
|
||||
const lib = require('@clusterio/lib');
|
||||
|
||||
class InstanceActionEvent {
|
||||
static type = 'event';
|
||||
static src = 'instance';
|
||||
static dst = 'controller';
|
||||
static plugin = 'ClusterChatSync';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static Response = lib.JsonString;
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
name: 'ClusterChatSync',
|
||||
title: 'Cluster Chat Sync',
|
||||
description: 'One way chat sync.',
|
||||
instanceEntrypoint: './instance.js',
|
||||
controllerEntrypoint: './controller.js',
|
||||
controllerConfigFields: {
|
||||
'ClusterChatSync.discord_bot_token': {
|
||||
title: 'Discord Bot Token',
|
||||
description: 'API Token',
|
||||
type: 'string'
|
||||
},
|
||||
'ClusterChatSync.datetime_on_message': {
|
||||
title: 'Message Datetime',
|
||||
description: 'Append datetime in front',
|
||||
type: 'boolean',
|
||||
initialValue: true
|
||||
},
|
||||
'ClusterChatSync.discord_channel_mapping': {
|
||||
title: 'Channels',
|
||||
description: 'Putting the discord channel id and instance relations here',
|
||||
type: 'object',
|
||||
initialValue: {
|
||||
'S1': '123'
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: [InstanceActionEvent],
|
||||
};
|
||||
|
||||
module.exports = {plugin, InstanceActionEvent};
|
||||
@@ -0,0 +1,61 @@
|
||||
import type * as lib from '@clusterio/lib';
|
||||
import {Type, Static} from '@sinclair/typebox';
|
||||
|
||||
export class ChatEvent {
|
||||
declare ['constructor']: typeof ChatEvent;
|
||||
static type = 'event' as const;
|
||||
static src = 'instance' as const;
|
||||
static dst = 'controller' as const;
|
||||
static plugin = 'ClusterChatSync' as const;
|
||||
static permission = null;
|
||||
|
||||
constructor(
|
||||
public instanceName: string,
|
||||
public action: string,
|
||||
public content: string,
|
||||
) {
|
||||
}
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
'instanceName': Type.String(),
|
||||
'action': Type.String(),
|
||||
'content': Type.String(),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof ChatEvent.jsonSchema>) {
|
||||
return new this(json.instanceName, json.action, json.content);
|
||||
}
|
||||
}
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: 'ClusterChatSync',
|
||||
title: 'Cluster Chat Sync',
|
||||
description: 'One way chat forward to Discord.',
|
||||
webEntrypoint: './web',
|
||||
instanceEntrypoint: 'dist/node/instance.js',
|
||||
controllerEntrypoint: 'dist/node/controller.js',
|
||||
controllerConfigFields: {
|
||||
'ClusterChatSync.discord_bot_token': {
|
||||
title: 'Discord Bot Token',
|
||||
description: 'API Token',
|
||||
type: 'string'
|
||||
},
|
||||
'ClusterChatSync.datetime_on_message': {
|
||||
title: 'Message Datetime',
|
||||
description: 'Append datetime in front',
|
||||
type: 'boolean',
|
||||
initialValue: true
|
||||
},
|
||||
'ClusterChatSync.discord_channel_mapping': {
|
||||
title: 'Channels',
|
||||
description: 'Putting the discord channel id and instance relations here',
|
||||
type: 'object',
|
||||
initialValue: {
|
||||
'S1': '123'
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: [
|
||||
ChatEvent,
|
||||
],
|
||||
};
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
'use strict';
|
||||
const lib = require('@clusterio/lib');
|
||||
const {BaseInstancePlugin} = require('@clusterio/host');
|
||||
const {InstanceActionEvent} = require('./index.js');
|
||||
|
||||
class InstancePlugin extends BaseInstancePlugin {
|
||||
async init() {
|
||||
this.messageQueue = [];
|
||||
}
|
||||
|
||||
onControllerConnectionEvent(event) {
|
||||
if (event === 'connect') {
|
||||
for (const [action, content] of this.messageQueue) {
|
||||
try {
|
||||
this.instance.sendTo('controller', new InstanceActionEvent(this.instance.name, action, content));
|
||||
} catch (err) {
|
||||
this.messageQueue.push([output.action, output.message]);
|
||||
}
|
||||
}
|
||||
this.messageQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
async onOutput(output) {
|
||||
if (output.type !== 'action') return;
|
||||
if (this.host.connector.connected) {
|
||||
this.instance.sendTo('controller', new InstanceActionEvent(this.instance.name, output.action, output.message));
|
||||
} else {
|
||||
this.messageQueue.push([output.action, output.message]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {InstancePlugin};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import type {InstancePluginContext} from '@clusterio/host';
|
||||
import {ChatEvent} from './index.js';
|
||||
|
||||
export default async function(context: InstancePluginContext) {
|
||||
const {instance, host, plugin} = context;
|
||||
let queue: [string, string][] = [];
|
||||
|
||||
instance.hooks.controllerConnectionEvent.attach(plugin.name, async (event) => {
|
||||
if (event !== 'connect' || queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = queue;
|
||||
queue = [];
|
||||
const failed: [string, string][] = [];
|
||||
|
||||
for (const [action, content] of pending) {
|
||||
try {
|
||||
await instance.sendTo('controller', new ChatEvent(instance.name, action, content));
|
||||
} catch (err) {
|
||||
failed.push([action, content]);
|
||||
}
|
||||
}
|
||||
|
||||
queue = failed;
|
||||
});
|
||||
|
||||
instance.hooks.output.attach(plugin.name, async (output) => {
|
||||
if (output.type !== 'action') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (host.connector.connected) {
|
||||
try {
|
||||
await instance.sendTo('controller', new ChatEvent(instance.name, output.action, output.message));
|
||||
} catch (err) {
|
||||
queue.push([output.action, output.message]);
|
||||
}
|
||||
} else {
|
||||
queue.push([output.action, output.message]);
|
||||
}
|
||||
});
|
||||
|
||||
instance.hooks.controllerConnectionEvent.attach(plugin.name, async (event) => {
|
||||
|
||||
});
|
||||
}
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@phidias0303/clusterio_chat_sync",
|
||||
"version": "1.0.2",
|
||||
"description": "One way chat sync. Created with the help of Cooldude2606",
|
||||
"version": "1.0.3",
|
||||
"description": "One way chat forward to Discord",
|
||||
"author": "PHIDIAS <phidias@aperx.org>",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/PHIDIAS0303/ClusterChatSync#readme",
|
||||
@@ -12,7 +12,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/PHIDIAS0303/ClusterChatSync/issues"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"aperx",
|
||||
"clusterio",
|
||||
@@ -20,10 +20,10 @@
|
||||
"discord",
|
||||
"clusterio-plugin"
|
||||
],
|
||||
"main": "index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prepare": "webpack-cli --env production"
|
||||
"prepare": "tsc --build && webpack-cli --mode production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@clusterio/lib": "workspace:^"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{"path": "../../packages/lib/tsconfig.browser.json"},
|
||||
{"path": "../../packages/web_ui/tsconfig.browser.json"}
|
||||
],
|
||||
"include": ["web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{"path": "./tsconfig.node.json"},
|
||||
{"path": "./tsconfig.browser.json"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{"path": "../../packages/lib/tsconfig.node.json"},
|
||||
{"path": "../../packages/host/tsconfig.node.json"},
|
||||
{"path": "../../packages/ctl/tsconfig.node.json"}
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"]
|
||||
}
|
||||
File renamed without changes.
@@ -5,20 +5,19 @@ const { merge } = require('webpack-merge');
|
||||
|
||||
const common = require('@clusterio/web_ui/webpack.common');
|
||||
|
||||
module.exports = (env = {}) => merge(common(env), {
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: './web/index.jsx',
|
||||
entry: './web/index.tsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist', 'web'),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: 'ClusterChatSync',
|
||||
library: {type: 'window', name: 'plugin_ClusterChatSync' },
|
||||
library: {type: 'var', name: 'plugin_ClusterChatSync'},
|
||||
exposes: {
|
||||
'./': './index.js',
|
||||
'./package.json': './package.json',
|
||||
'./web': './web/index.jsx',
|
||||
'./': './index.ts',
|
||||
'./package.json': './package.json'
|
||||
},
|
||||
shared: {
|
||||
'@clusterio/lib': {import: false},
|
||||
Reference in new issue
Block a user