refactor player data storage completely
This commit is contained in:
parent
69966ec746
commit
f82cbb736c
10 changed files with 297 additions and 63 deletions
|
@ -35,7 +35,7 @@ module.exports = {
|
||||||
node.permissionLevel = 0
|
node.permissionLevel = 0
|
||||||
},
|
},
|
||||||
|
|
||||||
sendCommand (context) {
|
async sendCommand (context) {
|
||||||
const source = context.source
|
const source = context.source
|
||||||
const bot = source.bot
|
const bot = source.bot
|
||||||
const player = source.getPlayerOrThrow()
|
const player = source.getPlayerOrThrow()
|
||||||
|
@ -43,7 +43,13 @@ module.exports = {
|
||||||
const username = context.getArgument('username')
|
const username = context.getArgument('username')
|
||||||
const message = context.getArgument('message')
|
const message = context.getArgument('message')
|
||||||
|
|
||||||
bot.sendMail(player.username, username, message)
|
try {
|
||||||
|
await bot.sendMail(player.username, username, message)
|
||||||
|
} catch (error) {
|
||||||
|
source.sendError('Unable to send mail: ' + error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
bot.tellraw([
|
bot.tellraw([
|
||||||
{ text: 'Sent ', ...bot.styles.primary },
|
{ text: 'Sent ', ...bot.styles.primary },
|
||||||
{ text: message, ...bot.styles.secondary },
|
{ text: message, ...bot.styles.secondary },
|
||||||
|
@ -56,16 +62,17 @@ module.exports = {
|
||||||
const source = context.source
|
const source = context.source
|
||||||
const bot = source.bot
|
const bot = source.bot
|
||||||
const player = source.getPlayerOrThrow()
|
const player = source.getPlayerOrThrow()
|
||||||
|
const playerData = bot.playerData[player.uuid]
|
||||||
|
|
||||||
const messages = bot.mail[player.username]
|
const messages = playerData.data?.mail
|
||||||
if (!messages || messages.length < 1) {
|
if (!messages || !messages.length) {
|
||||||
bot.tellraw({ text: 'You have no mail', ...bot.styles.primary }, createUuidSelector(player.uuid))
|
bot.tellraw({ text: 'You have no mail', ...bot.styles.primary }, createUuidSelector(player.uuid))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = [{ text: 'Mail:\n', ...bot.styles.primary }]
|
const msg = [{ text: 'Mail:\n', ...bot.styles.primary }]
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
msg.push(`${message.sender} (from ${message.host}): `)
|
msg.push(`${message.sender} (from ${message.host}:${message.port}): `)
|
||||||
msg.push({ text: `${message.message}\n`, ...bot.styles.secondary })
|
msg.push({ text: `${message.message}\n`, ...bot.styles.secondary })
|
||||||
})
|
})
|
||||||
msg[msg.length - 1].text = msg[msg.length - 1].text.slice(0, -1)
|
msg[msg.length - 1].text = msg[msg.length - 1].text.slice(0, -1)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const { literal, argument, string, DynamicCommandExceptionType } = require('brigadier-commands')
|
const { literal, argument, string, DynamicCommandExceptionType } = require('brigadier-commands')
|
||||||
const TextMessage = require('../util/command/text_message')
|
const TextMessage = require('../util/command/text_message')
|
||||||
|
|
||||||
const NEVER_SEEN_ERROR = new DynamicCommandExceptionType(username => new TextMessage([username, ' was never seen']))
|
const NEVER_SEEN_ERROR = new DynamicCommandExceptionType(username => new TextMessage([username, ' was never seen']))
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -16,14 +17,26 @@ module.exports = {
|
||||||
node.permissionLevel = 0
|
node.permissionLevel = 0
|
||||||
},
|
},
|
||||||
|
|
||||||
seenCommand (context) {
|
async seenCommand (context) {
|
||||||
const source = context.source
|
const source = context.source
|
||||||
const bot = source.bot
|
const bot = source.bot
|
||||||
|
|
||||||
const username = context.getArgument('username')
|
const username = context.getArgument('username')
|
||||||
if (bot.seen[username] == null) throw NEVER_SEEN_ERROR.create(username)
|
|
||||||
|
|
||||||
const { first, last } = bot.seen[username]
|
let playerData = Object.values(bot.playerData).find(playerData => playerData.data.username === username)
|
||||||
|
|
||||||
|
if (!playerData) {
|
||||||
|
try {
|
||||||
|
playerData = await bot.loadPlayerData(username)
|
||||||
|
} catch (error) {
|
||||||
|
source.sendError('Unable to load player data: ' + error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playerData.data?.seen) throw NEVER_SEEN_ERROR.create()
|
||||||
|
|
||||||
|
const { first, last } = playerData.data.seen
|
||||||
source.sendFeedback([
|
source.sendFeedback([
|
||||||
{ text: '', ...bot.styles.primary },
|
{ text: '', ...bot.styles.primary },
|
||||||
{ text: username, ...bot.styles.secondary },
|
{ text: username, ...bot.styles.secondary },
|
||||||
|
|
|
@ -1,27 +1,19 @@
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const filepath = path.resolve('persistent', 'mail.json')
|
|
||||||
|
|
||||||
// load the blacklist
|
|
||||||
let mail = {}
|
|
||||||
try {
|
|
||||||
mail = require(filepath)
|
|
||||||
} catch (e) {
|
|
||||||
console.log('An error occured while loading the mail.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// save it every 5 minutes
|
|
||||||
setInterval(() => {
|
|
||||||
fs.writeFileSync(filepath, JSON.stringify(mail))
|
|
||||||
}, 5 * 6000)
|
|
||||||
|
|
||||||
// make the bot uuid ban blacklisted players
|
|
||||||
function inject (bot) {
|
function inject (bot) {
|
||||||
bot.mail = mail
|
bot.sendMail = sendMail
|
||||||
bot.sendMail = (sender, reciever, message) => {
|
|
||||||
if (!mail[reciever]) mail[reciever] = []
|
async function sendMail (sender, receiver, message) {
|
||||||
mail[reciever].push({ sender: sender, message, host: bot.host })
|
let playerData = Object.values(bot.playerData).find(playerData => playerData.data.username === receiver)
|
||||||
|
|
||||||
|
let loadedManually = false
|
||||||
|
if (!playerData) {
|
||||||
|
playerData = await bot.loadPlayerData(receiver)
|
||||||
|
loadedManually = true
|
||||||
|
}
|
||||||
|
|
||||||
|
playerData.data.mail ??= []
|
||||||
|
playerData.data.mail.push({ sender: sender, message, host: bot.host, port: bot.port })
|
||||||
|
|
||||||
|
if (loadedManually) await playerData.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ function inject (bot, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.matrix.client.on('Room.timeline', (event, room, toStartOfTimeline) => {
|
bot.matrix.client.on('Room.timeline', (event, room, toStartOfTimeline) => {
|
||||||
if (event.getRoomId() !== bot.matrix.roomId || event.getType() !== 'm.room.message' || event.getTs() < startTime || event.sender.userId === bot.matrix.client.getUserId()) return
|
if (event.getRoomId() !== bot.matrix.roomId || event.getType() !== 'm.room.message' || event.getTs() < startTime || event.sender.userId === bot.matrix.client.getUserId() || !bot.loggedIn) return
|
||||||
|
|
||||||
const content = event.getContent()
|
const content = event.getContent()
|
||||||
const permissionLevel = event.sender.powerLevelNorm
|
const permissionLevel = event.sender.powerLevelNorm
|
||||||
|
|
92
plugins/player_data.js
Normal file
92
plugins/player_data.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
const fs = require('fs/promises')
|
||||||
|
const path = require('path')
|
||||||
|
const nbt = require('prismarine-nbt')
|
||||||
|
const PlayerData = require('../util/player_data')
|
||||||
|
const fileExists = require('../util/file_exists')
|
||||||
|
const getOfflineUUID = require('../util/offline_player_uuid')
|
||||||
|
|
||||||
|
const playerData = {}
|
||||||
|
|
||||||
|
async function inject (bot) {
|
||||||
|
const persistentDir = bot.paths.persistent
|
||||||
|
const playerDataDir = path.join(persistentDir, 'playerdata')
|
||||||
|
|
||||||
|
if (!await fileExists(playerDataDir)) await fs.mkdir(playerDataDir)
|
||||||
|
|
||||||
|
bot.playerData = playerData
|
||||||
|
bot.loadPlayerData = loadPlayerData
|
||||||
|
|
||||||
|
if (bot.loggedIn && bot.players) {
|
||||||
|
for (const player of bot.players) {
|
||||||
|
// If we logged in while the async file operations were happening (highly unlikely but possible), load the player data for the players on the server
|
||||||
|
handlePlayerAdded(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.on('player_added', handlePlayerAdded)
|
||||||
|
|
||||||
|
|
||||||
|
async function handlePlayerAdded (player) {
|
||||||
|
if (player.uuid === bot.uuid) return
|
||||||
|
|
||||||
|
let data = playerData[player.uuid]
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
data._bots.add(bot)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data = await loadPlayerData(player.username)
|
||||||
|
data._bots.add(bot)
|
||||||
|
|
||||||
|
playerData[player.uuid] = data
|
||||||
|
data.data.username = player.username
|
||||||
|
bot.emit('player_data_loaded', player, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPlayerData (uuid) {
|
||||||
|
if (uuid.length <= 16) {
|
||||||
|
// Usernames
|
||||||
|
uuid = getOfflineUUID(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new PlayerData(path.join(playerDataDir, uuid + '.dat'))
|
||||||
|
await data.load()
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.on('player_removed', async player => {
|
||||||
|
const data = playerData[player.uuid]
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
data._bots.delete(bot)
|
||||||
|
if (data._bots.size) return
|
||||||
|
|
||||||
|
bot.emit('player_data_unloading', player, data)
|
||||||
|
|
||||||
|
await data.unload(true)
|
||||||
|
delete bot.playerData[player.uuid]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAll () {
|
||||||
|
console.log('Saving player data...')
|
||||||
|
for (const uuid in playerData) {
|
||||||
|
const data = playerData[uuid]
|
||||||
|
if (!data?.data) {
|
||||||
|
// We _somehow_ found some unloaded data
|
||||||
|
delete playerData[uuid]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await data.save()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to write data for player %s:', data?.data?.username, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Saved!')
|
||||||
|
}
|
||||||
|
setInterval(saveAll, 60 * 3 * 1000)
|
||||||
|
|
||||||
|
module.exports = inject
|
|
@ -1,42 +1,22 @@
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const filepath = path.resolve('persistent', 'seen.json')
|
|
||||||
|
|
||||||
// load the seen data
|
|
||||||
let seen = {}
|
|
||||||
try {
|
|
||||||
seen = require(filepath)
|
|
||||||
} catch (e) {
|
|
||||||
console.log('An error occured while loading seen players.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// save it every 5 minutes
|
|
||||||
setInterval(() => {
|
|
||||||
fs.writeFileSync(filepath, JSON.stringify(seen))
|
|
||||||
}, 5 * 6000)
|
|
||||||
|
|
||||||
// expose the data to the bot
|
|
||||||
function inject (bot) {
|
function inject (bot) {
|
||||||
bot.seen = seen
|
bot.on('player_data_loaded', (player, data) => {
|
||||||
|
data.data.seen ??= {}
|
||||||
|
const seenData = data.data.seen
|
||||||
|
|
||||||
bot.on('player_added', player => {
|
if (seenData.first == null) {
|
||||||
if (player.uuid === bot.uuid) return
|
seenData.first = new Date()
|
||||||
|
bot.tellraw([
|
||||||
seen[player.username] ??= {}
|
|
||||||
if (seen[player.username].first == null) {
|
|
||||||
seen[player.username].first = new Date()
|
|
||||||
bot.core.run('minecraft:tellraw @a ' + JSON.stringify([
|
|
||||||
{ text: 'Welcome ', ...bot.styles.primary },
|
{ text: 'Welcome ', ...bot.styles.primary },
|
||||||
{ text: player.username, ...bot.styles.secondary },
|
{ text: player.username, ...bot.styles.secondary },
|
||||||
' to the server!'
|
' to the server!'
|
||||||
]))
|
], '@a')
|
||||||
}
|
}
|
||||||
seen[player.username].last = new Date()
|
seenData.last = new Date()
|
||||||
})
|
})
|
||||||
|
|
||||||
bot.on('player_removed', player => {
|
bot.on('player_data_unloading', (player, data) => {
|
||||||
if (seen[player.username] != null) seen[player.username].last = new Date()
|
const seenData = data.data.seen
|
||||||
|
if (seenData != null) seenData.last = new Date()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
test.nbt
Normal file
BIN
test.nbt
Normal file
Binary file not shown.
19
util/offline_player_uuid.js
Normal file
19
util/offline_player_uuid.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
function getOfflineUUID (username) {
|
||||||
|
// Hash the UUID
|
||||||
|
const md5 = crypto.createHash('md5')
|
||||||
|
md5.update('OfflinePlayer:' + username, 'utf-8')
|
||||||
|
const hash = md5.digest()
|
||||||
|
|
||||||
|
// From https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/UUID.java#L182-L185
|
||||||
|
hash[6] &= 0x0f /* clear version */
|
||||||
|
hash[6] |= 0x30 /* set to version 3 */
|
||||||
|
hash[8] &= 0x3f /* clear variant */
|
||||||
|
hash[8] |= 0x80 /* set to IETF variant */
|
||||||
|
|
||||||
|
const hex = hash.toString('hex')
|
||||||
|
return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getOfflineUUID
|
60
util/persistent_data.js
Normal file
60
util/persistent_data.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
const fs = require('fs/promises')
|
||||||
|
const nbt = require('prismarine-nbt')
|
||||||
|
const fileExists = require('./file_exists')
|
||||||
|
|
||||||
|
class PersistentData {
|
||||||
|
#endianness
|
||||||
|
data = null
|
||||||
|
|
||||||
|
constructor (filepath) {
|
||||||
|
this.filepath = filepath
|
||||||
|
this.loaded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async load () {
|
||||||
|
const { parsed, type } = await this.#loadData(this.filepath, true)
|
||||||
|
|
||||||
|
this.data = this.parse(parsed)
|
||||||
|
this.#endianness = type
|
||||||
|
this.loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async #loadData (filepath, tryBackup) {
|
||||||
|
if (!await fileExists(filepath)) {
|
||||||
|
return { parsed: nbt.comp({}), type: 'big' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await fs.readFile(filepath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return nbt.parse(buffer)
|
||||||
|
} catch (error) {
|
||||||
|
if (tryBackup) return this.#loadData(filepath + '_old', false)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async save () {
|
||||||
|
const data = this.unparse(this.data)
|
||||||
|
if (await fileExists(this.filepath)) await fs.copyFile(this.filepath, this.filepath + '_old') // Back up our data before a save
|
||||||
|
await fs.writeFile(this.filepath, nbt.writeUncompressed(data, this.#endianness))
|
||||||
|
}
|
||||||
|
|
||||||
|
async unload (save) {
|
||||||
|
if (save) await this.save()
|
||||||
|
|
||||||
|
this.data = null
|
||||||
|
this.#endianness = undefined
|
||||||
|
this.loaded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
parse (data) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
unparse (data) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PersistentData
|
71
util/player_data.js
Normal file
71
util/player_data.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
const PersistentData = require('./persistent_data')
|
||||||
|
const nbt = require('prismarine-nbt')
|
||||||
|
|
||||||
|
class PlayerData extends PersistentData {
|
||||||
|
_bots = new Set()
|
||||||
|
|
||||||
|
parse (data) {
|
||||||
|
const parsed = {}
|
||||||
|
data = data.value // * Convenient since I don't need to write data.value a bunch of times
|
||||||
|
|
||||||
|
if (data.username) parsed.username = data.username?.value
|
||||||
|
|
||||||
|
if (data.seen?.value) {
|
||||||
|
parsed.seen = {}
|
||||||
|
if (data.seen.value.first?.value) parsed.seen.first = new Date(Number(data.seen.value.first?.value))
|
||||||
|
if (data.seen.value.last?.value) parsed.seen.last = new Date(Number(data.seen.value.last?.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.mail?.value?.value && Array.isArray(data.mail.value.value)) {
|
||||||
|
parsed.mail = data.mail.value.value.map(this.#parseMail)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
#parseMail (mail) {
|
||||||
|
const signedPort = Number(mail?.port?.value ?? 25565)
|
||||||
|
const uint16Array = new Uint16Array(1)
|
||||||
|
uint16Array[0] = signedPort
|
||||||
|
const unsignedPort = uint16Array[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
sender: nbt.comp({ uuid: mail?.sender?.uuid }),
|
||||||
|
message: String(mail?.message?.value),
|
||||||
|
host: String(mail?.host?.value),
|
||||||
|
port: unsignedPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unparse (parsed) {
|
||||||
|
const data = {}
|
||||||
|
|
||||||
|
if (parsed.username) data.username = nbt.string(parsed.username)
|
||||||
|
|
||||||
|
if (parsed.seen) {
|
||||||
|
data.seen = nbt.comp({ first: nbt.long(BigInt(parsed.seen.first.getTime())), last: nbt.long(BigInt(parsed.seen.last.getTime())) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.mail) {
|
||||||
|
data.mail = nbt.list(nbt.comp(parsed.mail.map(this.#unparseMail)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nbt.comp(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#unparseMail (mail) {
|
||||||
|
const unsignedPort = mail.port ?? 25565
|
||||||
|
const int16Array = new Int16Array(1)
|
||||||
|
int16Array[0] = unsignedPort
|
||||||
|
const signedPort = int16Array[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
sender: nbt.string(mail.sender),
|
||||||
|
message: nbt.string(mail.message),
|
||||||
|
host: nbt.string(mail.host),
|
||||||
|
port: nbt.short(signedPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PlayerData
|
Loading…
Reference in a new issue