Compare commits
11
Commits
b1f3f955c1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
678b2991ad | ||
|
|
f048c1d077 | ||
|
|
1572e74ebf | ||
|
|
fc5f8572cd | ||
|
|
1101a5fc3e | ||
|
|
e610f4212c | ||
|
|
01f356c0b1 | ||
|
|
24696f6585 | ||
|
|
9040fc7df9 | ||
|
|
1360fbb5c7 | ||
|
|
9a802b358d |
+5
-4
@@ -15,6 +15,7 @@ module.exports = {
|
||||
operators: ['admin'],
|
||||
server: 'irc.libera.chat',
|
||||
port: 6697,
|
||||
tls: true,
|
||||
socket: 'ws://127.0.0.1:3000/socket/',
|
||||
api: 'http://127.0.0.1:3000/api',
|
||||
reconnectDelay: 10, // seconds
|
||||
@@ -69,11 +70,11 @@ module.exports = {
|
||||
chat: {
|
||||
apiKey: null, // OpenAI
|
||||
validModels: [
|
||||
'gpt-3.5-turbo',
|
||||
'text-davinci-003',
|
||||
'gpt-4',
|
||||
'gpt-6-luna',
|
||||
'gpt-6.1-sol',
|
||||
],
|
||||
model: 'gpt-3.5-turbo',
|
||||
model: 'gpt-6-luna',
|
||||
reasoningEffort: 'none', // none, low, medium, high; reasoning tokens count towards the user token limit
|
||||
userTokenLimit: 20000, // daily, roughly 100+ messages or $0.04 per user
|
||||
userTokenPeriod: 24, // hours
|
||||
replyTokenLimit: 1000,
|
||||
|
||||
Generated
+1427
-2576
File diff suppressed because it is too large
Load Diff
+8
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "schat2-clive",
|
||||
"version": "1.30.11",
|
||||
"version": "1.31.3",
|
||||
"description": "Game host for SChat 2-powered chat sites",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
@@ -18,23 +18,18 @@
|
||||
"author": "Niels Simenon",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.3.0",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"bhttp": "^1.2.8",
|
||||
"bottleneck": "^2.19.5",
|
||||
"config": "^3.3.6",
|
||||
"config": "^5.0.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"html-entities": "^2.3.2",
|
||||
"inflect": "^0.5.0",
|
||||
"irc": "^0.5.2",
|
||||
"irc-colors": "^1.5.0",
|
||||
"irc-upd": "^0.11.0",
|
||||
"jsdom": "^18.1.0",
|
||||
"irc-framework": "^4.14.0",
|
||||
"knex": "^2.4.2",
|
||||
"linkify-it": "^3.0.3",
|
||||
"markov-strings": "^3.0.1",
|
||||
"mathjs": "^11.8.0",
|
||||
"simple-node-logger": "^21.8.12",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"string-similarity": "^4.0.4",
|
||||
"tensify": "^0.0.4",
|
||||
"vm2": "^3.9.11",
|
||||
@@ -43,7 +38,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.16.0",
|
||||
"eslint": "^7.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1"
|
||||
},
|
||||
"allowScripts": {
|
||||
"better-sqlite3@13.0.3": true
|
||||
}
|
||||
}
|
||||
|
||||
+38
-17
@@ -2,6 +2,7 @@
|
||||
|
||||
const config = require('config');
|
||||
const bhttp = require('bhttp');
|
||||
const { format } = require('date-fns');
|
||||
|
||||
const style = require('../utils/style');
|
||||
const knex = require('../knex');
|
||||
@@ -145,6 +146,20 @@ function setRule(rule, context) {
|
||||
context.sendMessage(`Chat rule must be at least 3 characters long, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
}
|
||||
|
||||
function getInstructions(context) {
|
||||
return {
|
||||
role: 'developer',
|
||||
content: [
|
||||
`You are ${config.user.username}, ${settings.rule}, chatting in a group chat.`,
|
||||
`You are talking to ${context.user.username}. Today is ${format(new Date(), 'EEEE d MMMM yyyy')}.`,
|
||||
config.platform === 'irc'
|
||||
? 'Reply in plain text on a single line, IRC does not support any formatting.'
|
||||
: 'Reply in a single paragraph without headings or lists. You may use **bold** and *italic*.',
|
||||
settings.replyWordLimit && `Use ${settings.replyWordLimit} words or fewer.`,
|
||||
].filter(Boolean).join(' '),
|
||||
};
|
||||
}
|
||||
|
||||
async function getTokens(username) {
|
||||
const { used_tokens: usedTokens } = await knex('chat_tokens')
|
||||
.sum('tokens as used_tokens')
|
||||
@@ -229,21 +244,17 @@ async function onCommand(args, context, isConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
role: 'user',
|
||||
content: settings.replyWordLimit
|
||||
? `Using ${settings.replyWordLimit} words or fewer, answer as if you're ${settings.rule}. ${prompt}`
|
||||
: `Answer as if you're ${settings.rule}. ${prompt}`,
|
||||
};
|
||||
|
||||
const message = { role: 'user', content: prompt };
|
||||
const userHistory = (history.get(context.user.username) || []).concat(message);
|
||||
|
||||
context.logger.info(`${context.user.username} asked OpenAI '${config.chat.model}' (${userHistory.length}): ${message.content}`);
|
||||
context.logger.info(`${context.user.username} asked OpenAI '${settings.model}' (${userHistory.length}) to be ${settings.rule}: ${prompt}`);
|
||||
|
||||
const res = await bhttp.post('https://api.openai.com/v1/chat/completions', JSON.stringify({
|
||||
model: settings.model,
|
||||
max_tokens: config.chat.replyTokenLimit,
|
||||
messages: userHistory,
|
||||
max_completion_tokens: config.chat.replyTokenLimit,
|
||||
reasoning_effort: config.chat.reasoningEffort,
|
||||
temperature: settings.temperature,
|
||||
messages: [getInstructions(context), ...userHistory], // instructions are rebuilt for every request and not stored in history, so rule changes apply immediately
|
||||
user: `${context.user.id}:${context.user.username}`,
|
||||
}), {
|
||||
headers: {
|
||||
@@ -252,11 +263,20 @@ async function onCommand(args, context, isConversation) {
|
||||
},
|
||||
});
|
||||
|
||||
const reply = res.body.choices?.[0].message;
|
||||
if (res.statusCode !== 200) {
|
||||
throw new Error(`OpenAI responded with ${res.statusCode}: ${res.body.error?.message || JSON.stringify(res.body)}`);
|
||||
}
|
||||
|
||||
const reply = res.body.choices?.[0]?.message;
|
||||
|
||||
if (!reply?.content) {
|
||||
throw new Error(`OpenAI returned no content (finish reason '${res.body.choices?.[0]?.finish_reason}', ${res.body.usage?.total_tokens} tokens)`);
|
||||
}
|
||||
|
||||
const target = prompt.match(/@\s*[\w-]+\s*$/)?.[0].slice(1).trim();
|
||||
const curatedContent = reply.content.replace(/\n+/g, '. ');
|
||||
|
||||
context.logger.info(`OpenAI ${config.chat.model} replied to ${target || context.user.username} (${res.body.usage.total_tokens} tokens, ${(usedTokens || 0) + res.body.usage.total_tokens}/${config.chat.userTokenLimit} used): ${curatedContent}`);
|
||||
context.logger.info(`OpenAI ${settings.model} replied to ${target || context.user.username} (${res.body.usage.total_tokens} tokens, ${(usedTokens || 0) + res.body.usage.total_tokens}/${config.chat.userTokenLimit} used): ${curatedContent}`);
|
||||
|
||||
if (isConversation) {
|
||||
context.sendMessage(`${curatedContent}`, context.room?.id, { label: false }, context.user.username);
|
||||
@@ -270,15 +290,16 @@ async function onCommand(args, context, isConversation) {
|
||||
created: knex.raw("datetime('now')"),
|
||||
});
|
||||
|
||||
if (config.chat.history > 0) {
|
||||
history.set(context.user.username, userHistory.concat(reply).slice(-settings.history));
|
||||
if (settings.history > 0) {
|
||||
// only keep the fields the API accepts as input, newer models return extras like refusal and annotations
|
||||
history.set(context.user.username, userHistory.concat({ role: reply.role, content: reply.content }).slice(-settings.history));
|
||||
}
|
||||
} catch (error) {
|
||||
context.logger.error(error.response ? `${error.response.status}: ${JSON.stringify(error.response.data)}` : error.message);
|
||||
context.logger.error(error.message);
|
||||
context.sendMessage('Sorry, I can\'t think right now', context.room?.id, { label: false }, context.user.username);
|
||||
} finally {
|
||||
processing.delete(context.user.username);
|
||||
}
|
||||
|
||||
processing.delete(context.user.username);
|
||||
}
|
||||
|
||||
function onMessage(message, context) {
|
||||
|
||||
+12
-13
@@ -18,6 +18,7 @@ const availableLetters = {
|
||||
const types = { v: 'vowel', c: 'consonant' };
|
||||
|
||||
const settings = { ...config.letters };
|
||||
const minReminderInterval = 10; // seconds
|
||||
const games = new Map();
|
||||
|
||||
function getBoard(context) {
|
||||
@@ -106,22 +107,20 @@ async function play(context) {
|
||||
game.state = 'words';
|
||||
game.counts = countLetters(game.word);
|
||||
|
||||
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(settings.timeout))} seconds, let's start!`, context.room.id);
|
||||
// lock in the timeout so changing it mid-round doesn't skew the countdown
|
||||
const { timeout } = settings;
|
||||
const steps = Math.max(1, Math.min(5, Math.floor(timeout / minReminderInterval))); // don't repeat the board too often on short timeouts
|
||||
|
||||
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(timeout))} seconds, let's start!`, context.room.id);
|
||||
|
||||
try {
|
||||
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
|
||||
for (let step = 1; step <= steps; step += 1) {
|
||||
await timers.setTimeout((timeout / steps) * 1000, null, { signal: game.ac.signal }); // eslint-disable-line no-await-in-loop
|
||||
|
||||
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 5) * 4)} seconds`))} left`, context.room.id);
|
||||
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
|
||||
|
||||
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 5) * 3)} seconds`))} left`, context.room.id);
|
||||
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
|
||||
|
||||
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 5) * 2)} seconds`))} left`, context.room.id);
|
||||
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
|
||||
|
||||
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round(settings.timeout / 5)} seconds`))} left`, context.room.id);
|
||||
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
|
||||
if (step < steps) {
|
||||
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((timeout / steps) * (steps - step))} seconds`))} left`, context.room.id);
|
||||
}
|
||||
}
|
||||
|
||||
stop(context);
|
||||
} catch (error) {
|
||||
|
||||
+12
-7
@@ -41,6 +41,7 @@ limitedMath.import({
|
||||
}, { override: true });
|
||||
|
||||
const settings = { ...config.numbers };
|
||||
const minReminderInterval = 10; // seconds
|
||||
const games = new Map();
|
||||
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
@@ -177,17 +178,21 @@ async function play(context) {
|
||||
|
||||
game.state = 'solutions';
|
||||
|
||||
context.sendMessage(`${getBoard(context)}. You have ${style.bold(style.green(settings.timeout))} seconds, let's start!`, context.room.id);
|
||||
// lock in the timeout so changing it mid-round doesn't skew the countdown
|
||||
const { timeout } = settings;
|
||||
const steps = Math.max(1, Math.min(3, Math.floor(timeout / minReminderInterval))); // don't repeat the board too often on short timeouts
|
||||
|
||||
context.sendMessage(`${getBoard(context)}. You have ${style.bold(style.green(timeout))} seconds, let's start!`, context.room.id);
|
||||
context.logger.info(`Numbers game started by ${context.user.username}, numbers ${game.numbers.join('|')}, target ${game.target}, ${game.solution.answer === game.target ? 'exact' : 'closest'} solution for ${game.solution.answer} is: ${game.solution.solution}`);
|
||||
|
||||
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);
|
||||
for (let step = 1; step <= steps; step += 1) {
|
||||
await timers.setTimeout((timeout / steps) * 1000, null, { signal: game.ac.signal }); // eslint-disable-line no-await-in-loop
|
||||
|
||||
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);
|
||||
|
||||
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
|
||||
if (step < steps) {
|
||||
context.sendMessage(`${getBoard(context)}. You have ${style.bold(style.green(`${Math.round((timeout / steps) * (steps - step))} seconds`))} left`, context.room.id);
|
||||
}
|
||||
}
|
||||
|
||||
stop(context);
|
||||
} catch (error) {
|
||||
|
||||
+10
-1
@@ -226,6 +226,15 @@ function onCommand(args, context) {
|
||||
}
|
||||
}
|
||||
|
||||
// answers are plain text and may contain regex characters, like '*batteries not included'
|
||||
function getAnswerRegexp(answer) {
|
||||
const pattern = answer
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\\[.?*]|!/g, '$&?'); // punctuation is optional, so T.S. Eliot also matches TS Eliot, and *NSYNC matches NSYNC
|
||||
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
|
||||
async function onMessage(message, context) {
|
||||
const game = games.get(context.room?.id);
|
||||
|
||||
@@ -235,7 +244,7 @@ async function onMessage(message, context) {
|
||||
|
||||
const { answer, fullAnswer } = game.questions[game.round];
|
||||
|
||||
if (new RegExp(answer, 'i').test(decode(message.originalBody || message.body)) && !game.answers.has(context.user.id)) { // resolve HTML entities in case original body is not available, such as & to &
|
||||
if (getAnswerRegexp(answer).test(decode(message.originalBody || message.body)) && !game.answers.has(context.user.id)) { // resolve HTML entities in case original body is not available, such as & to &
|
||||
game.answers.set(context.user.id, {
|
||||
user: context.user,
|
||||
answer: message.body,
|
||||
|
||||
+88
-28
@@ -1,10 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const irc = require('irc');
|
||||
const logger = require('simple-node-logger').createSimpleLogger();
|
||||
const { Client } = require('irc-framework');
|
||||
const { argv } = require('yargs');
|
||||
// const timers = require('timers/promises');
|
||||
const logger = require('./utils/logger')();
|
||||
|
||||
const {
|
||||
onMessage,
|
||||
@@ -13,15 +12,22 @@ const {
|
||||
|
||||
logger.setLevel(argv.level || 'info');
|
||||
|
||||
const client = new irc.Client(config.server, config.user.id, {
|
||||
userName: config.user.username,
|
||||
realName: config.user.realName,
|
||||
password: config.user.password,
|
||||
port: config.port,
|
||||
secure: true,
|
||||
});
|
||||
function reclaimNick(client, currentNick) {
|
||||
logger.info(`Connected as '${currentNick}' because '${config.user.id}' was taken, asking NickServ to release it`);
|
||||
|
||||
client.say('NickServ', `GHOST ${config.user.id}`);
|
||||
|
||||
// NickServ disconnects the stale session, give it a moment before taking the nick back
|
||||
setTimeout(() => {
|
||||
if (client.connected && client.user.nick !== config.user.id) {
|
||||
client.changeNick(config.user.id);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const client = new Client();
|
||||
|
||||
const bot = {
|
||||
client,
|
||||
rooms: config.channels.map((channel) => ({
|
||||
@@ -32,35 +38,89 @@ async function init() {
|
||||
|
||||
const games = await getGames(bot, config.user.id);
|
||||
|
||||
// registered fires again on every reconnect, so these must only be bound once
|
||||
client.addListener('message', (from, to, body) => onMessage({
|
||||
from,
|
||||
to,
|
||||
recipient: /^#/.test(to) ? null : to,
|
||||
body,
|
||||
// client.connected is true as soon as the socket opens, not after registration as documented
|
||||
let registered = false;
|
||||
|
||||
client.on('socket connected', () => { registered = false; });
|
||||
|
||||
// only plain messages, replying to notices is bad IRC bot etiquette and risks loops with services and other bots
|
||||
client.on('privmsg', (event) => onMessage({
|
||||
from: event.nick,
|
||||
to: event.target,
|
||||
recipient: /^#/.test(event.target) ? null : event.target,
|
||||
body: event.message,
|
||||
type: 'message',
|
||||
}, bot, games));
|
||||
|
||||
client.addListener('error', (error) => {
|
||||
logger.error(error);
|
||||
client.on('nick in use', (event) => {
|
||||
// registration stalls until we pick another nick
|
||||
if (!registered) {
|
||||
logger.warn(`Nick '${event.nick}' is in use, trying '${event.nick}_'`);
|
||||
client.changeNick(`${event.nick}_`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Nick '${event.nick}' is still in use: ${event.reason}`);
|
||||
});
|
||||
|
||||
client.addListener('registered', () => {
|
||||
logger.info('Connected!');
|
||||
logger.info('Identifying with NickServ');
|
||||
client.on('nick', (event) => event.new_nick === config.user.id && logger.info(`Reclaimed nick '${config.user.id}'`));
|
||||
client.on('loggedin', (event) => logger.info(`Logged in to account '${event.account}'`));
|
||||
client.on('sasl failed', (event) => logger.error(`SASL authentication failed: ${event.reason} ${event.message || ''}`));
|
||||
client.on('irc error', (event) => logger.error(`IRC error: ${JSON.stringify(event)}`));
|
||||
client.on('kick', (event) => event.kicked === client.user.nick && logger.warn(`Kicked from ${event.channel} by ${event.nick}: ${event.message}`));
|
||||
client.on('reconnecting', (event) => logger.warn(`Disconnected, reconnect attempt ${event.attempt} in ${Math.round(event.wait / 1000)} seconds`));
|
||||
|
||||
client.say('nickserv', `IDENTIFY ${config.user.username} ${config.user.password}`);
|
||||
// the library only auto-reconnects after a successful registration, cover failed (initial) connections too
|
||||
client.on('close', () => {
|
||||
logger.error(`Connection closed, reconnecting in ${config.reconnectDelay} seconds`);
|
||||
setTimeout(() => client.connect(), config.reconnectDelay * 1000);
|
||||
});
|
||||
|
||||
client.on('join', (event) => {
|
||||
if (event.nick !== client.user.nick) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Joined ${event.channel}`);
|
||||
|
||||
if (config.greeting) {
|
||||
client.say(event.channel, config.greeting);
|
||||
}
|
||||
});
|
||||
|
||||
// fires again on every reconnect
|
||||
client.on('registered', (event) => {
|
||||
registered = true;
|
||||
|
||||
// client.user.nick is only updated after this listener runs, use the nick from the event
|
||||
logger.info(`Connected to ${config.server} as '${event.nick}'`);
|
||||
|
||||
if (event.nick !== config.user.id && config.user.password) {
|
||||
reclaimNick(client, event.nick);
|
||||
}
|
||||
|
||||
config.channels.forEach((channel) => {
|
||||
logger.info(`Joining ${channel}`);
|
||||
|
||||
client.join(channel, () => logger.info(`Joined ${channel}`));
|
||||
|
||||
if (config.greeting) {
|
||||
client.say(channel, config.greeting);
|
||||
}
|
||||
client.join(channel);
|
||||
});
|
||||
});
|
||||
|
||||
client.connect({
|
||||
host: config.server,
|
||||
port: config.port,
|
||||
tls: config.tls,
|
||||
nick: config.user.id,
|
||||
username: config.user.username,
|
||||
gecos: config.user.realName,
|
||||
// SASL logs in before registration completes, so channels requiring an identified user can be joined straight away
|
||||
account: config.user.password
|
||||
? { account: config.user.username, password: config.user.password }
|
||||
: {},
|
||||
auto_reconnect: true,
|
||||
auto_reconnect_max_retries: Infinity,
|
||||
auto_reconnect_max_wait: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
|
||||
+1
-1
@@ -2,9 +2,9 @@
|
||||
|
||||
const config = require('config');
|
||||
const fs = require('fs').promises;
|
||||
const logger = require('simple-node-logger').createSimpleLogger();
|
||||
const { getWeek } = require('date-fns');
|
||||
const { argv } = require('yargs');
|
||||
const logger = require('./utils/logger')();
|
||||
// const timers = require('timers/promises');
|
||||
|
||||
const getLeaders = require('./utils/get-leaders');
|
||||
|
||||
+1
-1
@@ -4,8 +4,8 @@ const config = require('config');
|
||||
const { setTimeout: delay } = require('timers/promises');
|
||||
const bhttp = require('bhttp');
|
||||
const WebSocket = require('ws');
|
||||
const logger = require('simple-node-logger').createSimpleLogger();
|
||||
const { argv } = require('yargs');
|
||||
const logger = require('./utils/logger')();
|
||||
|
||||
const {
|
||||
onMessage,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
|
||||
// drop-in for simple-node-logger's createSimpleLogger, which no longer runs on recent Node versions
|
||||
const levels = ['debug', 'info', 'warn', 'error'];
|
||||
|
||||
function createLogger() {
|
||||
let minLevel = levels.indexOf('info');
|
||||
|
||||
function log(level, args) {
|
||||
if (levels.indexOf(level) < minLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const time = new Date().toTimeString().slice(0, 8);
|
||||
const ms = String(new Date().getMilliseconds()).padStart(3, '0');
|
||||
// everything goes to stdout like before, so pm2 keeps writing it all to the same log file
|
||||
console.log(`${time}.${ms} ${level.toUpperCase().padEnd(5)} ${util.format(...args)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
setLevel: (level) => {
|
||||
if (levels.includes(level)) {
|
||||
minLevel = levels.indexOf(level);
|
||||
}
|
||||
},
|
||||
...Object.fromEntries(levels.map((level) => [level, (...args) => log(level, args)])),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createLogger;
|
||||
Reference in New Issue
Block a user