pokemon-showdown/roomlogs.js
Guangcong Luo 2f0be453cc Gen 3: Fix Pressure PP tracking
This commit makes it so players are always informed about their own
Pressure activations. This fixes Pressure PP tracking for players.
PP tracking for spectators will still be wrong; there's no fix for
that.

Fixes https://github.com/Zarel/Pokemon-Showdown-Client/issues/766
2018-10-16 21:00:03 -05:00

310 lines
8.2 KiB
JavaScript

/**
* Roomlogs
* Pokemon Showdown - http://pokemonshowdown.com/
*
* This handles data storage for rooms.
*
* @license MIT license
*/
'use strict';
const FS = require('./lib/fs');
/**
* Most rooms have three logs:
* - scrollback
* - roomlog
* - modlog
* This class keeps track of all three.
*
* The scrollback is stored in memory, and is the log you get when you
* join the room. It does not get moderator messages.
*
* The modlog is stored in
* `logs/modlog/modlog_<ROOMID>.txt`
* It contains moderator messages, formatted for ease of search.
*
* The roomlog is stored in
* `logs/chat/<ROOMID>/<YEAR>-<MONTH>/<YEAR>-<MONTH>-<DAY>.txt`
* It contains (nearly) everything.
*/
class Roomlog {
/**
* @param {BasicChatRoom} room
* @param {{isMultichannel?: any, autoTruncate?: any, logTimes?: any}} [options]
*/
constructor(room, options = {}) {
this.id = room.id;
/**
* Scrollback log
* @type {string[]}
*/
this.log = [];
this.broadcastBuffer = '';
/**
* Battle rooms are multichannel, which means their logs are split
* into four channels, public, p1, p2, full.
*/
this.isMultichannel = !!options.isMultichannel;
/**
* Chat rooms auto-truncate, which means it only stores the recent
* messages, if there are more.
*/
this.autoTruncate = !!options.autoTruncate;
/**
* Chat rooms include timestamps.
*/
this.logTimes = !!options.logTimes;
/**
* undefined = uninitialized,
* null = disabled
* @type {WriteStream? | undefined}
*/
this.modlogStream = undefined;
/**
* undefined = uninitialized,
* null = disabled
* @type {WriteStream? | undefined}
*/
this.roomlogStream = undefined;
// modlog/roomlog state
this.sharedModlog = false;
// TypeScript bug: can't infer
/** @type {string} */
this.roomlogFilename = '';
this.setupModlogStream();
this.setupRoomlogStream(true);
}
getScrollback(channel = 0) {
let log = this.log;
if (this.logTimes) log = [`|:|${~~(Date.now() / 1000)}`].concat(log);
if (!this.isMultichannel) {
return log.join('\n') + '\n';
}
log = [];
for (let i = 0; i < this.log.length; ++i) {
const line = this.log[i];
if (line === '|split') {
const ownLine = this.log[i + channel + 1];
if (ownLine) log.push(ownLine);
i += 4;
} else {
log.push(line);
}
}
let textLog = log.join('\n') + '\n';
if (channel === 0) {
return textLog.replace(/\n\|choice\|\|\n/g, '\n').replace(/\n\|seed\|\n/g, '\n');
}
return textLog;
}
setupModlogStream() {
if (this.modlogStream !== undefined) return;
if (!this.id.includes('-')) {
this.modlogStream = FS(`logs/modlog/modlog_${this.id}.txt`).createAppendStream();
return;
}
const sharedStreamId = this.id.split('-')[0];
let stream = Roomlogs.sharedModlogs.get(sharedStreamId);
if (!stream) {
stream = FS(`logs/modlog/modlog_${sharedStreamId}.txt`).createAppendStream();
Roomlogs.sharedModlogs.set(sharedStreamId, stream);
}
this.modlogStream = stream;
this.sharedModlog = true;
}
async setupRoomlogStream(sync = false) {
if (this.roomlogStream === null) return;
if (!Config.logchat) {
this.roomlogStream = null;
return;
}
if (this.id.startsWith('battle-')) {
this.roomlogStream = null;
return;
}
const date = new Date();
const dateString = Chat.toTimestamp(date).split(' ')[0];
const monthString = dateString.split('-', 2).join('-');
const basepath = `logs/chat/${this.id}/`;
const relpath = `${monthString}/${dateString}.txt`;
if (relpath === this.roomlogFilename) return;
if (sync) {
FS(basepath + monthString).mkdirpSync();
} else {
await FS(basepath + monthString).mkdirp();
if (this.roomlogStream === null) return;
}
this.roomlogFilename = relpath;
if (this.roomlogStream) this.roomlogStream.end();
this.roomlogStream = FS(basepath + relpath).createAppendStream();
// Create a symlink to today's lobby log.
// These operations need to be synchronous, but it's okay
// because this code is only executed once every 24 hours.
let link0 = basepath + 'today.txt.0';
FS(link0).unlinkIfExistsSync();
try {
FS(link0).symlinkToSync(relpath); // intentionally a relative link
FS(link0).renameSync(basepath + 'today.txt');
} catch (e) {} // OS might not support symlinks or atomic rename
if (!Roomlogs.rollLogTimer) Roomlogs.rollLogs();
}
/**
* @param {string} message
*/
add(message) {
if (message.startsWith('|uhtmlchange|')) return this.uhtmlchange(message);
this.roomlog(message);
if (this.logTimes && message.startsWith('|c|')) {
message = '|c:|' + (~~(Date.now() / 1000)) + '|' + message.substr(3);
}
this.log.push(message);
this.broadcastBuffer += message + '\n';
return this;
}
/**
* @param {string} username
*/
hasUsername(username) {
const userid = toId(username);
for (const line of this.log) {
if (line.startsWith('|c:|')) {
const curUserid = toId(line.split('|', 4)[3]);
if (curUserid === userid) return true;
} else if (line.startsWith('|c|')) {
const curUserid = toId(line.split('|', 3)[2]);
if (curUserid === userid) return true;
}
}
return false;
}
/**
* @param {string[]} userids
*/
clearText(userids) {
const messageStart = this.logTimes ? '|c:|' : '|c|';
const section = this.logTimes ? 4 : 3; // ['', 'c' timestamp?, author, message]
/** @type {string[]} */
let cleared = [];
this.log = this.log.filter(line => {
if (line.startsWith(messageStart)) {
const parts = Chat.splitFirst(line, '|', section);
const userid = toId(parts[section - 1]);
if (userids.includes(userid)) {
if (!cleared.includes(userid)) cleared.push(userid);
if (this.id.startsWith('battle-')) return true; // Don't remove messages in battle rooms to preserve evidence
return false;
}
}
return true;
});
return cleared;
}
/**
* @param {string} message
*/
uhtmlchange(message) {
const thirdPipe = message.indexOf('|', 13);
const originalStart = '|uhtml|' + message.slice(13, thirdPipe + 1);
for (const [i, line] of this.log.entries()) {
if (line.startsWith(originalStart)) {
this.log[i] = originalStart + message.slice(thirdPipe + 1);
break;
}
}
this.broadcastBuffer += message + '\n';
return this;
}
/**
* @param {string} message
*/
roomlog(message, date = new Date()) {
if (!this.roomlogStream) return;
const timestamp = Chat.toTimestamp(date).split(' ')[1] + ' ';
message = message.replace(/<img[^>]* src="data:image\/png;base64,[^">]+"[^>]*>/g, '');
this.roomlogStream.write(timestamp + message + '\n');
}
/**
* @param {string} message
*/
modlog(message) {
if (!this.modlogStream) return;
this.modlogStream.write('[' + (new Date().toJSON()) + '] ' + message + '\n');
}
static async rollLogs() {
if (Roomlogs.rollLogTimer === true) return;
if (Roomlogs.rollLogTimer) {
clearTimeout(Roomlogs.rollLogTimer);
}
Roomlogs.rollLogTimer = true;
for (const log of Roomlogs.roomlogs.values()) {
await log.setupRoomlogStream();
}
const time = Date.now();
const nextMidnight = new Date(time + 24 * 60 * 60 * 1000);
nextMidnight.setHours(0, 0, 1);
Roomlogs.rollLogTimer = setTimeout(() => Roomlog.rollLogs(), nextMidnight.getTime() - time);
}
truncate() {
if (!this.autoTruncate) return;
if (this.log.length > 100) {
this.log.splice(0, this.log.length - 100);
}
}
destroy() {
let promises = [];
if (this.sharedModlog) {
this.modlogStream = null;
}
if (this.modlogStream) {
promises.push(this.modlogStream.end());
this.modlogStream = null;
}
if (this.roomlogStream) {
promises.push(this.roomlogStream.end());
this.roomlogStream = null;
}
Roomlogs.roomlogs.delete(this.id);
return Promise.all(promises);
}
}
/** @type {Map<string, WriteStream>} */
const sharedModlogs = new Map();
/** @type {Map<string, Roomlog>} */
const roomlogs = new Map();
/**
* @param {BasicChatRoom} room
*/
function createRoomlog(room, options = {}) {
let roomlog = Roomlogs.roomlogs.get(room.id);
if (roomlog) throw new Error(`Roomlog ${room.id} already exists`);
roomlog = new Roomlog(room, options);
Roomlogs.roomlogs.set(room.id, roomlog);
return roomlog;
}
const Roomlogs = {
create: createRoomlog,
Roomlog,
roomlogs,
sharedModlogs,
rollLogs: Roomlog.rollLogs,
/** @type {NodeJS.Timer? | true} */
rollLogTimer: null,
};
module.exports = Roomlogs;