mirror of
https://github.com/PHIDIAS0303/ClusterChatSync.git
synced 2025-12-27 03:05:21 +09:00
193
controller.js
Normal file
193
controller.js
Normal file
@@ -0,0 +1,193 @@
|
||||
'use strict';
|
||||
const Discord = require('discord.js');
|
||||
const fetch = require('node-fetch');
|
||||
const {BaseControllerPlugin} = require('@clusterio/controller');
|
||||
const {InstanceActionEvent} = require('./info.js');
|
||||
const {ChatEvent} = require("./message.js");
|
||||
|
||||
const MAX_DISCORD_MESSAGE_LENGTH = 1950;
|
||||
const MIN_CONFIDENCE_SCORE = 10.0;
|
||||
|
||||
class LibreTranslateAPI {
|
||||
constructor(url, apiKey, logger = console) {
|
||||
if (!url || !apiKey) this.logger.error('[Chat Sync] LibreTranslate API configuration is incomplete.');
|
||||
try {new URL(url);} catch {this.logger.error('[Chat Sync] LibreTranslate url is invalid');}
|
||||
this.url = url.endsWith('/') ? url : url + '/';
|
||||
this.apiKey = apiKey;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async handleResponse(response) {
|
||||
if (!response.ok) this.logger.error(`[Chat Sync] API Request got HTTP ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
this.allowedLanguages = (await this.handleResponse(await fetch(`${this.url}languages?api_key=${this.apiKey}`, {method: 'GET'})))?.[0]?.targets || [];
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] failed to initialize languages:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
async translateRequest(q, source, target) {
|
||||
try {
|
||||
return (await this.handleResponse(await fetch(`${this.url}translate`, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({q: q, api_key: this.apiKey, source: source, target: target})})))?.translatedText;
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] Translation failed:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
async detectLanguage(q) {
|
||||
try {
|
||||
return (await this.handleResponse(await fetch(`${this.url}detect`, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({q: q, api_key: this.apiKey})})))?.[0];
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] Detection failed:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
async translate(query, targetLanguages) {
|
||||
console.log(query);
|
||||
const result = {action: false, passage: []};
|
||||
|
||||
try {
|
||||
const detection = await this.detectLanguage(query);
|
||||
|
||||
if (!detection || typeof detection !== 'object' || !detection.confidence || !detection.language) {
|
||||
this.logger.warn('[Chat Sync] Invalid language detection result:', detection);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (detection.confidence > MIN_CONFIDENCE_SCORE) {
|
||||
for (const targetLang of targetLanguages) {
|
||||
if (!((detection.language === 'zh-Hans' || detection.language === 'zh-Hant') && (targetLang === 'zh-Hans' || targetLang === 'zh-Hant')) && detection.language !== targetLang && this.allowedLanguages.includes(detection.language) && this.allowedLanguages.includes(targetLang)) {
|
||||
result.action = true;
|
||||
const translated = await this.translateRequest(query, detection.language, targetLang);
|
||||
result.passage.push(`[${detection.language} -> ${targetLang}] ${translated}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] translation failed:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
|
||||
if (this.controller.config.get('ClusterChatSync.use_libretranslate')) {
|
||||
this.translator = new LibreTranslateAPI(this.controller.config.get('ClusterChatSync.libretranslate_url'), this.controller.config.get('ClusterChatSync.libretranslate_key'), this.logger);
|
||||
await this.translator.init();
|
||||
this.translator_language = this.controller.config.get('ClusterChatSync.libretranslate_language').trim().split(/\s+/) || ['zh-Hant', 'en'];
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
|
||||
if (this.controller.config.get('ClusterChatSync.use_libretranslate')) {
|
||||
const result = await this.translator.translate(nrc_message, this.translator_language);
|
||||
|
||||
if (result && result.action) {
|
||||
await this.sendMessage(request, `**\`${nrc_username}\`**: ${result.passage}`);
|
||||
this.instance.sendTo({ instanceId: this.instance.id }, new ChatEvent(this.instance.name, `[color=255,255,255]\`${nrc_username}\`: ${result}[/color]`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {ControllerPlugin};
|
||||
280
controller.ts
280
controller.ts
@@ -1,280 +0,0 @@
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import fetch from 'node-fetch';
|
||||
import { BaseControllerPlugin } from '@clusterio/controller';
|
||||
import { InstanceActionEvent } from './info';
|
||||
import { ChatEvent } from './message';
|
||||
|
||||
const MAX_DISCORD_MESSAGE_LENGTH = 1950;
|
||||
const MIN_CONFIDENCE_SCORE = 10.0;
|
||||
|
||||
interface TranslationResult {
|
||||
action: boolean;
|
||||
passage: string[];
|
||||
}
|
||||
|
||||
interface LanguageDetection {
|
||||
confidence: number;
|
||||
language: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
class LibreTranslateAPI {
|
||||
private url: string;
|
||||
private apiKey: string;
|
||||
private logger: Console;
|
||||
private allowedLanguages: string[] = [];
|
||||
|
||||
constructor(url: string, apiKey: string, logger: Console = console) {
|
||||
if (!url || !apiKey) {
|
||||
logger.error('[Chat Sync] LibreTranslate API configuration is incomplete.');
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
logger.error('[Chat Sync] LibreTranslate url is invalid');
|
||||
}
|
||||
this.url = url.endsWith('/') ? url : url + '/';
|
||||
this.apiKey = apiKey;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private async handleResponse(response: fetch.Response): Promise<any> {
|
||||
if (!response.ok) {
|
||||
this.logger.error(`[Chat Sync] API Request got HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
const languages = await this.handleResponse(
|
||||
await fetch(`${this.url}languages?api_key=${this.apiKey}`, { method: 'GET' })
|
||||
);
|
||||
this.allowedLanguages = languages?.[0]?.targets || [];
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] failed to initialize languages:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
async translateRequest(q: string, source: string, target: string): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await this.handleResponse(
|
||||
await fetch(`${this.url}translate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ q, api_key: this.apiKey, source, target })
|
||||
})
|
||||
);
|
||||
return response?.translatedText;
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] Translation failed:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
async detectLanguage(q: string): Promise<LanguageDetection | undefined> {
|
||||
try {
|
||||
const response = await this.handleResponse(
|
||||
await fetch(`${this.url}detect`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ q, api_key: this.apiKey })
|
||||
})
|
||||
);
|
||||
return response?.[0];
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] Detection failed:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
async translate(query: string, targetLanguages: string[]): Promise<TranslationResult> {
|
||||
const result: TranslationResult = { action: false, passage: [] };
|
||||
try {
|
||||
const detection = await this.detectLanguage(query);
|
||||
if (!detection || typeof detection !== 'object' || !detection.confidence || !detection.language) {
|
||||
this.logger.warn('[Chat Sync] Invalid language detection result:', detection);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (detection.confidence > MIN_CONFIDENCE_SCORE) {
|
||||
for (const targetLang of targetLanguages) {
|
||||
if (
|
||||
!((detection.language === 'zh-Hans' || detection.language === 'zh-Hant') &&
|
||||
(targetLang === 'zh-Hans' || targetLang === 'zh-Hant')) &&
|
||||
detection.language !== targetLang &&
|
||||
this.allowedLanguages.includes(detection.language) &&
|
||||
this.allowedLanguages.includes(targetLang)
|
||||
) {
|
||||
result.action = true;
|
||||
const translated = await this.translateRequest(query, detection.language, targetLang);
|
||||
if (translated) {
|
||||
result.passage.push(`[${detection.language} -> ${targetLang}] ${translated}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Chat Sync] translation failed:\n${err.stack}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
interface ControllerConfig {
|
||||
get(key: string): any;
|
||||
on(event: string, callback: (field: string, curr: any, prev: any) => void);
|
||||
}
|
||||
|
||||
interface InstanceInfo {
|
||||
name: string;
|
||||
id: number;
|
||||
sendTo(target: string | { instanceId: number }, message: any): void;
|
||||
}
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
private client: Client | null = null;
|
||||
private translator?: LibreTranslateAPI;
|
||||
private translator_language: string[] = [];
|
||||
|
||||
constructor(
|
||||
private controller: {
|
||||
config: ControllerConfig;
|
||||
handle(event: any, handler: Function): void;
|
||||
logger: Console;
|
||||
},
|
||||
private instance: InstanceInfo
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.controller.config.on('fieldChanged', (field: string, curr: any, prev: any) => {
|
||||
if (field === 'ClusterChatSync.discord_bot_token') {
|
||||
this.connect().catch(err => {
|
||||
this.controller.logger.error(`[Chat Sync] Discord bot token:\n${err.stack}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
this.controller.handle(InstanceActionEvent, this.handleInstanceAction.bind(this));
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
private async clientDestroy(): Promise<void> {
|
||||
if (this.client) {
|
||||
this.client.destroy();
|
||||
this.client = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async connect(): Promise<void> {
|
||||
await this.clientDestroy();
|
||||
const token = this.controller.config.get('ClusterChatSync.discord_bot_token');
|
||||
if (!token) {
|
||||
this.controller.logger.error('[Chat Sync] Discord bot token not configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent
|
||||
]
|
||||
});
|
||||
|
||||
this.controller.logger.info('[Chat Sync] Logging into Discord.');
|
||||
try {
|
||||
await this.client.login(token);
|
||||
} catch (err) {
|
||||
this.controller.logger.error(`[Chat Sync] Discord login error:\n${err.stack}`);
|
||||
await this.clientDestroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.controller.logger.info('[Chat Sync] Logged in Discord successfully.');
|
||||
|
||||
if (this.controller.config.get('ClusterChatSync.use_libretranslate')) {
|
||||
this.translator = new LibreTranslateAPI(
|
||||
this.controller.config.get('ClusterChatSync.libretranslate_url'),
|
||||
this.controller.config.get('ClusterChatSync.libretranslate_key'),
|
||||
this.controller.logger
|
||||
);
|
||||
await this.translator.init();
|
||||
this.translator_language = this.controller.config.get('ClusterChatSync.libretranslate_language')
|
||||
.trim()
|
||||
.split(/\s+/) || ['zh-Hant', 'en'];
|
||||
}
|
||||
}
|
||||
|
||||
async onShutdown(): Promise<void> {
|
||||
await this.clientDestroy();
|
||||
}
|
||||
|
||||
private async sendMessage(request: { instanceName: string }, message: string): Promise<void> {
|
||||
if (!this.client) return;
|
||||
|
||||
const channelMapping = this.controller.config.get('ClusterChatSync.discord_channel_mapping');
|
||||
const channel_id = channelMapping[request.instanceName];
|
||||
if (!channel_id) return;
|
||||
|
||||
try {
|
||||
const channel = await this.client.channels.fetch(channel_id);
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
this.controller.logger.error(`[Chat Sync] Discord Channel ID ${channel_id} not found or not text channel.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.controller.config.get('ClusterChatSync.datetime_on_message')) {
|
||||
const now = new Date();
|
||||
const timestamp = `${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')}`;
|
||||
message = `${timestamp} ${message}`;
|
||||
}
|
||||
|
||||
while (message.length > 0) {
|
||||
let chunk = message.slice(0, MAX_DISCORD_MESSAGE_LENGTH);
|
||||
const lastSpace = chunk.lastIndexOf(' ');
|
||||
|
||||
if (lastSpace !== -1 && chunk.length === MAX_DISCORD_MESSAGE_LENGTH) {
|
||||
chunk = chunk.slice(0, lastSpace);
|
||||
message = message.slice(lastSpace).trim();
|
||||
} else {
|
||||
message = message.slice(chunk.length).trim();
|
||||
}
|
||||
|
||||
await channel.send(chunk, { allowedMentions: { parse: [] } });
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.code !== 10003) { // Unknown Channel error
|
||||
this.controller.logger.error(`[Chat Sync] Discord channel error:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleInstanceAction(request: {
|
||||
action: string;
|
||||
content: string;
|
||||
instanceName: string
|
||||
}): Promise<void> {
|
||||
if (request.action !== 'CHAT' && request.action !== 'SHOUT') return;
|
||||
|
||||
const sanitizedContent = request.content
|
||||
.replace(/\[special-item=.*?\]/g, '<blueprint>')
|
||||
.replace(/<@/g, '<@\u200c>');
|
||||
const colonIndex = sanitizedContent.indexOf(':');
|
||||
const username = sanitizedContent.substring(0, colonIndex);
|
||||
const message = sanitizedContent.substring(colonIndex + 1).trim();
|
||||
|
||||
await this.sendMessage(request, `**\`${username}\`**: ${message}`);
|
||||
|
||||
if (this.translator && this.controller.config.get('ClusterChatSync.use_libretranslate')) {
|
||||
const result = await this.translator.translate(message, this.translator_language);
|
||||
if (result?.action) {
|
||||
await this.sendMessage(request, `**\`${username}\`**: ${result.passage.join('\n')}`);
|
||||
this.instance.sendTo(
|
||||
{ instanceId: this.instance.id },
|
||||
new ChatEvent(this.instance.name, `[color=255,255,255]\`${username}\`: ${result.passage.join('\n')}[/color]`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
info.js
Normal file
81
info.js
Normal file
@@ -0,0 +1,81 @@
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
name: 'ClusterChatSync',
|
||||
title: 'Cluster Chat Sync',
|
||||
description: 'One way chat sync.',
|
||||
instanceEntrypoint: 'instance',
|
||||
controllerEntrypoint: 'controller',
|
||||
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'
|
||||
},
|
||||
},
|
||||
'ClusterChatSync.use_libretranslate': {
|
||||
title: 'Translate Message',
|
||||
description: 'Using self host or paid service of libretranslate',
|
||||
type: 'boolean',
|
||||
initialValue: false
|
||||
},
|
||||
'ClusterChatSync.libretranslate_url': {
|
||||
title: 'Translate Server URL',
|
||||
description: 'Including http protocol, and the port if needed',
|
||||
type: 'string',
|
||||
initialValue: 'http://localhost:5000'
|
||||
},
|
||||
'ClusterChatSync.libretranslate_key': {
|
||||
title: 'Translate Server API Key',
|
||||
description: 'The API key for the translate server',
|
||||
type: 'string',
|
||||
initialValue: '123456'
|
||||
},
|
||||
'ClusterChatSync.libretranslate_language': {
|
||||
title: 'Translate Server Target Language',
|
||||
description: 'Put a space between each language, using ISO 639-1 codes',
|
||||
type: 'string',
|
||||
initialValue: 'zh-Hant en'
|
||||
},
|
||||
},
|
||||
messages: [InstanceActionEvent],
|
||||
};
|
||||
|
||||
module.exports = {plugin, InstanceActionEvent};
|
||||
92
info.ts
92
info.ts
@@ -1,92 +0,0 @@
|
||||
import type { PluginConfigFields, SerializedEvent } from '@clusterio/lib';
|
||||
|
||||
export class InstanceActionEvent {
|
||||
static type = 'event';
|
||||
static src = 'instance';
|
||||
static dst = 'controller';
|
||||
static plugin = 'ClusterChatSync';
|
||||
|
||||
instanceName: string;
|
||||
action: string;
|
||||
content: string;
|
||||
|
||||
constructor(instanceName: string, action: string, content: string) {
|
||||
this.instanceName = instanceName;
|
||||
this.action = action;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['instanceName', 'action', 'content'] as const,
|
||||
properties: {
|
||||
instanceName: { type: 'string' },
|
||||
action: { type: 'string' },
|
||||
content: { type: 'string' }
|
||||
}
|
||||
};
|
||||
|
||||
static fromJSON(json: SerializedEvent<InstanceActionEvent>): InstanceActionEvent {
|
||||
return new this(json.instanceName, json.action, json.content);
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelMapping {
|
||||
[instanceName: string]: string;
|
||||
}
|
||||
|
||||
const pluginConfigFields: PluginConfigFields = {
|
||||
'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'
|
||||
} as ChannelMapping,
|
||||
},
|
||||
'ClusterChatSync.use_libretranslate': {
|
||||
title: 'Translate Message',
|
||||
description: 'Using self host or paid service of libretranslate',
|
||||
type: 'boolean',
|
||||
initialValue: false,
|
||||
},
|
||||
'ClusterChatSync.libretranslate_url': {
|
||||
title: 'Translate Server URL',
|
||||
description: 'Including http protocol, and the port if needed',
|
||||
type: 'string',
|
||||
initialValue: 'http://localhost:5000',
|
||||
},
|
||||
'ClusterChatSync.libretranslate_key': {
|
||||
title: 'Translate Server API Key',
|
||||
description: 'The API key for the translate server',
|
||||
type: 'string',
|
||||
initialValue: '123456',
|
||||
},
|
||||
'ClusterChatSync.libretranslate_language': {
|
||||
title: 'Translate Server Target Language',
|
||||
description: 'Put a space between each language, using ISO 639-1 codes',
|
||||
type: 'string',
|
||||
initialValue: 'zh-Hant en',
|
||||
},
|
||||
};
|
||||
|
||||
export const plugin = {
|
||||
name: 'ClusterChatSync',
|
||||
title: 'Cluster Chat Sync',
|
||||
description: 'One way chat sync.',
|
||||
instanceEntrypoint: 'instance',
|
||||
controllerEntrypoint: 'controller',
|
||||
controllerConfigFields: pluginConfigFields,
|
||||
messages: [InstanceActionEvent],
|
||||
};
|
||||
34
instance.js
Normal file
34
instance.js
Normal file
@@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
const lib = require("@clusterio/lib");
|
||||
const {BaseInstancePlugin} = require("@clusterio/host");
|
||||
const {InstanceActionEvent} = require("./info.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
instance.ts
47
instance.ts
@@ -1,47 +0,0 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import { InstanceActionEvent } from "./info";
|
||||
|
||||
type MessageQueueItem = [string, unknown]; // [action, content]
|
||||
type ControllerEvent = 'connect' | 'disconnect' | string;
|
||||
type OutputMessage = {
|
||||
type: string;
|
||||
action: string;
|
||||
message: unknown;
|
||||
};
|
||||
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
private messageQueue: MessageQueueItem[] = [];
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.messageQueue = [];
|
||||
}
|
||||
|
||||
onControllerConnectionEvent(event: ControllerEvent): void {
|
||||
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([action, content]);
|
||||
|
||||
// Optional: Log the error
|
||||
console.error('Failed to send queued message:', err);
|
||||
}
|
||||
}
|
||||
this.messageQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
async onOutput(output: OutputMessage): Promise<void> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
message.js
Normal file
33
message.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// import { Type, Static } from "@sinclair/typebox";
|
||||
const {Type, Static} = require("@sinclair/typebox");
|
||||
|
||||
export class ChatEvent {
|
||||
// declare ["constructor"]: typeof ChatEvent;
|
||||
// as const
|
||||
static type = "event";
|
||||
// as const
|
||||
static src = ["control", "instance"];
|
||||
// as const
|
||||
static dst = "instance";
|
||||
// as const
|
||||
static plugin = "global_chat";
|
||||
static permission = null;
|
||||
|
||||
/*
|
||||
constructor(
|
||||
public instanceName: string,
|
||||
public content: string,
|
||||
) {
|
||||
}
|
||||
*/
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"instanceName": Type.String(),
|
||||
"content": Type.String(),
|
||||
});
|
||||
|
||||
// json: Static<typeof ChatEvent.jsonSchema>
|
||||
static fromJSON(json) {
|
||||
return new this(json.instanceName, json.content);
|
||||
}
|
||||
}
|
||||
25
message.ts
25
message.ts
@@ -1,25 +0,0 @@
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
|
||||
export class ChatEvent {
|
||||
declare ["constructor"]: typeof ChatEvent;
|
||||
static type = "event" as const;
|
||||
static src = ["control", "instance"] as const;
|
||||
static dst = "instance" as const;
|
||||
static plugin = "global_chat" as const;
|
||||
static permission = null;
|
||||
|
||||
constructor(
|
||||
public instanceName: string,
|
||||
public content: string,
|
||||
) {
|
||||
}
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"instanceName": Type.String(),
|
||||
"content": Type.String(),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof ChatEvent.jsonSchema>) {
|
||||
return new this(json.instanceName, json.content);
|
||||
}
|
||||
}
|
||||
20
package.json
20
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "@phidias0303/clusterio_chat_sync",
|
||||
"version": "1.0.2",
|
||||
"description": "One way chat sync",
|
||||
"main": "info.ts",
|
||||
"main": "info.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prepare": "webpack-cli --env production"
|
||||
@@ -25,19 +25,19 @@
|
||||
},
|
||||
"homepage": "https://github.com/PHIDIAS0303/ClusterChatSync#readme",
|
||||
"peerDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.21"
|
||||
"@clusterio/lib": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.21",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.21",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-merge": "^6.0.1"
|
||||
"@clusterio/lib": "catalog:",
|
||||
"@clusterio/web_ui": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"discord.js": "^14.19.3",
|
||||
"@sinclair/typebox": "^0.30.4",
|
||||
"node-fetch": "^3.3.2"
|
||||
"discord.js": "catalog:",
|
||||
"@sinclair/typebox": "catalog:",
|
||||
"node-fetch": "catalog:"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
12
pnpm-workspace.yaml
Normal file
12
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
packages:
|
||||
- "*"
|
||||
|
||||
catalog:
|
||||
"@clusterio/lib": ^2.0.0-alpha.21
|
||||
"@clusterio/web_ui": ^2.0.0-alpha.21
|
||||
"@sinclair/typebox": ^0.34.39
|
||||
"discord.js": ^14.21.0
|
||||
"webpack": ^5.101.2
|
||||
"webpack-cli": ^6.0.1
|
||||
"webpack-merge": ^6.0.1
|
||||
"node-fetch": ^3.3.2
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.browser.json",
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.node.json",
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
}
|
||||
@@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), {
|
||||
name: 'ClusterChatSync',
|
||||
library: {type: 'var', name: 'plugin_ClusterChatSync' },
|
||||
exposes: {
|
||||
'./': './info.ts',
|
||||
'./': './info.js',
|
||||
'./package.json': './package.json',
|
||||
},
|
||||
shared: {
|
||||
|
||||
Reference in New Issue
Block a user