Compare commits

...

20 Commits

Author SHA1 Message Date
ThePendulum
48cf433a5c 1.28.5 2024-04-02 00:56:21 +02:00
ThePendulum
61e935b925 Showing board 4 instead of 3 times in letters game. 2024-04-02 00:56:19 +02:00
ThePendulum
9a7ef61d93 1.28.4 2024-03-08 00:49:44 +01:00
ThePendulum
bad5dc52f7 Added user point merging utility. 2024-03-08 00:49:37 +01:00
ThePendulum
d510caa123 1.28.3 2024-02-11 17:33:21 +01:00
ThePendulum
b6cc3d716b Using cookie instead of http session so we can strip Domain attribute. 2024-02-11 17:33:19 +01:00
ThePendulum
6a36df3593 1.28.2 2024-01-13 03:55:55 +01:00
ThePendulum
69bc1c9e6e Added points merge utility. 2024-01-13 03:55:46 +01:00
ThePendulum
aeb405967b 1.28.1 2024-01-13 02:26:05 +01:00
ThePendulum
73e60b81f1 Added ping. 2024-01-13 02:26:03 +01:00
ThePendulum
c9b985f768 1.28.0 2024-01-13 01:29:14 +01:00
ThePendulum
cb5de62e1c Reverted SocketIO to ws. 2024-01-13 01:29:10 +01:00
ThePendulum
b3cab90810 1.27.3 2024-01-02 16:15:39 +01:00
ThePendulum
47484ba7e2 Preventing ~solve when numbers game is in progress. 2024-01-02 16:15:37 +01:00
ThePendulum
4017f1cd07 1.27.2 2024-01-02 02:13:14 +01:00
ThePendulum
03dfe8e437 Exposing numbers game solver through command. 2024-01-02 02:13:11 +01:00
ThePendulum
1a992a6026 1.27.1 2023-12-07 22:05:53 +01:00
ThePendulum
9f8f503f13 Removed reconnect logic, handled by SocketIO. 2023-12-07 22:05:51 +01:00
ThePendulum
d460ba13c5 1.27.0 2023-12-05 20:55:47 +01:00
ThePendulum
ed5337d828 Fixed socket close event listener. 2023-12-05 20:55:39 +01:00
7 changed files with 164 additions and 37 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "schat2-clive",
"version": "1.26.12",
"version": "1.28.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "schat2-clive",
"version": "1.26.12",
"version": "1.28.5",
"license": "ISC",
"dependencies": {
"better-sqlite3": "^8.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "schat2-clive",
"version": "1.26.12",
"version": "1.28.5",
"description": "Game host for SChat 2-powered chat sites",
"main": "src/app.js",
"scripts": {

View File

@@ -92,12 +92,16 @@ async function play(context) {
try {
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 3) * 2)} seconds`))} left`, context.room.id);
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 4) * 3)} seconds`))} left`, context.room.id);
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round(settings.timeout / 3)} seconds`))} left`, context.room.id);
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 4) * 2)} seconds`))} left`, context.room.id);
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round(settings.timeout / 4)} seconds`))} left`, context.room.id);
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
stop(context);
} catch (error) {
// abort expected, probably not an error

View File

@@ -336,7 +336,7 @@ function start(context, numbers) {
context.sendMessage('Let\'s play the numbers! Would you like a large number or a small one?', context.room.id);
}
function solve(calculation, context) {
function calc(calculation, context) {
try {
const parsed = math.parse(calculation.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
const answer = parsed.evaluate();
@@ -347,6 +347,51 @@ function solve(calculation, context) {
}
}
async function solve(equation, context) {
const [numberString, targetString] = equation.split('=');
const numbers = numberString?.split(' ').map((string) => Number(string)).filter(Boolean);
const target = Number(targetString);
if (!numberString || !target) {
context.sendMessage('The input should be in the form 1 2 3 4 5 6 7 = 10', context.room.id);
return;
}
if (numbers.length < 2 || numbers.length > 7) {
context.sendMessage('The selection should contain at least 2 and at most 7 numbers', context.room.id);
return;
}
if (target < 100 || target > 999) {
context.sendMessage('The target must be a number between 100 and 999', context.room.id);
return;
}
if (Array.from(games.entries()).some(([roomId, game]) => roomId === context.room.id || game.target === target)) {
context.sendMessage('Nice try! Please wait for this numbers round to end :)', context.room.id);
return;
}
try {
const solutions = await solveAll(numbers, target, 3);
const bestSolution = solutions.reduce((closest, solution) => (!closest || Math.abs(target - solution.answer) < Math.abs(target - closest.answer) ? solution : closest), null);
if (!bestSolution) {
context.sendMessage(`I could not find a solution for ${numbers.join(' ')} = ${target} :(`, context.room.id);
return;
}
if (bestSolution.answer === target) {
context.sendMessage(`My best solution for ${numbers.join(' ')} = ${target} is: ${style.bold(bestSolution.solution)}`, context.room.id);
return;
}
context.sendMessage(`I could not find an exact solution for ${numbers.join(' ')} = ${target}. My closest solution is: ${style.bold(bestSolution.solution)} = ${style.italic(bestSolution.answer)}`, context.room.id);
} catch (error) {
console.log(error);
}
}
function onCommand(args, context) {
if (context.subcommand === 'stop') {
stop(context, true);
@@ -358,7 +403,12 @@ function onCommand(args, context) {
return;
}
if (['calculate', 'calc', 'solve'].includes(context.subcommand || context.command)) {
if (['calculate', 'calc'].includes(context.subcommand || context.command)) {
calc(args.join(' '), context); // two from the top, four from the bottom, please Rachel
return;
}
if (['solve'].includes(context.subcommand || context.command)) {
solve(args.join(' '), context); // two from the top, four from the bottom, please Rachel
return;
}

View File

@@ -3,8 +3,7 @@
const config = require('config');
const { setTimeout: delay } = require('timers/promises');
const bhttp = require('bhttp');
// const WebSocket = require('ws');
const io = require('socket.io-client');
const WebSocket = require('ws');
const logger = require('simple-node-logger').createSimpleLogger();
const { argv } = require('yargs');
@@ -18,10 +17,9 @@ const instance = process.env.NODE_APP_INSTANCE || 'main';
logger.setLevel(argv.level || 'info');
async function auth() {
const httpSession = bhttp.session();
const username = config.uniqueUsername ? `${config.user.username}-${new Date().getTime().toString().slice(-5)}` : config.user.username;
const res = await httpSession.post(`${config.api}/session`, {
const res = await bhttp.post(`${config.api}/session`, {
...config.user,
username,
}, {
@@ -36,13 +34,17 @@ async function auth() {
return {
user: res.body,
httpSession,
sessionCookie: res.headers['set-cookie'][0],
// auth may return an explicit auth cookie domain, but we connect through the VPN domain that would break the cookie, so don't use a bhttp session and strip the domain
sessionCookie: res.headers['set-cookie'][0].replace(/Domain=.*;/, ''),
};
}
async function getWsId(httpSession) {
const res = await httpSession.get(`${config.api}/socket`);
async function getWsId(sessionCookie) {
const res = await bhttp.get(`${config.api}/socket`, {
headers: {
cookie: sessionCookie,
},
});
if (res.statusCode !== 200) {
throw new Error(`Failed to retrieve WebSocket ID: ${res.body.toString()}`);
@@ -136,37 +138,27 @@ async function connect(bot, games) {
socket.connect = async () => {
try {
const { user, httpSession, sessionCookie } = await auth();
const wsCreds = await getWsId(httpSession);
const { user, sessionCookie } = await auth();
const wsCreds = await getWsId(sessionCookie);
bot.user = user;
bot.httpSession = httpSession;
logger.info(`Attempting to connect to ${config.socket}`);
const { origin, pathname } = new URL(config.socket);
socket.io = io(origin, {
transports: ['websocket'],
path: pathname,
query: {
v: wsCreds.wsId,
t: wsCreds.timestamp,
},
extraHeaders: {
socket.ws = new WebSocket(`${config.socket}?${new URLSearchParams({ v: wsCreds.wsId, t: wsCreds.timestamp }).toString()}`, [], {
headers: {
cookie: sessionCookie,
},
});
socket.io.on('connect', () => {
logger.info(`Connected to ${config.socket}`);
});
socket.ws.on('message', async (msgData) => {
const msg = msgData.toString();
socket.io.on('connect_error', (error) => {
logger.info(`Failed to connect to ${config.socket}: ${error}`);
});
if (typeof msg === 'string' && msg.includes('pong')) {
logger.debug(`Received pong ${msg.split(':')[1]}`);
return;
}
socket.io.on('_', async (msg) => {
const [domain, data] = JSON.parse(msg);
logger.debug(`Received ${domain}: ${JSON.stringify(data)}`);
@@ -180,12 +172,18 @@ async function connect(bot, games) {
}
});
socket.io.on('close', async (info) => {
socket.ws.on('close', async (info) => {
logger.error(`WebSocket closed, reconnecting in ${config.reconnectDelay} seconds: ${info}`);
await delay(config.reconnectDelay * 1000);
socket.connect();
});
socket.ws.on('error', async (error) => {
logger.error(`WebSocket error: ${error.message}`);
});
logger.info(`Connected to ${config.socket}`);
} catch (error) {
logger.error(`Failed to connect, retrying in ${config.reconnectDelay} seconds: ${error.message}`);
@@ -195,9 +193,24 @@ async function connect(bot, games) {
};
socket.transmit = (domain, data) => {
socket.io.emit('_', JSON.stringify([domain, data]));
socket.ws.send(JSON.stringify([domain, data]));
};
function ping() {
setTimeout(() => {
if (socket.ws && socket.ws?.readyState === socket.ws?.OPEN) {
const now = Date.now();
socket.ws.send(`ping:${now}`);
logger.debug(`Sent ping ${now}`);
}
ping();
}, 10000);
}
ping();
socket.connect();
return socket;

View File

@@ -0,0 +1,25 @@
const path = require('path');
const args = require('yargs').argv;
function init() {
const scoresA = require(path.resolve(__dirname, args.a));
const scoresB = require(path.resolve(__dirname, args.b));
const sum = {};
[scoresA, scoresB].forEach((record) => {
Object.entries(record).forEach(([game, scores]) => {
if (!sum[game]) {
sum[game] = {};
}
Object.entries(scores).forEach(([id, score]) => {
sum[game][id] = (sum[game][id] || 0) + score;
});
});
});
console.log(JSON.stringify(sum, null, 4));
}
init();

View File

@@ -0,0 +1,35 @@
const path = require('path');
const args = require('yargs').argv;
const fs = require('fs').promises;
async function init() {
await fs.copyFile(args.file, `${path.basename(args.file, path.extname(args.file))}_backup_${Date.now()}.json`);
const filepath = path.join(process.cwd(), args.file);
const points = require(filepath); // eslint-disable-line
Object.entries(points).forEach(([game, scores]) => {
const originKey = Object.keys(scores).find((userKey) => userKey.includes(`:${args.origin}`));
const originScore = scores[originKey];
const targetKey = Object.keys(scores).find((userKey) => userKey.includes(`:${args.target}`));
const targetScore = scores[targetKey];
if (typeof originScore === 'undefined' || typeof targetScore === 'undefined') {
return;
}
const totalScore = targetScore + originScore;
points[game][targetKey] = targetScore + originScore;
delete points[game][originKey];
console.log(`${game} ${targetScore} (${args.target}) + ${originScore} (${args.origin}) = ${totalScore}`);
});
await fs.writeFile(filepath, JSON.stringify(points, null, 4));
console.log(`Saved ${filepath}`);
}
init();