Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PrismarineJS
GitHub Repository: PrismarineJS/mineflayer
Path: blob/master/lib/plugins/chat.js
1467 views
1
const { onceWithCleanup } = require('../promise_utils')
2
3
const USERNAME_REGEX = '(?:\\(.{1,15}\\)|\\[.{1,15}\\]|.){0,5}?(\\w+)'
4
const LEGACY_VANILLA_CHAT_REGEX = new RegExp(`^${USERNAME_REGEX}\\s?[>:\\-ยป\\]\\)~]+\\s(.*)$`)
5
6
module.exports = inject
7
8
function inject (bot, options) {
9
const CHAT_LENGTH_LIMIT = options.chatLengthLimit ?? (bot.supportFeature('lessCharsInChat') ? 100 : 256)
10
const defaultChatPatterns = options.defaultChatPatterns ?? true
11
12
const ChatMessage = require('prismarine-chat')(bot.registry)
13
// chat.pattern.type will emit an event for bot.on() of the same type, eg chatType = whisper will trigger bot.on('whisper')
14
const _patterns = {}
15
let _length = 0
16
// deprecated
17
bot.chatAddPattern = (patternValue, typeValue) => {
18
return bot.addChatPattern(typeValue, patternValue, { deprecated: true })
19
}
20
21
bot.addChatPatternSet = (name, patterns, opts = {}) => {
22
if (!patterns.every(p => p instanceof RegExp)) throw new Error('Pattern parameter should be of type RegExp')
23
const { repeat = true, parse = false } = opts
24
_patterns[_length++] = {
25
name,
26
patterns,
27
position: 0,
28
matches: [],
29
messages: [],
30
repeat,
31
parse
32
}
33
return _length
34
}
35
36
bot.addChatPattern = (name, pattern, opts = {}) => {
37
if (!(pattern instanceof RegExp)) throw new Error('Pattern parameter should be of type RegExp')
38
const { repeat = true, deprecated = false, parse = false } = opts
39
_patterns[_length] = {
40
name,
41
patterns: [pattern],
42
position: 0,
43
matches: [],
44
messages: [],
45
deprecated,
46
repeat,
47
parse
48
}
49
return _length++ // increment length after we give it back to the user
50
}
51
52
bot.removeChatPattern = name => {
53
if (typeof name === 'number') {
54
_patterns[name] = undefined
55
} else {
56
const matchingPatterns = Object.entries(_patterns).filter(pattern => pattern[1]?.name === name)
57
matchingPatterns.forEach(([indexString]) => {
58
_patterns[+indexString] = undefined
59
})
60
}
61
}
62
63
function findMatchingPatterns (msg) {
64
const found = []
65
for (const [indexString, pattern] of Object.entries(_patterns)) {
66
if (!pattern) continue
67
const { position, patterns } = pattern
68
if (patterns[position].test(msg)) {
69
found.push(+indexString)
70
}
71
}
72
return found
73
}
74
75
bot.on('messagestr', (msg, _, originalMsg) => {
76
const foundPatterns = findMatchingPatterns(msg)
77
78
for (const ix of foundPatterns) {
79
_patterns[ix].matches.push(msg)
80
_patterns[ix].messages.push(originalMsg)
81
_patterns[ix].position++
82
83
if (_patterns[ix].deprecated) {
84
const [, ...matches] = _patterns[ix].matches[0].match(_patterns[ix].patterns[0])
85
bot.emit(_patterns[ix].name, ...matches, _patterns[ix].messages[0].translate, ..._patterns[ix].messages)
86
_patterns[ix].messages = [] // clear out old messages
87
} else { // regular parsing
88
if (_patterns[ix].patterns.length > _patterns[ix].matches.length) return // we have all the matches, so we can emit the done event
89
if (_patterns[ix].parse) {
90
const matches = _patterns[ix].patterns.map((pattern, i) => {
91
const [, ...matches] = _patterns[ix].matches[i].match(pattern) // delete full message match
92
return matches
93
})
94
bot.emit(`chat:${_patterns[ix].name}`, matches)
95
} else {
96
bot.emit(`chat:${_patterns[ix].name}`, _patterns[ix].matches)
97
}
98
// these are possibly null-ish if the user deletes them as soon as the event for the match is emitted
99
}
100
if (_patterns[ix]?.repeat) {
101
_patterns[ix].position = 0
102
_patterns[ix].matches = []
103
} else {
104
_patterns[ix] = undefined
105
}
106
}
107
})
108
109
addDefaultPatterns()
110
111
bot._client.on('playerChat', (data) => {
112
const message = data.formattedMessage
113
const verified = data.verified
114
let msg
115
if (bot.supportFeature('clientsideChatFormatting')) {
116
const parameters = {
117
sender: data.senderName ? JSON.parse(data.senderName) : undefined,
118
target: data.targetName ? JSON.parse(data.targetName) : undefined,
119
content: message ? JSON.parse(message) : { text: data.plainMessage }
120
}
121
const registryIndex = data.type.chatType != null ? data.type.chatType : data.type
122
msg = ChatMessage.fromNetwork(registryIndex, parameters)
123
124
if (data.unsignedContent) {
125
msg.unsigned = ChatMessage.fromNetwork(registryIndex, { sender: parameters.sender, target: parameters.target, content: JSON.parse(data.unsignedContent) })
126
}
127
} else {
128
msg = ChatMessage.fromNotch(message)
129
}
130
bot.emit('message', msg, 'chat', data.sender, verified)
131
bot.emit('messagestr', msg.toString(), 'chat', msg, data.sender, verified)
132
})
133
134
bot._client.on('systemChat', (data) => {
135
const msg = ChatMessage.fromNotch(data.formattedMessage)
136
const chatPositions = {
137
1: 'system',
138
2: 'game_info'
139
}
140
bot.emit('message', msg, chatPositions[data.positionId], null)
141
bot.emit('messagestr', msg.toString(), chatPositions[data.positionId], msg, null)
142
if (data.positionId === 2) bot.emit('actionBar', msg, null)
143
})
144
145
function chatWithHeader (header, message) {
146
if (typeof message === 'number') message = message.toString()
147
if (typeof message !== 'string') {
148
throw new Error('Chat message type must be a string or number: ' + typeof message)
149
}
150
151
if (!header && message.startsWith('/')) {
152
// Do not try and split a command without a header
153
bot._client.chat(message)
154
return
155
}
156
157
const lengthLimit = CHAT_LENGTH_LIMIT - header.length
158
message.split('\n').forEach((subMessage) => {
159
if (!subMessage) return
160
let i
161
let smallMsg
162
for (i = 0; i < subMessage.length; i += lengthLimit) {
163
smallMsg = header + subMessage.substring(i, i + lengthLimit)
164
bot._client.chat(smallMsg)
165
}
166
})
167
}
168
169
async function tabComplete (text, assumeCommand = false, sendBlockInSight = true, timeout = 5000) {
170
let position
171
172
if (sendBlockInSight) {
173
const block = bot.blockAtCursor()
174
175
if (block) {
176
position = block.position
177
}
178
}
179
180
bot._client.write('tab_complete', {
181
text,
182
assumeCommand,
183
lookedAtBlock: position
184
})
185
186
const [packet] = await onceWithCleanup(bot._client, 'tab_complete', { timeout })
187
return packet.matches
188
}
189
190
bot.whisper = (username, message) => {
191
chatWithHeader(`/tell ${username} `, message)
192
}
193
bot.chat = (message) => {
194
chatWithHeader('', message)
195
}
196
197
bot.tabComplete = tabComplete
198
199
function addDefaultPatterns () {
200
// 1.19 changes the chat format to move <sender> prefix from message contents to a separate field.
201
// TODO: new chat lister to handle this
202
if (!defaultChatPatterns) return
203
bot.addChatPattern('whisper', new RegExp(`^${USERNAME_REGEX} whispers(?: to you)?:? (.*)$`), { deprecated: true })
204
bot.addChatPattern('whisper', new RegExp(`^\\[${USERNAME_REGEX} -> \\w+\\s?\\] (.*)$`), { deprecated: true })
205
bot.addChatPattern('chat', LEGACY_VANILLA_CHAT_REGEX, { deprecated: true })
206
}
207
208
function awaitMessage (...args) {
209
return new Promise((resolve, reject) => {
210
const resolveMessages = args.flatMap(x => x)
211
function messageListener (msg) {
212
if (resolveMessages.some(x => x instanceof RegExp ? x.test(msg) : msg === x)) {
213
resolve(msg)
214
bot.off('messagestr', messageListener)
215
}
216
}
217
bot.on('messagestr', messageListener)
218
})
219
}
220
bot.awaitMessage = awaitMessage
221
}
222
223