mirror of
https://github.com/barkeser2002/mcsleepingserverstarter.git
synced 2026-09-25 04:30:14 +03:00
593 lines
22 KiB
JavaScript
593 lines
22 KiB
JavaScript
const crypto = require('crypto')
|
|
const { computeChatChecksum } = require('../datatypes/checksums')
|
|
const concat = require('../transforms/binaryStream').concat
|
|
const { processNbtMessage } = require('prismarine-chat')
|
|
const messageExpireTime = 420000 // 7 minutes (ms)
|
|
|
|
function isFormatted (message) {
|
|
// This should match the ChatComponent.isDecorated function from Vanilla
|
|
try {
|
|
const comp = JSON.parse(message)
|
|
for (const key in comp) {
|
|
if (key !== 'text') return true
|
|
}
|
|
if (comp.text && comp.text !== message) return true
|
|
return false
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
module.exports = function (client, options) {
|
|
const mcData = require('minecraft-data')(client.version)
|
|
client._players = {}
|
|
client._lastChatSignature = null
|
|
client._lastRejectedMessage = null
|
|
|
|
// This stores the last n (5 or 20) messages that the player has seen, from unique players
|
|
if (mcData.supportFeature('chainedChatWithHashing')) client._lastSeenMessages = new LastSeenMessages()
|
|
else client._lastSeenMessages = new LastSeenMessagesWithInvalidation()
|
|
// 1.20.3+ serializes chat components in either NBT or JSON. If the chat is sent as NBT, then the structure read will differ
|
|
// from the normal JSON structure, so it needs to be normalized. prismarine-chat processNbtMessage will do that by default
|
|
// on a fromNotch call. Since we don't call fromNotch here (done in mineflayer), we manually call processNbtMessage
|
|
const processMessage = (msg) => mcData.supportFeature('chatPacketsUseNbtComponents') ? processNbtMessage(msg) : msg
|
|
|
|
// This stores the last 128 inbound (signed) messages for 1.19.3 chat validation
|
|
client._signatureCache = new SignatureCache()
|
|
|
|
// This stores last 1024 inbound messages for report lookup
|
|
client._lastChatHistory = new class extends Array {
|
|
capacity = 1024
|
|
push (e) {
|
|
super.push(e)
|
|
if (this.length > this.capacity) {
|
|
this.shift()
|
|
}
|
|
}
|
|
}()
|
|
|
|
function updateAndValidateSession (uuid, message, currentSignature, index, previousMessages, salt, timestamp) {
|
|
const player = client._players[uuid]
|
|
|
|
if (player && player.hasChainIntegrity) {
|
|
if (!player.lastSignature || player.lastSignature.equals(currentSignature) || index > player.sessionIndex) {
|
|
player.lastSignature = currentSignature
|
|
player.sessionIndex = index
|
|
} else {
|
|
player.hasChainIntegrity = false
|
|
}
|
|
|
|
if (player.hasChainIntegrity) {
|
|
const length = Buffer.byteLength(message, 'utf8')
|
|
const validBuffers = previousMessages
|
|
.filter(msg => msg.signature || client._signatureCache[msg.id]) // Filter out invalid messages
|
|
.map(msg => msg.signature || client._signatureCache[msg.id])
|
|
.filter(buf => Buffer.isBuffer(buf))
|
|
|
|
const acknowledgements = validBuffers.length > 0
|
|
? ['i32', validBuffers.length, 'buffer', Buffer.concat(validBuffers)]
|
|
: ['i32', 0]
|
|
const signable = concat('i32', 1, 'UUID', uuid, 'UUID', player.sessionUuid, 'i32', index, 'i64', salt, 'i64', timestamp / 1000n, 'i32', length, 'pstring', message, ...acknowledgements)
|
|
|
|
player.hasChainIntegrity = crypto.verify('RSA-SHA256', signable, player.publicKey, currentSignature)
|
|
}
|
|
|
|
return player.hasChainIntegrity
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function updateAndValidateChat (uuid, previousSignature, currentSignature, payload) {
|
|
// Get the player information
|
|
const player = client._players[uuid]
|
|
if (player && player.hasChainIntegrity) {
|
|
if (!player.lastSignature) {
|
|
// First time client is handling a chat message from this player, allow
|
|
player.lastSignature = currentSignature
|
|
} else if (player.lastSignature.equals(previousSignature)) {
|
|
player.lastSignature = currentSignature
|
|
} else {
|
|
// Not valid, client can no longer authenticate messages until player quits and reconnects
|
|
player.hasChainIntegrity = false
|
|
}
|
|
|
|
if (player.hasChainIntegrity) {
|
|
const verifier = crypto.createVerify('RSA-SHA256')
|
|
if (previousSignature) verifier.update(previousSignature)
|
|
verifier.update(concat('UUID', uuid))
|
|
verifier.update(payload)
|
|
player.hasChainIntegrity = verifier.verify(player.publicKey, currentSignature)
|
|
}
|
|
|
|
return player.hasChainIntegrity
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
client.on('player_remove', (packet) => {
|
|
for (const player of packet.players) {
|
|
delete client._players[player.UUID]
|
|
}
|
|
})
|
|
|
|
client.on('player_info', (packet) => {
|
|
for (const player of packet.data) {
|
|
if (player.chatSession) {
|
|
client._players[player.uuid] = {
|
|
publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
|
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
|
sessionUuid: player.chatSession.uuid
|
|
}
|
|
client._players[player.uuid].sessionIndex = true
|
|
client._players[player.uuid].hasChainIntegrity = true
|
|
}
|
|
|
|
if (player.crypto) {
|
|
client._players[player.uuid] = {
|
|
publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
|
publicKeyDER: player.crypto.publicKey,
|
|
signature: player.crypto.signature,
|
|
displayName: player.displayName || player.name
|
|
}
|
|
client._players[player.uuid].hasChainIntegrity = true
|
|
}
|
|
|
|
if (packet.action === 'remove_player') { // Only 1.8-1.9
|
|
delete client._players[player.uuid]
|
|
}
|
|
}
|
|
})
|
|
|
|
client.on('profileless_chat', (packet) => {
|
|
// Profileless chat is parsed as an unsigned player chat message but logged as a system message
|
|
client.emit('playerChat', {
|
|
formattedMessage: processMessage(packet.message),
|
|
type: packet.type,
|
|
senderName: processMessage(packet.name),
|
|
targetName: processMessage(packet.target),
|
|
verified: false
|
|
})
|
|
|
|
client._lastChatHistory.push({
|
|
type: 2, // System message
|
|
message: {
|
|
decorated: packet.content // This should actually decorate the message with the sender and target name using the chat type
|
|
},
|
|
timestamp: Date.now()
|
|
})
|
|
})
|
|
|
|
client.on('system_chat', (packet) => {
|
|
client.emit('systemChat', {
|
|
positionId: packet.isActionBar ? 2 : 1,
|
|
formattedMessage: processMessage(packet.content)
|
|
})
|
|
|
|
client._lastChatHistory.push({
|
|
type: 2, // System message
|
|
message: {
|
|
decorated: packet.content
|
|
},
|
|
timestamp: Date.now()
|
|
})
|
|
})
|
|
|
|
client.on('message_header', (packet) => { // [1.19.2]
|
|
updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, packet.messageHash)
|
|
|
|
client._lastChatHistory.push({
|
|
type: 1, // Message header
|
|
previousSignature: packet.previousSignature,
|
|
signature: packet.signature,
|
|
messageHash: packet.messageHash
|
|
})
|
|
})
|
|
|
|
client.on('hide_message', (packet) => {
|
|
if (mcData.supportFeature('useChatSessions')) {
|
|
const signature = packet.signature || client._signatureCache[packet.id]
|
|
if (signature) client._lastSeenMessages = client._lastSeenMessages.map(ack => (ack.signature === signature && ack.pending) ? null : ack)
|
|
}
|
|
})
|
|
|
|
client.on('player_chat', (packet) => {
|
|
if (mcData.supportFeature('useChatSessions')) {
|
|
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
|
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
|
const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
|
if (verified) client._signatureCache.push(packet.signature)
|
|
client.emit('playerChat', {
|
|
globalIndex: packet.globalIndex,
|
|
plainMessage: packet.plainMessage,
|
|
unsignedContent: processMessage(packet.unsignedChatContent),
|
|
type: packet.type,
|
|
sender: packet.senderUuid,
|
|
senderName: processMessage(packet.networkName),
|
|
targetName: processMessage(packet.networkTargetName),
|
|
verified
|
|
})
|
|
|
|
client._lastChatHistory.push({
|
|
type: 0, // Player message
|
|
signature: packet.signature,
|
|
message: {
|
|
plain: packet.plainMessage
|
|
},
|
|
session: {
|
|
index: packet.index,
|
|
uuid: client._players[packet.senderUuid]?.sessionUuid
|
|
},
|
|
timestamp: packet.timestamp,
|
|
salt: packet.salt,
|
|
lastSeen: packet.previousMessages.map(msg => msg.signature || client._signatureCache[msg.id])
|
|
})
|
|
|
|
if (client._lastSeenMessages.push(packet.signature) && client._lastSeenMessages.pending > 64) {
|
|
client.write('message_acknowledgement', {
|
|
count: client._lastSeenMessages.pending
|
|
})
|
|
client._lastSeenMessages.pending = 0
|
|
}
|
|
return
|
|
}
|
|
|
|
if (mcData.supportFeature('chainedChatWithHashing')) {
|
|
const hash = crypto.createHash('sha256')
|
|
hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.plainMessage, 'i8', 70))
|
|
if (packet.formattedMessage) hash.update(packet.formattedMessage)
|
|
for (const previousMessage of packet.previousMessages) {
|
|
hash.update(concat('i8', 70, 'UUID', previousMessage.messageSender))
|
|
hash.update(previousMessage.messageSignature)
|
|
}
|
|
|
|
// Chain integrity remains even if message is considered unverified due to expiry
|
|
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
|
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
|
const verified = !packet.unsignedChatContent && updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired
|
|
client.emit('playerChat', {
|
|
plainMessage: packet.plainMessage,
|
|
unsignedContent: packet.unsignedChatContent,
|
|
formattedMessage: packet.formattedMessage,
|
|
type: packet.type,
|
|
sender: packet.senderUuid,
|
|
senderName: packet.networkName,
|
|
targetName: packet.networkTargetName,
|
|
verified
|
|
})
|
|
|
|
// We still accept a message (by pushing to seenMessages) even if the chain is broken. A vanilla client
|
|
// will reject a message if the client sets secure chat to be required and the message from the server
|
|
// isn't signed, or the client has blocked the sender.
|
|
// client1.19.1/client/net/minecraft/client/multiplayer/ClientPacketListener.java#L768
|
|
client._lastChatHistory.push({
|
|
type: 0, // Player message
|
|
previousSignature: packet.previousSignature,
|
|
signature: packet.signature,
|
|
message: {
|
|
plain: packet.plainMessage,
|
|
decorated: packet.formattedMessage
|
|
},
|
|
messageHash: packet.messageHash,
|
|
timestamp: packet.timestamp,
|
|
salt: packet.salt,
|
|
lastSeen: packet.previousMessages
|
|
})
|
|
|
|
if (client._lastSeenMessages.push({ sender: packet.senderUuid, signature: packet.signature }) && client._lastSeenMessages.pending++ > 64) {
|
|
client.write('message_acknowledgement', {
|
|
previousMessages: client._lastSeenMessages.map((e) => ({
|
|
messageSender: e.sender,
|
|
messageSignature: e.signature
|
|
})),
|
|
lastRejectedMessage: client._lastRejectedMessage
|
|
})
|
|
client._lastSeenMessages.pending = 0
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
const pubKey = client._players[packet.senderUuid]?.publicKey
|
|
client.emit('playerChat', {
|
|
formattedMessage: packet.signedChatContent || packet.unsignedChatContent,
|
|
type: packet.type,
|
|
sender: packet.senderUuid,
|
|
senderName: packet.senderName,
|
|
senderTeam: packet.senderTeam,
|
|
verified: (pubKey && !packet.unsignedChatContent) ? client.verifyMessage(pubKey, packet) : false
|
|
})
|
|
})
|
|
|
|
const sliceIndexForMessage = {}
|
|
client.on('declare_commands', (packet) => {
|
|
const nodes = packet.nodes
|
|
for (const commandNode of nodes[0].children) {
|
|
const node = nodes[commandNode]
|
|
const commandName = node.extraNodeData.name
|
|
function visit (node, depth = 0) {
|
|
const name = node.extraNodeData.name
|
|
if (node.extraNodeData.parser === 'minecraft:message') {
|
|
sliceIndexForMessage[commandName] = [name, depth]
|
|
}
|
|
for (const child of node.children) {
|
|
visit(nodes[child], depth + 1)
|
|
}
|
|
}
|
|
visit(node, 0)
|
|
}
|
|
})
|
|
|
|
function signaturesForCommand (string, ts, salt, preview, acknowledgements) {
|
|
const signatures = []
|
|
const slices = string.split(' ')
|
|
if (sliceIndexForMessage[slices[0]]) {
|
|
const [fieldName, sliceIndex] = sliceIndexForMessage[slices[0]]
|
|
const sliced = slices.slice(sliceIndex)
|
|
if (sliced.length > 0) {
|
|
const signable = sliced.join(' ')
|
|
signatures.push({ argumentName: fieldName, signature: client.signMessage(signable, ts, salt, preview, acknowledgements) })
|
|
}
|
|
}
|
|
return signatures
|
|
}
|
|
|
|
// Chat Sending
|
|
let pendingChatRequest
|
|
let lastPreviewRequestId = 0
|
|
|
|
function getAcknowledgements () {
|
|
let acc = 0
|
|
const acknowledgements = []
|
|
|
|
for (let i = 0; i < client._lastSeenMessages.capacity; i++) {
|
|
const idx = (client._lastSeenMessages.offset + i) % 20
|
|
const message = client._lastSeenMessages[idx]
|
|
if (message) {
|
|
acc |= 1 << i
|
|
acknowledgements.push(message.signature)
|
|
message.pending = false
|
|
}
|
|
}
|
|
|
|
const bitset = Buffer.allocUnsafe(3)
|
|
bitset[0] = acc & 0xFF
|
|
bitset[1] = (acc >> 8) & 0xFF
|
|
bitset[2] = (acc >> 16) & 0xFF
|
|
|
|
return {
|
|
acknowledgements,
|
|
acknowledged: bitset
|
|
}
|
|
}
|
|
|
|
client._signedChat = (message, options = {}) => {
|
|
options.timestamp = options.timestamp || BigInt(Date.now())
|
|
options.salt = options.salt || 1n
|
|
|
|
if (message.startsWith('/')) {
|
|
const command = message.slice(1)
|
|
if (mcData.supportFeature('useChatSessions')) { // 1.19.3+
|
|
const { acknowledged, acknowledgements } = getAcknowledgements()
|
|
const canSign = client.profileKeys && client._session
|
|
const chatPacket = {
|
|
command,
|
|
timestamp: options.timestamp,
|
|
salt: options.salt,
|
|
argumentSignatures: canSign ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [],
|
|
messageCount: client._lastSeenMessages.pending,
|
|
checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+
|
|
acknowledged
|
|
}
|
|
client.write((mcData.supportFeature('seperateSignedChatCommandPacket') && canSign) ? 'chat_command_signed' : 'chat_command', chatPacket)
|
|
client._lastSeenMessages.pending = 0
|
|
} else {
|
|
client.write('chat_command', {
|
|
command,
|
|
timestamp: options.timestamp,
|
|
salt: options.salt,
|
|
argumentSignatures: client.profileKeys ? signaturesForCommand(command, options.timestamp, options.salt) : [],
|
|
signedPreview: options.didPreview,
|
|
previousMessages: client._lastSeenMessages.map((e) => ({
|
|
messageSender: e.sender,
|
|
messageSignature: e.signature
|
|
})),
|
|
checksum: computeChatChecksum(client._lastSeenMessages),
|
|
lastRejectedMessage: client._lastRejectedMessage
|
|
})
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if (mcData.supportFeature('useChatSessions')) {
|
|
const { acknowledgements, acknowledged } = getAcknowledgements()
|
|
client.write('chat_message', {
|
|
message,
|
|
timestamp: options.timestamp,
|
|
salt: options.salt,
|
|
signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
|
offset: client._lastSeenMessages.pending,
|
|
checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+
|
|
acknowledged
|
|
})
|
|
client._lastSeenMessages.pending = 0
|
|
|
|
return
|
|
}
|
|
|
|
if (options.skipPreview || !client.serverFeatures.chatPreview) {
|
|
client.write('chat_message', {
|
|
message,
|
|
timestamp: options.timestamp,
|
|
salt: options.salt,
|
|
signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
|
signedPreview: options.didPreview,
|
|
previousMessages: client._lastSeenMessages.map((e) => ({
|
|
messageSender: e.sender,
|
|
messageSignature: e.signature
|
|
})),
|
|
lastRejectedMessage: client._lastRejectedMessage,
|
|
checksum: computeChatChecksum(client._lastSeenMessages) // 1.21.5+
|
|
})
|
|
client._lastSeenMessages.pending = 0
|
|
} else if (client.serverFeatures.chatPreview) {
|
|
client.write('chat_preview', {
|
|
query: lastPreviewRequestId,
|
|
message
|
|
})
|
|
pendingChatRequest = { id: lastPreviewRequestId, message, options }
|
|
lastPreviewRequestId++
|
|
}
|
|
}
|
|
|
|
client.on('chat_preview', (packet) => {
|
|
if (pendingChatRequest && pendingChatRequest.id === packet.queryId) {
|
|
client._signedChat(pendingChatRequest.message, { ...pendingChatRequest.options, skipPreview: true, didPreview: true, preview: isFormatted(packet.message) ? packet.message : undefined })
|
|
pendingChatRequest = null
|
|
}
|
|
})
|
|
|
|
// Signing methods
|
|
client.signMessage = (message, timestamp, salt = 0, preview, acknowledgements) => {
|
|
if (!client.profileKeys) throw Error("Can't sign message without profile keys, please set valid auth mode")
|
|
|
|
if (mcData.supportFeature('useChatSessions')) {
|
|
if (!client._session.uuid) throw Error("Chat session not initialized. Can't send chat")
|
|
|
|
const length = Buffer.byteLength(message, 'utf8')
|
|
const previousMessages = acknowledgements.length > 0 ? ['i32', acknowledgements.length, 'buffer', Buffer.concat(acknowledgements)] : ['i32', 0]
|
|
|
|
const signable = concat('i32', 1, 'UUID', client.uuid, 'UUID', client._session.uuid, 'i32', client._session.index++, 'i64', salt, 'i64', timestamp / 1000n, 'i32', length, 'pstring', message, ...previousMessages)
|
|
return crypto.sign('RSA-SHA256', signable, client.profileKeys.private)
|
|
} else if (mcData.supportFeature('chainedChatWithHashing')) {
|
|
// 1.19.2
|
|
const signer = crypto.createSign('RSA-SHA256')
|
|
if (client._lastChatSignature) signer.update(client._lastChatSignature)
|
|
signer.update(concat('UUID', client.uuid))
|
|
|
|
// Hash of chat body now opposed to signing plaintext. This lets server give us hashes for chat
|
|
// chain without needing to reveal message contents
|
|
if (message instanceof Buffer) {
|
|
signer.update(message)
|
|
} else {
|
|
const hash = crypto.createHash('sha256')
|
|
hash.update(concat('i64', salt, 'i64', timestamp / 1000n, 'pstring', message, 'i8', 70))
|
|
if (preview) hash.update(preview)
|
|
for (const previousMessage of client._lastSeenMessages) {
|
|
hash.update(concat('i8', 70, 'UUID', previousMessage.sender))
|
|
hash.update(previousMessage.signature)
|
|
}
|
|
// Feed hash back into signing payload
|
|
signer.update(hash.digest())
|
|
}
|
|
|
|
client._lastChatSignature = signer.sign(client.profileKeys.private)
|
|
} else {
|
|
// 1.19
|
|
const signable = concat('i64', salt, 'UUID', client.uuid, 'i64', timestamp / 1000n, 'pstring', JSON.stringify({ text: message }))
|
|
client._lastChatSignature = crypto.sign('RSA-SHA256', signable, client.profileKeys.private)
|
|
}
|
|
|
|
return client._lastChatSignature
|
|
}
|
|
|
|
client.verifyMessage = (pubKey, packet) => {
|
|
if (mcData.supportFeature('chainedChatWithHashing')) { // 1.19.1+
|
|
// Verification handled internally in 1.19.1+ as previous messages must be stored to verify future messages
|
|
throw new Error("Please listen to the 'playerChat' event instead to check message validity. client.verifyMessage is deprecated and only works on version 1.19.")
|
|
}
|
|
|
|
if (pubKey instanceof Buffer) pubKey = crypto.createPublicKey({ key: pubKey, format: 'der', type: 'spki' })
|
|
const signable = concat('i64', packet.salt, 'UUID', packet.senderUuid, 'i64', packet.timestamp / 1000n, 'pstring', packet.signedChatContent)
|
|
return crypto.verify('RSA-SHA256', signable, pubKey, packet.signature)
|
|
}
|
|
|
|
// Report a chat message.
|
|
client.reportPlayer = (uuid, reason, reportedSignatures, comments) => {
|
|
const evidence = []
|
|
|
|
function addEvidence (entry) {
|
|
evidence.push({
|
|
previousHeaderSignature: entry.previousSignature,
|
|
uuid: entry.senderUuid,
|
|
message: entry.message,
|
|
messageHash: entry.messageHash,
|
|
signature: entry.signature,
|
|
timestamp: entry.timestamp,
|
|
salt: entry.salt,
|
|
lastSeen: entry.lastSeen
|
|
})
|
|
}
|
|
|
|
for (let i = 0; i < client._lastChatHistory.capacity; i++) {
|
|
const entry = client._lastChatHistory[i]
|
|
for (const reportSig of reportedSignatures) {
|
|
if (reportSig.equals(entry.signature)) addEvidence(entry)
|
|
}
|
|
}
|
|
|
|
return client.authflow.mca.reportPlayerChat({
|
|
reason,
|
|
comments,
|
|
messages: evidence,
|
|
reportedPlayer: uuid,
|
|
createdTime: Date.now(),
|
|
clientVersion: client.version,
|
|
serverAddress: options.host + ':' + options.port,
|
|
realmInfo: undefined // { realmId, slotId }
|
|
})
|
|
}
|
|
}
|
|
|
|
class SignatureCache extends Array {
|
|
capacity = 128
|
|
index = 0
|
|
|
|
push (e) {
|
|
if (!e) return
|
|
|
|
this[this.index++] = e
|
|
this.index %= this.capacity
|
|
}
|
|
}
|
|
|
|
class LastSeenMessagesWithInvalidation extends Array {
|
|
capacity = 20
|
|
offset = 0
|
|
pending = 0
|
|
|
|
push (e) {
|
|
if (!e) return false
|
|
|
|
this[this.offset] = { pending: true, signature: e }
|
|
this.offset = (this.offset + 1) % this.capacity
|
|
this.pending++
|
|
return true
|
|
}
|
|
}
|
|
|
|
class LastSeenMessages extends Array {
|
|
capacity = 5
|
|
pending = 0
|
|
|
|
push (e) {
|
|
if (!e || !e.signature || e.signature.length === 0) return false // We do not acknowledge unsigned messages
|
|
|
|
// Insert a new entry at the top and shift everything to the right
|
|
let last = this[0]
|
|
this[0] = e
|
|
if (last && last.sender !== e.sender) {
|
|
for (let i = 1; i < this.capacity; i++) {
|
|
const current = this[i]
|
|
this[i] = last
|
|
last = current
|
|
// If we found an existing entry for the sender ID, we can stop shifting
|
|
if (!current || (current.sender === e.sender)) break
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|