mirror of
https://github.com/a-sync/game-server-watcher.git
synced 2026-03-31 06:33:44 -04:00
slack-bot WIP
This commit is contained in:
@@ -14,5 +14,9 @@ PORT=8080
|
|||||||
DISCORD_BOT_TOKEN=
|
DISCORD_BOT_TOKEN=
|
||||||
# Telegram bot token
|
# Telegram bot token
|
||||||
TELEGRAM_BOT_TOKEN=
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
# Slack bot token
|
||||||
|
SLACK_BOT_TOKEN=
|
||||||
|
# Slack app token
|
||||||
|
SLACK_APP_TOKEN=
|
||||||
# Steam web API key
|
# Steam web API key
|
||||||
STEAM_WEB_API_KEY=
|
STEAM_WEB_API_KEY=
|
||||||
|
|||||||
6
TODO
6
TODO
@@ -1,8 +1,10 @@
|
|||||||
|
https://slack.dev/bolt-js/tutorial/getting-started
|
||||||
|
|
||||||
// move loop inside watcher instance
|
// move loop inside watcher instance
|
||||||
// flush game server specific (d/t) data
|
// flush game server specific (d/t) data
|
||||||
// response normalization: https://github.com/GameServerManagers/LinuxGSM/blob/master/lgsm/functions/query_gamedig.sh
|
// response normalization: https://github.com/GameServerManagers/LinuxGSM/blob/master/lgsm/functions/query_gamedig.sh
|
||||||
|
|
||||||
//v3
|
//v3
|
||||||
// refactor config structure (object first, nested settings for complex entities)
|
// refactor config structure (object first, nested settings for complex entities)
|
||||||
// introduce storage layer and add free postgresql support
|
// introduce storage layer system and add free postgresql/redis support
|
||||||
// support gamedig udpListenPort config
|
// support gamedig udpListenPort config
|
||||||
|
|||||||
1914
package-lock.json
generated
1914
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@commonify/lowdb": "^3.0.0",
|
"@commonify/lowdb": "^3.0.0",
|
||||||
|
"@slack/bolt": "^3.13.3",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"discord.js": "^14.7.1",
|
"discord.js": "^14.7.1",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class ServerInfoMessage {
|
|||||||
this.messageId = this.message.id;
|
this.messageId = this.message.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (db.data) {
|
if (db.data && this.messageId) {
|
||||||
const mi = db.data.findIndex(d => {
|
const mi = db.data.findIndex(d => {
|
||||||
return d.channelId === this.channelId && d.host === this.host && d.port === this.port;
|
return d.channelId === this.channelId && d.host === this.host && d.port === this.port;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface ApiResponse {
|
|||||||
steam: boolean;
|
steam: boolean;
|
||||||
discord: boolean;
|
discord: boolean;
|
||||||
telegram: boolean;
|
telegram: boolean;
|
||||||
|
slack: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,8 @@ createServer(async (req, res) => {
|
|||||||
re.features = {
|
re.features = {
|
||||||
steam: Boolean(process.env.STEAM_WEB_API_KEY),
|
steam: Boolean(process.env.STEAM_WEB_API_KEY),
|
||||||
discord: Boolean(process.env.DISCORD_BOT_TOKEN),
|
discord: Boolean(process.env.DISCORD_BOT_TOKEN),
|
||||||
telegram: Boolean(process.env.TELEGRAM_BOT_TOKEN)
|
telegram: Boolean(process.env.TELEGRAM_BOT_TOKEN),
|
||||||
|
slack: Boolean(process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN)
|
||||||
};
|
};
|
||||||
} else if (reqPath[0] === 'config') {
|
} else if (reqPath[0] === 'config') {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
|
|||||||
217
src/slack-bot.ts
Normal file
217
src/slack-bot.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { App } from '@slack/bolt';
|
||||||
|
import { Low, JSONFile } from '@commonify/lowdb';
|
||||||
|
import { GameServer } from './game-server';
|
||||||
|
import hhmmss from './lib/hhmmss';
|
||||||
|
|
||||||
|
const DATA_PATH = process.env.DATA_PATH || './data/';
|
||||||
|
const DBG = Boolean(Number(process.env.DBG));
|
||||||
|
|
||||||
|
interface SlackData {
|
||||||
|
channelId: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
messageId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = new JSONFile<SlackData[]>(DATA_PATH + 'slack.json');
|
||||||
|
const db = new Low<SlackData[]>(adapter);
|
||||||
|
|
||||||
|
const serverInfoMessages: ServerInfoMessage[] = [];
|
||||||
|
|
||||||
|
let bot: App;
|
||||||
|
export async function init(token: string, appToken: string) {
|
||||||
|
if (!bot) {
|
||||||
|
console.log('slack-bot starting...');
|
||||||
|
bot = new App({
|
||||||
|
token,
|
||||||
|
appToken,
|
||||||
|
socketMode: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (DBG) {
|
||||||
|
bot.message('ping', async ({ message, say }) => {
|
||||||
|
// Handle only newly posted messages here
|
||||||
|
if (message.subtype === undefined
|
||||||
|
|| message.subtype === 'bot_message'
|
||||||
|
|| message.subtype === 'file_share'
|
||||||
|
|| message.subtype === 'thread_broadcast') {
|
||||||
|
await say(`<@${message.user}> pong`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await bot.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
serverInfoMessages.length = 0;
|
||||||
|
await db.read();
|
||||||
|
db.data = db.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function serverUpdate(gs: GameServer) {
|
||||||
|
if (DBG) console.log('slack.serverUpdate', gs.config.host, gs.config.port, gs.config.slack);
|
||||||
|
|
||||||
|
if (gs.config.slack) {
|
||||||
|
for (const ch of gs.config.slack) {
|
||||||
|
try {
|
||||||
|
let m = await getServerInfoMessage(ch.channelId, gs.config.host, gs.config.port);
|
||||||
|
await m.updatePost(gs);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(['slack-bot.sup', ch.channelId, gs.config.host, gs.config.port].join(':'), e.message || e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getServerInfoMessage(cid: string, host: string, port: number) {
|
||||||
|
let m = serverInfoMessages.find(n => {
|
||||||
|
return n.channelId === cid && n.host === host && n.port === port;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!m) {
|
||||||
|
m = new ServerInfoMessage(cid, host, port);
|
||||||
|
|
||||||
|
let msgId;
|
||||||
|
if (db.data) {
|
||||||
|
const md = db.data.find(d => {
|
||||||
|
return d.channelId === cid && d.host === host && d.port === port;
|
||||||
|
});
|
||||||
|
if (md) msgId = md.messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await m.init(msgId);
|
||||||
|
|
||||||
|
serverInfoMessages.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerInfoMessage {
|
||||||
|
public channelId: string;
|
||||||
|
public host: string;
|
||||||
|
public port: number;
|
||||||
|
public messageId: string = '0';
|
||||||
|
|
||||||
|
constructor(channelId: string, host: string, port: number) {
|
||||||
|
this.channelId = channelId;
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(msgId?: string) {
|
||||||
|
if (msgId) this.messageId = msgId;
|
||||||
|
else {
|
||||||
|
const message = await bot.client.chat.postMessage({
|
||||||
|
channel: this.channelId,
|
||||||
|
text: 'Initializing server info...'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (message.ok && message.ts) {
|
||||||
|
this.messageId = message.ts;
|
||||||
|
} else {
|
||||||
|
console.error(['slack.init.msg', this.channelId, this.host, this.port].join(':'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db.data && this.messageId) {
|
||||||
|
const mi = db.data.findIndex(d => {
|
||||||
|
return d.channelId === this.channelId && d.host === this.host && d.port === this.port;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mi === -1 || mi === undefined) {
|
||||||
|
db.data.push({
|
||||||
|
channelId: this.channelId,
|
||||||
|
host: this.host,
|
||||||
|
port: this.port,
|
||||||
|
messageId: this.messageId
|
||||||
|
});
|
||||||
|
} else db.data[mi].messageId = this.messageId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.write();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(['slack.init.db', this.channelId, this.host, this.port].join(':'), e.message || e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePost(gs: GameServer) {
|
||||||
|
// const embed = new EmbedBuilder();
|
||||||
|
// const fields: APIEmbedField[] = [];
|
||||||
|
// embed.setFooter({ text: 'Last updated' });
|
||||||
|
// embed.setTimestamp();
|
||||||
|
// embed.setImage(gs.history.statsChart());
|
||||||
|
|
||||||
|
let text = this.escapeMarkdown(gs.niceName) + ' offline...';
|
||||||
|
|
||||||
|
if (gs.info && gs.online) {
|
||||||
|
text = this.escapeMarkdown(gs.niceName) + ' online! game:' + gs.info.game + ' map:' + gs.info.map + ' players:' + gs.info.players.length;
|
||||||
|
// embed.setTitle(gs.niceName.slice(0, 256));
|
||||||
|
// embed.setColor('#000000');
|
||||||
|
|
||||||
|
// if (gs.info.game) fields.push({ name: 'Game', value: String(gs.info.game), inline: true });
|
||||||
|
// if (gs.info.map) fields.push({ name: 'Map', value: String(gs.info.map), inline: true });
|
||||||
|
// fields.push({ name: 'Players', value: gs.info.playersNum + '/' + gs.info.playersMax, inline: true });
|
||||||
|
// fields.push({ name: 'Connect', value: 'steam://connect/' + gs.info.connect });
|
||||||
|
|
||||||
|
// if (gs.info?.players.length > 0) {
|
||||||
|
// const pNames: string[] = [];
|
||||||
|
// const pTimes: string[] = [];
|
||||||
|
// const pScores: string[] = [];
|
||||||
|
// const pPings: string[] = [];
|
||||||
|
|
||||||
|
// for (const p of gs.info?.players) {
|
||||||
|
// if (pNames.join('\n').length > 1016
|
||||||
|
// || pTimes.join('\n').length > 1016
|
||||||
|
// || pScores.join('\n').length > 1016
|
||||||
|
// || pPings.join('\n').length > 1016) {
|
||||||
|
// if (pNames.length) pNames.pop();
|
||||||
|
// if (pTimes.length) pTimes.pop();
|
||||||
|
// if (pScores.length) pScores.pop();
|
||||||
|
// if (pPings.length) pPings.pop();
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (p.get('name') !== undefined) pNames.push(p.get('name') || 'n/a');
|
||||||
|
// if (p.get('time') !== undefined) pTimes.push(hhmmss(p.get('time') || 0));
|
||||||
|
// if (p.get('score') !== undefined) pScores.push(p.get('score') || '0');
|
||||||
|
// if (p.get('ping') !== undefined) pPings.push(String(p.get('ping') || 0) + ' ms');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (pNames.length) fields.push({ name: 'Name', value: '```\n' + pNames.join('\n').slice(0, 1016) + '\n```', inline: true });
|
||||||
|
// if (pTimes.length) fields.push({ name: 'Time', value: '```\n' + pTimes.join('\n').slice(0, 1016) + '\n```', inline: true });
|
||||||
|
// if (pScores.length) fields.push({ name: 'Score', value: '```\n' + pScores.join('\n').slice(0, 1016) + '\n```', inline: true });
|
||||||
|
// if (pPings.length) fields.push({ name: 'Ping', value: '```\n' + pPings.join('\n').slice(0, 1016) + '\n```', inline: true });
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
// embed.setTitle(gs.niceName.slice(0, 245) + ' offline...');
|
||||||
|
// embed.setColor('#ff0000');
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed.setFields(fields);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bot.client.chat.update({
|
||||||
|
as_user: true,
|
||||||
|
channel: this.channelId,
|
||||||
|
ts: this.messageId,
|
||||||
|
text
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(['slack.up', this.channelId, this.host, this.port].join(':'), e.message || e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeMarkdown(str: string): string {
|
||||||
|
const patterns = [
|
||||||
|
/_/g,
|
||||||
|
/~/g,
|
||||||
|
/`/g,
|
||||||
|
/</g,
|
||||||
|
/>/g
|
||||||
|
];
|
||||||
|
|
||||||
|
return patterns.reduce((acc: string, pattern: RegExp) => acc.replace(pattern, '\\$&'), str);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ class ServerInfoMessage {
|
|||||||
this.messageId = msg.message_id;
|
this.messageId = msg.message_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (db.data) {
|
if (db.data && this.messageId) {
|
||||||
const mi = db.data.findIndex(d => {
|
const mi = db.data.findIndex(d => {
|
||||||
return d.chatId === this.chatId && d.host === this.host && d.port === this.port;
|
return d.chatId === this.chatId && d.host === this.host && d.port === this.port;
|
||||||
});
|
});
|
||||||
@@ -160,11 +160,14 @@ class ServerInfoMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
escapeMarkdown(str: string): string {
|
escapeMarkdown(str: string): string {
|
||||||
return str
|
const patterns = [
|
||||||
.replace(/_/g, '\\_')
|
/_/g,
|
||||||
.replace('~', '\\~')
|
/~/g,
|
||||||
.replace(/`/g, '\\`')
|
/`/g,
|
||||||
.replace(/\</g, '\\<')
|
/</g,
|
||||||
.replace(/\>/g, '\\>');
|
/>/g
|
||||||
|
];
|
||||||
|
|
||||||
|
return patterns.reduce((acc: string, pattern: RegExp) => acc.replace(pattern, '\\$&'), str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import { Low, JSONFile } from '@commonify/lowdb';
|
|||||||
import { GameServer, initDb, saveDb } from './game-server';
|
import { GameServer, initDb, saveDb } from './game-server';
|
||||||
import * as discordBot from './discord-bot';
|
import * as discordBot from './discord-bot';
|
||||||
import * as telegramBot from './telegram-bot';
|
import * as telegramBot from './telegram-bot';
|
||||||
|
import * as slackBot from './slack-bot';
|
||||||
|
|
||||||
const REFRESH_TIME_MINUTES = parseInt(process.env.REFRESH_TIME_MINUTES || '2', 10);
|
const REFRESH_TIME_MINUTES = parseInt(process.env.REFRESH_TIME_MINUTES || '2', 10);
|
||||||
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || '';
|
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || '';
|
||||||
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
|
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
|
||||||
|
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || '';
|
||||||
|
const SLACK_APP_TOKEN = process.env.SLACK_APP_TOKEN || '';
|
||||||
const DATA_PATH = process.env.DATA_PATH || './data/';
|
const DATA_PATH = process.env.DATA_PATH || './data/';
|
||||||
const DBG = Boolean(Number(process.env.DBG));
|
const DBG = Boolean(Number(process.env.DBG));
|
||||||
|
|
||||||
@@ -18,6 +21,10 @@ interface TelegramConfig {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SlackConfig {
|
||||||
|
channelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameServerConfig {
|
export interface GameServerConfig {
|
||||||
name: string;
|
name: string;
|
||||||
appId?: number;//0
|
appId?: number;//0
|
||||||
@@ -26,6 +33,7 @@ export interface GameServerConfig {
|
|||||||
timezoneOffset?: number;//0
|
timezoneOffset?: number;//0
|
||||||
discord?: DiscordConfig[];
|
discord?: DiscordConfig[];
|
||||||
telegram?: TelegramConfig[];
|
telegram?: TelegramConfig[];
|
||||||
|
slack?: SlackConfig[];
|
||||||
|
|
||||||
// node-gamedig stuff
|
// node-gamedig stuff
|
||||||
type: Type;
|
type: Type;
|
||||||
@@ -57,7 +65,7 @@ export async function updateConfig(data: GameServerConfig[]) {
|
|||||||
db.data = data;
|
db.data = data;
|
||||||
return await db.write();
|
return await db.write();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('w.saveDb', e.message || e);
|
console.error('w.updateConfig', e.message || e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +77,7 @@ class Watcher {
|
|||||||
|
|
||||||
if (DISCORD_BOT_TOKEN) await discordBot.init(DISCORD_BOT_TOKEN);
|
if (DISCORD_BOT_TOKEN) await discordBot.init(DISCORD_BOT_TOKEN);
|
||||||
if (TELEGRAM_BOT_TOKEN) await telegramBot.init(TELEGRAM_BOT_TOKEN);
|
if (TELEGRAM_BOT_TOKEN) await telegramBot.init(TELEGRAM_BOT_TOKEN);
|
||||||
|
if (SLACK_BOT_TOKEN && SLACK_APP_TOKEN) await slackBot.init(SLACK_BOT_TOKEN, SLACK_APP_TOKEN);
|
||||||
|
|
||||||
await initDb();
|
await initDb();
|
||||||
|
|
||||||
@@ -86,6 +95,7 @@ class Watcher {
|
|||||||
promises.push(gs.update().then(async () => {
|
promises.push(gs.update().then(async () => {
|
||||||
if (DISCORD_BOT_TOKEN) await discordBot.serverUpdate(gs);
|
if (DISCORD_BOT_TOKEN) await discordBot.serverUpdate(gs);
|
||||||
if (TELEGRAM_BOT_TOKEN) await telegramBot.serverUpdate(gs);
|
if (TELEGRAM_BOT_TOKEN) await telegramBot.serverUpdate(gs);
|
||||||
|
if (SLACK_BOT_TOKEN && SLACK_APP_TOKEN) await slackBot.serverUpdate(gs);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user