Handle DMs, reply in DM
This commit is contained in:
+14
-10
@@ -20,8 +20,18 @@ class Commands:
|
|||||||
|
|
||||||
self.g.chat.set_handler(self.handle_chat)
|
self.g.chat.set_handler(self.handle_chat)
|
||||||
|
|
||||||
|
def _send_reply(self, reply, private=False, sender=None):
|
||||||
|
if private and not reply.startswith('/'):
|
||||||
|
if isinstance(sender, str) and re.fullmatch(r'[A-Za-z0-9_]{1,16}', sender):
|
||||||
|
self.g.chat.send('/msg {} {}'.format(sender, reply))
|
||||||
|
else:
|
||||||
|
self.g.chat.send('/r ' + reply)
|
||||||
|
else:
|
||||||
|
self.g.chat.send(reply)
|
||||||
|
|
||||||
def handle_chat(self, message):
|
def handle_chat(self, message):
|
||||||
source, sender, text = message
|
source, sender, text = message[:3]
|
||||||
|
reply_target = message[3] if len(message) > 3 else sender
|
||||||
reply = ''
|
reply = ''
|
||||||
private = False
|
private = False
|
||||||
for_me = False
|
for_me = False
|
||||||
@@ -46,7 +56,7 @@ class Commands:
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
if '-> me' in meta:
|
if source == 'DM' or '-> me' in meta:
|
||||||
private = True
|
private = True
|
||||||
|
|
||||||
if prefix == bot_num:
|
if prefix == bot_num:
|
||||||
@@ -64,10 +74,7 @@ class Commands:
|
|||||||
(command == 'farm' and not data.lower().startswith('crop'))):
|
(command == 'farm' and not data.lower().startswith('crop'))):
|
||||||
reply = 'not migrated to Minecraft 26.3 yet'
|
reply = 'not migrated to Minecraft 26.3 yet'
|
||||||
print('Reply:', reply)
|
print('Reply:', reply)
|
||||||
if private:
|
self._send_reply(reply, private, reply_target)
|
||||||
self.g.chat.send('/r ' + reply)
|
|
||||||
else:
|
|
||||||
self.g.chat.send(reply)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -642,8 +649,5 @@ class Commands:
|
|||||||
if len(reply) >= 256:
|
if len(reply) >= 256:
|
||||||
reply = 'reply too long, check console'
|
reply = 'reply too long, check console'
|
||||||
|
|
||||||
if private and not reply.startswith('/'):
|
self._send_reply(reply, private, reply_target)
|
||||||
self.g.chat.send('/r ' + reply)
|
|
||||||
else:
|
|
||||||
self.g.chat.send(reply)
|
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -34,7 +34,13 @@ def component_text(value):
|
|||||||
if 'text' in fields:
|
if 'text' in fields:
|
||||||
return component_text(fields['text']) + component_text(fields.get('extra', []))
|
return component_text(fields['text']) + component_text(fields.get('extra', []))
|
||||||
if 'translate' in fields:
|
if 'translate' in fields:
|
||||||
return component_text(fields['translate']) + component_text(fields.get('with', []))
|
translation = component_text(fields['translate'])
|
||||||
|
arguments = fields.get('with', [])
|
||||||
|
if translation == 'commands.message.display.incoming' and len(arguments) >= 2:
|
||||||
|
return '{} whispers to you: {}'.format(
|
||||||
|
component_text(arguments[0]),
|
||||||
|
component_text(arguments[1]))
|
||||||
|
return translation + component_text(arguments)
|
||||||
return ''.join(component_text(v) for v in fields.values())
|
return ''.join(component_text(v) for v in fields.values())
|
||||||
if isinstance(value, (list, tuple)):
|
if isinstance(value, (list, tuple)):
|
||||||
return ''.join(component_text(item) for item in value)
|
return ''.join(component_text(item) for item in value)
|
||||||
@@ -135,12 +141,14 @@ def main(global_state=None):
|
|||||||
@connection.listener(SystemChat)
|
@connection.listener(SystemChat)
|
||||||
def on_system_chat(packet):
|
def on_system_chat(packet):
|
||||||
message = component_text(packet.content)
|
message = component_text(packet.content)
|
||||||
|
chat_handled = False
|
||||||
if global_state is not None:
|
if global_state is not None:
|
||||||
global_state.last_chat = message
|
global_state.last_chat = message
|
||||||
chat = global_state.get('chat')
|
chat = global_state.get('chat')
|
||||||
if chat is not None:
|
if chat is not None:
|
||||||
chat.handle_system_chat(packet, message)
|
chat_handled = chat.handle_system_chat(packet, message)
|
||||||
print('[server] ' + message, flush=True)
|
if not chat_handled:
|
||||||
|
print('[server] ' + message, flush=True)
|
||||||
lowered = message.lower()
|
lowered = message.lower()
|
||||||
if 'already registered' in lowered:
|
if 'already registered' in lowered:
|
||||||
send_auth_command('login')
|
send_auth_command('login')
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Chat adapter for the Minecraft 26.3 play protocol."""
|
"""Chat adapter for the Minecraft 26.3 play protocol."""
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
|
|
||||||
@@ -7,6 +8,20 @@ from minecraft.networking.modern import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_incoming_private_message(message):
|
||||||
|
"""Return (sender_name, body) for common Minecraft incoming-DM forms."""
|
||||||
|
patterns = (
|
||||||
|
r'^([A-Za-z0-9_]{1,16}) whispers(?: to you)?:\s*(.*)$',
|
||||||
|
r'^([A-Za-z0-9_]{1,16})\s*->\s*me:\s*(.*)$',
|
||||||
|
r'^From ([A-Za-z0-9_]{1,16}):\s*(.*)$',
|
||||||
|
)
|
||||||
|
for pattern in patterns:
|
||||||
|
match = re.match(pattern, message, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
return match.group(1), match.group(2)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ModernChatManager:
|
class ModernChatManager:
|
||||||
def __init__(self, global_state):
|
def __init__(self, global_state):
|
||||||
self.g = global_state
|
self.g = global_state
|
||||||
@@ -19,16 +34,48 @@ class ModernChatManager:
|
|||||||
|
|
||||||
def handle_player_chat(self, packet):
|
def handle_player_chat(self, packet):
|
||||||
sender = str(packet.sender)
|
sender = str(packet.sender)
|
||||||
text = '{}: {}'.format(sender, packet.message)
|
sender_name = self.g.get('player_names', {}).get(sender, sender)
|
||||||
print('[chat] {}: {}'.format(sender, packet.message), flush=True)
|
registry = getattr(self.g.connection, 'registry_ids', {})
|
||||||
|
chat_types = registry.get('minecraft:chat_type', {})
|
||||||
|
chat_type_id = getattr(packet, 'chat_type_id', None)
|
||||||
|
chat_type = next((name for name, index in chat_types.items()
|
||||||
|
if index == chat_type_id), None)
|
||||||
|
is_dm = (
|
||||||
|
getattr(packet, 'network_target_name_present', False) or
|
||||||
|
chat_type in (
|
||||||
|
'minecraft:msg_command_incoming',
|
||||||
|
'minecraft:msg_command_outgoing',
|
||||||
|
'minecraft:team_msg_command_incoming',
|
||||||
|
'minecraft:team_msg_command_outgoing',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
label = '[DM] ' if is_dm else ''
|
||||||
|
print('[chat] {}{}: {}'.format(label, sender_name, packet.message),
|
||||||
|
flush=True)
|
||||||
if self.handler:
|
if self.handler:
|
||||||
with self.g.get('reload_lock') or nullcontext():
|
with self.g.get('reload_lock') or nullcontext():
|
||||||
self.handler(('CHAT', sender, text))
|
if is_dm:
|
||||||
|
text = '{} -> me: {}'.format(sender_name, packet.message)
|
||||||
|
self.handler(('DM', sender, text, sender_name))
|
||||||
|
else:
|
||||||
|
text = '{}: {}'.format(sender_name, packet.message)
|
||||||
|
self.handler(('CHAT', sender, text))
|
||||||
|
|
||||||
def handle_system_chat(self, packet, message):
|
def handle_system_chat(self, packet, message):
|
||||||
|
incoming = parse_incoming_private_message(message)
|
||||||
|
if incoming:
|
||||||
|
sender_name, body = incoming
|
||||||
|
print('[chat] [DM] {}: {}'.format(sender_name, body), flush=True)
|
||||||
|
if self.handler:
|
||||||
|
text = '{} -> me: {}'.format(sender_name, body)
|
||||||
|
with self.g.get('reload_lock') or nullcontext():
|
||||||
|
self.handler(('DM', sender_name, text, sender_name))
|
||||||
|
return True
|
||||||
|
|
||||||
if self.handler:
|
if self.handler:
|
||||||
with self.g.get('reload_lock') or nullcontext():
|
with self.g.get('reload_lock') or nullcontext():
|
||||||
self.handler(('SYSTEM', None, message))
|
self.handler(('SYSTEM', None, message))
|
||||||
|
return False
|
||||||
|
|
||||||
def send(self, message):
|
def send(self, message):
|
||||||
if not message:
|
if not message:
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import struct
|
||||||
|
import unittest
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from munch import Munch
|
||||||
|
from minecraft.networking.modern import ClientboundPlayerChatPacket
|
||||||
|
|
||||||
|
from mosfet.commands import Commands
|
||||||
|
from mosfet.connect import component_text
|
||||||
|
from mosfet.protocol.modern import (
|
||||||
|
ModernChatManager, parse_incoming_private_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConnection:
|
||||||
|
def register_packet_listener(self, *_args, **_kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FakeChat:
|
||||||
|
def __init__(self):
|
||||||
|
self.messages = []
|
||||||
|
|
||||||
|
def set_handler(self, _handler):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send(self, message):
|
||||||
|
self.messages.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
class TestModernChat(unittest.TestCase):
|
||||||
|
def test_incoming_dm_translation_is_rendered_and_parsed(self):
|
||||||
|
rendered = component_text({
|
||||||
|
'translate': 'commands.message.display.incoming',
|
||||||
|
'with': ['tanner', '!ping'],
|
||||||
|
})
|
||||||
|
self.assertEqual(rendered, 'tanner whispers to you: !ping')
|
||||||
|
self.assertEqual(parse_incoming_private_message(rendered),
|
||||||
|
('tanner', '!ping'))
|
||||||
|
self.assertEqual(parse_incoming_private_message(
|
||||||
|
'tanner -> me: 1ping'), ('tanner', '1ping'))
|
||||||
|
|
||||||
|
def test_player_chat_console_uses_name_but_keeps_uuid_for_commands(self):
|
||||||
|
player_uuid = 'f6126022-2c68-37f7-be93-aaa551374663'
|
||||||
|
state = Munch(
|
||||||
|
connection=FakeConnection(),
|
||||||
|
player_names={player_uuid: 'tanner'},
|
||||||
|
reload_lock=None,
|
||||||
|
)
|
||||||
|
manager = ModernChatManager(state)
|
||||||
|
received = []
|
||||||
|
manager.set_handler(received.append)
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
manager.handle_player_chat(Munch(
|
||||||
|
sender=player_uuid, message='!ping', chat_type_id=1,
|
||||||
|
network_target_name_present=False))
|
||||||
|
|
||||||
|
self.assertEqual(output.getvalue(), '[chat] tanner: !ping\n')
|
||||||
|
self.assertEqual(received, [
|
||||||
|
('CHAT', player_uuid, 'tanner: !ping'),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_player_chat_target_name_marks_dm_and_routes_reply(self):
|
||||||
|
player_uuid = 'f6126022-2c68-37f7-be93-aaa551374663'
|
||||||
|
state = Munch(
|
||||||
|
connection=FakeConnection(),
|
||||||
|
player_names={player_uuid: 'tanner'},
|
||||||
|
reload_lock=None,
|
||||||
|
)
|
||||||
|
manager = ModernChatManager(state)
|
||||||
|
received = []
|
||||||
|
manager.set_handler(received.append)
|
||||||
|
output = io.StringIO()
|
||||||
|
packet = Munch(
|
||||||
|
sender=player_uuid, message='!ping', chat_type_id=2,
|
||||||
|
network_target_name_present=True)
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
manager.handle_player_chat(packet)
|
||||||
|
|
||||||
|
self.assertEqual(output.getvalue(), '[chat] [DM] tanner: !ping\n')
|
||||||
|
self.assertEqual(received, [
|
||||||
|
('DM', player_uuid, 'tanner -> me: !ping', 'tanner'),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_player_chat_parser_reads_optional_target_name(self):
|
||||||
|
def varint(value):
|
||||||
|
encoded = bytearray()
|
||||||
|
while value > 0x7f:
|
||||||
|
encoded.append((value & 0x7f) | 0x80)
|
||||||
|
value >>= 7
|
||||||
|
encoded.append(value)
|
||||||
|
return bytes(encoded)
|
||||||
|
|
||||||
|
message = b'!ping'
|
||||||
|
payload = (
|
||||||
|
varint(0) + uuid.UUID(int=0).bytes + varint(0) + b'\x00' +
|
||||||
|
varint(len(message)) + message +
|
||||||
|
struct.pack('>qq', 0, 0) + varint(0) + b'\x00' +
|
||||||
|
varint(0) + varint(2) + b'\x00\x01\x00'
|
||||||
|
)
|
||||||
|
packet = ClientboundPlayerChatPacket()
|
||||||
|
packet.read(io.BytesIO(payload))
|
||||||
|
|
||||||
|
self.assertEqual(packet.message, '!ping')
|
||||||
|
self.assertEqual(packet.chat_type_id, 2)
|
||||||
|
self.assertTrue(packet.network_target_name_present)
|
||||||
|
|
||||||
|
def test_chat_type_registry_identifies_dm_without_target_name(self):
|
||||||
|
player_uuid = 'f6126022-2c68-37f7-be93-aaa551374663'
|
||||||
|
state = Munch(
|
||||||
|
connection=FakeConnection(),
|
||||||
|
player_names={player_uuid: 'tanner'},
|
||||||
|
reload_lock=None,
|
||||||
|
)
|
||||||
|
state.connection.registry_ids = {
|
||||||
|
'minecraft:chat_type': {
|
||||||
|
'minecraft:msg_command_incoming': 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
manager = ModernChatManager(state)
|
||||||
|
received = []
|
||||||
|
manager.set_handler(received.append)
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
manager.handle_player_chat(Munch(
|
||||||
|
sender=player_uuid, message='!ping', chat_type_id=2,
|
||||||
|
network_target_name_present=False))
|
||||||
|
|
||||||
|
self.assertEqual(output.getvalue(), '[chat] [DM] tanner: !ping\n')
|
||||||
|
self.assertEqual(received, [
|
||||||
|
('DM', player_uuid, 'tanner -> me: !ping', 'tanner'),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_system_dm_console_uses_dm_prefix(self):
|
||||||
|
state = Munch(connection=FakeConnection(), reload_lock=None)
|
||||||
|
manager = ModernChatManager(state)
|
||||||
|
received = []
|
||||||
|
manager.set_handler(received.append)
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
handled = manager.handle_system_chat(
|
||||||
|
None, 'tanner whispers to you: !ping')
|
||||||
|
|
||||||
|
self.assertTrue(handled)
|
||||||
|
self.assertEqual(output.getvalue(), '[chat] [DM] tanner: !ping\n')
|
||||||
|
self.assertEqual(received, [
|
||||||
|
('DM', 'tanner', 'tanner -> me: !ping', 'tanner'),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_dm_command_replies_to_named_sender(self):
|
||||||
|
chat = FakeChat()
|
||||||
|
command = Commands.__new__(Commands)
|
||||||
|
command.g = Munch(name='mosfet1', chat=chat)
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
command.handle_chat((
|
||||||
|
'DM', 'f6126022-2c68-37f7-be93-aaa551374663',
|
||||||
|
'tanner -> me: !ping', 'tanner'))
|
||||||
|
|
||||||
|
self.assertIn('Reply: pong', output.getvalue())
|
||||||
|
self.assertEqual(chat.messages, ['/msg tanner pong'])
|
||||||
|
|
||||||
|
def test_here_dm_uses_sender_uuid_and_private_reply_name(self):
|
||||||
|
sender_uuid = 'f6126022-2c68-37f7-be93-aaa551374663'
|
||||||
|
chat = FakeChat()
|
||||||
|
command = Commands.__new__(Commands)
|
||||||
|
command.g = Munch(
|
||||||
|
name='mosfet1', chat=chat, pos=(0, 64, 0), path=[], job=None,
|
||||||
|
players={1: Munch(player_uuid=sender_uuid, x=10, y=64, z=20)},
|
||||||
|
world=Munch(path_to_place=lambda start, goal: [start, goal]),
|
||||||
|
)
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
command.handle_chat((
|
||||||
|
'DM', sender_uuid, 'tanner -> me: 1here', 'tanner'))
|
||||||
|
|
||||||
|
self.assertEqual(command.g.path, [(0, 64, 0), (10, 64, 20)])
|
||||||
|
self.assertIn('Reply: ok', output.getvalue())
|
||||||
|
self.assertEqual(chat.messages, ['/msg tanner ok'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user