221 lines
7.3 KiB
JavaScript
221 lines
7.3 KiB
JavaScript
|
const { once } = require('events')
|
||
|
|
||
|
const USERNAME_REGEX = '(?:\\(.{1,15}\\)|\\[.{1,15}\\]|.){0,5}?(\\w+)'
|
||
|
const LEGACY_VANILLA_CHAT_REGEX = new RegExp(`^${USERNAME_REGEX}\\s?[>:\\-»\\]\\)~]+\\s(.*)$`)
|
||
|
|
||
|
module.exports = inject
|
||
|
|
||
|
function inject (bot, options) {
|
||
|
const CHAT_LENGTH_LIMIT = options.chatLengthLimit ?? (bot.supportFeature('lessCharsInChat') ? 100 : 256)
|
||
|
const defaultChatPatterns = options.defaultChatPatterns ?? true
|
||
|
|
||
|
const ChatMessage = require('prismarine-chat')(bot.registry)
|
||
|
// chat.pattern.type will emit an event for bot.on() of the same type, eg chatType = whisper will trigger bot.on('whisper')
|
||
|
const _patterns = {}
|
||
|
let _length = 0
|
||
|
// deprecated
|
||
|
bot.chatAddPattern = (patternValue, typeValue) => {
|
||
|
return bot.addChatPattern(typeValue, patternValue, { deprecated: true })
|
||
|
}
|
||
|
|
||
|
bot.addChatPatternSet = (name, patterns, opts = {}) => {
|
||
|
if (!patterns.every(p => p instanceof RegExp)) throw new Error('Pattern parameter should be of type RegExp')
|
||
|
const { repeat = true, parse = false } = opts
|
||
|
_patterns[_length++] = {
|
||
|
name,
|
||
|
patterns,
|
||
|
position: 0,
|
||
|
matches: [],
|
||
|
messages: [],
|
||
|
repeat,
|
||
|
parse
|
||
|
}
|
||
|
return _length
|
||
|
}
|
||
|
|
||
|
bot.addChatPattern = (name, pattern, opts = {}) => {
|
||
|
if (!(pattern instanceof RegExp)) throw new Error('Pattern parameter should be of type RegExp')
|
||
|
const { repeat = true, deprecated = false, parse = false } = opts
|
||
|
_patterns[_length] = {
|
||
|
name,
|
||
|
patterns: [pattern],
|
||
|
position: 0,
|
||
|
matches: [],
|
||
|
messages: [],
|
||
|
deprecated,
|
||
|
repeat,
|
||
|
parse
|
||
|
}
|
||
|
return _length++ // increment length after we give it back to the user
|
||
|
}
|
||
|
|
||
|
bot.removeChatPattern = name => {
|
||
|
if (typeof name === 'number') {
|
||
|
_patterns[name] = undefined
|
||
|
} else {
|
||
|
const matchingPatterns = Object.entries(_patterns).filter(pattern => pattern[1]?.name === name)
|
||
|
matchingPatterns.forEach(([indexString]) => {
|
||
|
_patterns[+indexString] = undefined
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function findMatchingPatterns (msg) {
|
||
|
const found = []
|
||
|
for (const [indexString, pattern] of Object.entries(_patterns)) {
|
||
|
if (!pattern) continue
|
||
|
const { position, patterns } = pattern
|
||
|
if (patterns[position].test(msg)) {
|
||
|
found.push(+indexString)
|
||
|
}
|
||
|
}
|
||
|
return found
|
||
|
}
|
||
|
|
||
|
bot.on('messagestr', (msg, _, originalMsg) => {
|
||
|
const foundPatterns = findMatchingPatterns(msg)
|
||
|
|
||
|
for (const ix of foundPatterns) {
|
||
|
_patterns[ix].matches.push(msg)
|
||
|
_patterns[ix].messages.push(originalMsg)
|
||
|
_patterns[ix].position++
|
||
|
|
||
|
if (_patterns[ix].deprecated) {
|
||
|
const [, ...matches] = _patterns[ix].matches[0].match(_patterns[ix].patterns[0])
|
||
|
bot.emit(_patterns[ix].name, ...matches, _patterns[ix].messages[0].translate, ..._patterns[ix].messages)
|
||
|
_patterns[ix].messages = [] // clear out old messages
|
||
|
} else { // regular parsing
|
||
|
if (_patterns[ix].patterns.length > _patterns[ix].matches.length) return // we have all the matches, so we can emit the done event
|
||
|
if (_patterns[ix].parse) {
|
||
|
const matches = _patterns[ix].patterns.map((pattern, i) => {
|
||
|
const [, ...matches] = _patterns[ix].matches[i].match(pattern) // delete full message match
|
||
|
return matches
|
||
|
})
|
||
|
bot.emit(`chat:${_patterns[ix].name}`, matches)
|
||
|
} else {
|
||
|
bot.emit(`chat:${_patterns[ix].name}`, _patterns[ix].matches)
|
||
|
}
|
||
|
// these are possibly null-ish if the user deletes them as soon as the event for the match is emitted
|
||
|
}
|
||
|
if (_patterns[ix]?.repeat) {
|
||
|
_patterns[ix].position = 0
|
||
|
_patterns[ix].matches = []
|
||
|
} else {
|
||
|
_patterns[ix] = undefined
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
addDefaultPatterns()
|
||
|
|
||
|
bot._client.on('playerChat', (data) => {
|
||
|
const message = data.formattedMessage
|
||
|
const verified = data.verified
|
||
|
let msg
|
||
|
if (bot.supportFeature('clientsideChatFormatting')) {
|
||
|
const parameters = {
|
||
|
sender: data.senderName ? JSON.parse(data.senderName) : undefined,
|
||
|
target: data.targetName ? JSON.parse(data.targetName) : undefined,
|
||
|
content: message ? JSON.parse(message) : { text: data.plainMessage }
|
||
|
}
|
||
|
msg = ChatMessage.fromNetwork(data.type, parameters)
|
||
|
|
||
|
if (data.unsignedContent) {
|
||
|
msg.unsigned = ChatMessage.fromNetwork(data.type, { sender: parameters.sender, target: parameters.target, content: JSON.parse(data.unsignedContent) })
|
||
|
}
|
||
|
} else {
|
||
|
msg = ChatMessage.fromNotch(message)
|
||
|
}
|
||
|
bot.emit('message', msg, 'chat', data.sender, verified)
|
||
|
bot.emit('messagestr', msg.toString(), 'chat', msg, data.sender, verified)
|
||
|
})
|
||
|
|
||
|
bot._client.on('systemChat', (data) => {
|
||
|
const msg = ChatMessage.fromNotch(data.formattedMessage)
|
||
|
const chatPositions = {
|
||
|
1: 'system',
|
||
|
2: 'game_info'
|
||
|
}
|
||
|
bot.emit('message', msg, chatPositions[data.positionId], null)
|
||
|
bot.emit('messagestr', msg.toString(), chatPositions[data.positionId], msg, null)
|
||
|
if (data.positionId === 2) bot.emit('actionBar', msg, null)
|
||
|
})
|
||
|
|
||
|
function chatWithHeader (header, message) {
|
||
|
if (typeof message === 'number') message = message.toString()
|
||
|
if (typeof message !== 'string') {
|
||
|
throw new Error('Incorrect type! Should be a string or number.')
|
||
|
}
|
||
|
|
||
|
if (!header && message.startsWith('/')) {
|
||
|
// Do not try and split a command without a header
|
||
|
bot._client.chat(message)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const lengthLimit = CHAT_LENGTH_LIMIT - header.length
|
||
|
message.split('\n').forEach((subMessage) => {
|
||
|
if (!subMessage) return
|
||
|
let i
|
||
|
let smallMsg
|
||
|
for (i = 0; i < subMessage.length; i += lengthLimit) {
|
||
|
smallMsg = header + subMessage.substring(i, i + lengthLimit)
|
||
|
bot._client.chat(smallMsg)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
async function tabComplete (text, assumeCommand = false, sendBlockInSight = true) {
|
||
|
let position
|
||
|
|
||
|
if (sendBlockInSight) {
|
||
|
const block = bot.blockAtCursor()
|
||
|
|
||
|
if (block) {
|
||
|
position = block.position
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bot._client.write('tab_complete', {
|
||
|
text,
|
||
|
assumeCommand,
|
||
|
lookedAtBlock: position
|
||
|
})
|
||
|
|
||
|
const [packet] = await once(bot._client, 'tab_complete')
|
||
|
return packet.matches
|
||
|
}
|
||
|
|
||
|
bot.whisper = (username, message) => {
|
||
|
chatWithHeader(`/tell ${username} `, message)
|
||
|
}
|
||
|
bot.chat = (message) => {
|
||
|
chatWithHeader('', message)
|
||
|
}
|
||
|
|
||
|
bot.tabComplete = tabComplete
|
||
|
|
||
|
function addDefaultPatterns () {
|
||
|
// 1.19 changes the chat format to move <sender> prefix from message contents to a seperate field.
|
||
|
// TODO: new chat lister to handle this
|
||
|
if (!defaultChatPatterns) return
|
||
|
bot.addChatPattern('whisper', new RegExp(`^${USERNAME_REGEX} whispers(?: to you)?:? (.*)$`), { deprecated: true })
|
||
|
bot.addChatPattern('whisper', new RegExp(`^\\[${USERNAME_REGEX} -> \\w+\\s?\\] (.*)$`), { deprecated: true })
|
||
|
bot.addChatPattern('chat', LEGACY_VANILLA_CHAT_REGEX, { deprecated: true })
|
||
|
}
|
||
|
|
||
|
function awaitMessage (...args) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
const resolveMessages = args.flatMap(x => x)
|
||
|
function messageListener (msg) {
|
||
|
if (resolveMessages.some(x => x instanceof RegExp ? x.test(msg) : msg === x)) {
|
||
|
resolve(msg)
|
||
|
bot.off('messagestr', messageListener)
|
||
|
}
|
||
|
}
|
||
|
bot.on('messagestr', messageListener)
|
||
|
})
|
||
|
}
|
||
|
bot.awaitMessage = awaitMessage
|
||
|
}
|