1 |
7efcc006
|
Vincent Le Goff
|
|
2 |
2e7e9634
|
Vincent Le Goff
|
|
3 |
|
|
|
4 |
|
|
|
5 |
|
|
|
6 |
|
|
|
7 |
|
|
|
8 |
|
|
|
9 |
|
|
|
10 |
|
|
|
11 |
|
|
|
12 |
|
|
|
13 |
|
|
|
14 |
|
|
|
15 |
|
|
|
16 |
|
|
|
17 |
|
|
|
18 |
|
|
|
19 |
|
|
|
20 |
|
|
|
21 |
|
|
|
22 |
|
|
|
23 |
|
|
|
24 |
|
|
|
25 |
|
|
|
26 |
|
|
|
27 |
|
|
|
28 |
|
|
|
29 |
|
|
"""This file contains the client that can connect to a MUD.
|
30 |
e1d2d0dc
|
Vincent Le Goff
|
|
31 |
e9cc2ac8
|
Vincent Le Goff
|
Starting from CocoMUD 45, the network is handled by Twisted. The
|
32 |
|
|
Client class inherits from 'twisted.conch.telnet.Telnet', which
|
33 |
|
|
already handles a great deal of the Telnet protocol.
|
34 |
e1d2d0dc
|
Vincent Le Goff
|
|
35 |
|
|
"""
|
36 |
|
|
|
37 |
4d2b7a46
|
Vincent Le Goff
|
import os
|
38 |
51965cb1
|
Vincent Le Goff
|
from random import randint
|
39 |
e1d2d0dc
|
Vincent Le Goff
|
import re
|
40 |
c377606f
|
Vincent Le Goff
|
import socket
|
41 |
1f0e117b
|
Vincent Le Goff
|
from telnetlib import Telnet, WONT, WILL, ECHO, NOP, AYT, IAC, GA
|
42 |
e1d2d0dc
|
Vincent Le Goff
|
import threading
|
43 |
|
|
import time
|
44 |
|
|
|
45 |
1f0e117b
|
Vincent Le Goff
|
from twisted.internet import reactor
|
46 |
e9cc2ac8
|
Vincent Le Goff
|
from twisted.internet.error import ConnectionDone
|
47 |
|
|
from twisted.internet.protocol import ReconnectingClientFactory
|
48 |
|
|
from twisted.conch.telnet import Telnet
|
49 |
|
|
import wx
|
50 |
8f68a7b0
|
Vincent Le Goff
|
from wx.lib.pubsub import pub
|
51 |
e9cc2ac8
|
Vincent Le Goff
|
|
52 |
d0fd91d5
|
Vincent Le Goff
|
from log import logger
|
53 |
59b194ab
|
Vincent Le Goff
|
from screenreader import ScreenReader
|
54 |
e9cc2ac8
|
Vincent Le Goff
|
from screenreader import ScreenReader
|
55 |
fa348e33
|
Vincent Le Goff
|
|
56 |
e1d2d0dc
|
Vincent Le Goff
|
|
57 |
|
|
ANSI_ESCAPE = re.compile(r'\x1b[^m]*m')
|
58 |
|
|
|
59 |
e9cc2ac8
|
Vincent Le Goff
|
class Client(Telnet):
|
60 |
e1d2d0dc
|
Vincent Le Goff
|
|
61 |
e9cc2ac8
|
Vincent Le Goff
|
"""Class to receive data from the MUD using a Telnet protocol."""
|
62 |
e1d2d0dc
|
Vincent Le Goff
|
|
63 |
a5b9bff4
|
Vincent Le Goff
|
def disconnect(self):
|
64 |
|
|
"""Disconnect, close the client."""
|
65 |
e9cc2ac8
|
Vincent Le Goff
|
if self.transport:
|
66 |
|
|
self.transport.loseConnection()
|
67 |
a5b9bff4
|
Vincent Le Goff
|
|
68 |
|
|
self.running = False
|
69 |
|
|
|
70 |
e9cc2ac8
|
Vincent Le Goff
|
def connectionMade(self):
|
71 |
|
|
"""Established connection, send the differed commands."""
|
72 |
1f0e117b
|
Vincent Le Goff
|
self.has_GA = False
|
73 |
|
|
self.queue = b""
|
74 |
|
|
self.defer = None
|
75 |
51965cb1
|
Vincent Le Goff
|
self.anti_idle = None
|
76 |
e9cc2ac8
|
Vincent Le Goff
|
host = self.transport.getPeer().host
|
77 |
|
|
port = self.transport.getPeer().port
|
78 |
|
|
log = logger("client")
|
79 |
|
|
log.info("Connected to {host}:{port}".format(
|
80 |
|
|
host=host, port=port))
|
81 |
1f0e117b
|
Vincent Le Goff
|
|
82 |
|
|
self.commandMap[GA] = self.handle_GA
|
83 |
|
|
|
84 |
e9cc2ac8
|
Vincent Le Goff
|
self.factory.resetDelay()
|
85 |
|
|
for command in self.factory.commands:
|
86 |
de07fb33
|
Vincent Le Goff
|
self.transport.write(command.encode() + b"\r\n")
|
87 |
e9cc2ac8
|
Vincent Le Goff
|
|
88 |
51965cb1
|
Vincent Le Goff
|
self.factory.stopTrying()
|
89 |
|
|
|
90 |
e9cc2ac8
|
Vincent Le Goff
|
def connectionLost(self, reason):
|
91 |
|
|
"""The connection was lost."""
|
92 |
51965cb1
|
Vincent Le Goff
|
self.send_queue()
|
93 |
e9cc2ac8
|
Vincent Le Goff
|
host = self.transport.getPeer().host
|
94 |
|
|
port = self.transport.getPeer().port
|
95 |
|
|
log = logger("client")
|
96 |
|
|
log.info("Lost Connection on {host}:{port}: {reason}".format(
|
97 |
|
|
host=host, port=port, reason=reason.type))
|
98 |
e1e0950a
|
Vincent Le Goff
|
wx.CallAfter(pub.sendMessage, "disconnect", client=self,
|
99 |
|
|
reason=reason)
|
100 |
e9cc2ac8
|
Vincent Le Goff
|
if reason.type is ConnectionDone:
|
101 |
|
|
self.factory.stopTrying()
|
102 |
|
|
|
103 |
|
|
def applicationDataReceived(self, data):
|
104 |
|
|
"""Receive something."""
|
105 |
1f0e117b
|
Vincent Le Goff
|
if self.has_GA:
|
106 |
|
|
self.queue += data
|
107 |
|
|
if self.defer:
|
108 |
|
|
self.defer.cancel()
|
109 |
|
|
self.defer = reactor.callLater(0.2, self.send_queue)
|
110 |
|
|
else:
|
111 |
|
|
if self.queue:
|
112 |
|
|
data = self.queue + b"\r\n" + data
|
113 |
|
|
self.queue = b""
|
114 |
|
|
|
115 |
|
|
|
116 |
|
|
if self.defer:
|
117 |
|
|
self.defer.cancel()
|
118 |
|
|
self.defer = None
|
119 |
|
|
|
120 |
|
|
encoding = self.factory.engine.settings["options.general.encoding"]
|
121 |
|
|
msg = data.decode(encoding, errors="replace")
|
122 |
|
|
with self.factory.world.lock:
|
123 |
|
|
self.handle_lines(msg)
|
124 |
|
|
|
125 |
|
|
def send_queue(self):
|
126 |
|
|
old_GA = self.has_GA
|
127 |
|
|
self.has_GA = False
|
128 |
|
|
self.defer = None
|
129 |
|
|
if self.queue:
|
130 |
|
|
queue = self.queue
|
131 |
|
|
self.queue = b""
|
132 |
|
|
self.applicationDataReceived(queue)
|
133 |
|
|
self.has_GA = old_GA
|
134 |
|
|
|
135 |
|
|
def handle_GA(self, *args, **kwargs):
|
136 |
|
|
"""Handle the Telnet Go-Ahead."""
|
137 |
|
|
self.has_GA = False
|
138 |
|
|
if self.queue:
|
139 |
|
|
queue = self.queue
|
140 |
|
|
self.queue = b""
|
141 |
|
|
self.applicationDataReceived(queue)
|
142 |
|
|
self.has_GA = True
|
143 |
de33964a
|
Vincent Le Goff
|
|
144 |
51965cb1
|
Vincent Le Goff
|
def reverse_anti_idle(self, verbose=False, to_panel=False):
|
145 |
|
|
"""Reverse anti-idle."""
|
146 |
|
|
if self.anti_idle:
|
147 |
|
|
|
148 |
|
|
if verbose:
|
149 |
|
|
self.handle_message("Anti idle off.")
|
150 |
|
|
self.anti_idle.cancel()
|
151 |
|
|
self.anti_idle = None
|
152 |
|
|
if to_panel:
|
153 |
|
|
self.factory.panel.window.gameMenu.Check(
|
154 |
|
|
self.factory.panel.window.chk_anti_idle.GetId(), False)
|
155 |
|
|
else:
|
156 |
|
|
|
157 |
|
|
if verbose:
|
158 |
|
|
self.handle_message("Anti idle on.")
|
159 |
|
|
self.anti_idle = reactor.callLater(1, self.keep_anti_idle)
|
160 |
|
|
if to_panel:
|
161 |
|
|
self.factory.panel.window.gameMenu.Check(
|
162 |
|
|
self.factory.panel.window.chk_anti_idle.GetId(), True)
|
163 |
|
|
|
164 |
|
|
def keep_anti_idle(self):
|
165 |
|
|
"""Keep the anti-idle active."""
|
166 |
|
|
self.transport.write(b"\r\n")
|
167 |
|
|
next_time = randint(30, 60)
|
168 |
|
|
next_time += randint(1, 100) / 100
|
169 |
|
|
self.anti_idle = reactor.callLater(next_time, self.keep_anti_idle)
|
170 |
|
|
|
171 |
e1d2d0dc
|
Vincent Le Goff
|
def run(self):
|
172 |
|
|
"""Run the thread."""
|
173 |
6e131b55
|
Vincent Le Goff
|
|
174 |
5e6fddad
|
Vincent Le Goff
|
host = self.factory.world.hostname
|
175 |
|
|
port = self.factory.world.port
|
176 |
|
|
protocol = self.factory.world.protocol.lower()
|
177 |
|
|
protocol = "SSL" if protocol == "ssl" else "telnet"
|
178 |
e9cc2ac8
|
Vincent Le Goff
|
log = logger("client")
|
179 |
5e6fddad
|
Vincent Le Goff
|
log.info("Connecting {protocol} client for {host}:{port}".format(
|
180 |
|
|
protocol=protocol, host=host, port=port))
|
181 |
6e131b55
|
Vincent Le Goff
|
self.running = True
|
182 |
058529ee
|
Vincent Le Goff
|
|
183 |
c8dee3b4
|
Vincent Le Goff
|
def handle_lines(self, msg):
|
184 |
|
|
"""Handle multiple lines of text."""
|
185 |
0fd19395
|
Vincent Le Goff
|
mark = None
|
186 |
c8dee3b4
|
Vincent Le Goff
|
lines = []
|
187 |
875753b1
|
Vincent Le Goff
|
no_ansi_lines = []
|
188 |
c8dee3b4
|
Vincent Le Goff
|
triggers = []
|
189 |
060e5a35
|
Vincent Le Goff
|
|
190 |
|
|
|
191 |
|
|
if self.factory.panel and self.factory.panel.rich:
|
192 |
|
|
nl = "\n"
|
193 |
|
|
else:
|
194 |
|
|
nl = "\r\n"
|
195 |
|
|
|
196 |
c8dee3b4
|
Vincent Le Goff
|
for line in msg.splitlines():
|
197 |
6824bc76
|
Vincent Le Goff
|
no_ansi_line = ANSI_ESCAPE.sub('', line)
|
198 |
c8dee3b4
|
Vincent Le Goff
|
display = True
|
199 |
e9cc2ac8
|
Vincent Le Goff
|
for trigger in self.factory.world.triggers:
|
200 |
|
|
trigger.sharp_engine = self.factory.sharp_engine
|
201 |
c8dee3b4
|
Vincent Le Goff
|
try:
|
202 |
0873e347
|
Vincent Le Goff
|
match = trigger.test(no_ansi_line)
|
203 |
c8dee3b4
|
Vincent Le Goff
|
except Exception:
|
204 |
|
|
log = logger("client")
|
205 |
|
|
log.exception("The trigger {} failed".format(
|
206 |
|
|
repr(trigger.readction)))
|
207 |
|
|
else:
|
208 |
0873e347
|
Vincent Le Goff
|
if match:
|
209 |
|
|
triggers.append((trigger, match, no_ansi_line))
|
210 |
c8dee3b4
|
Vincent Le Goff
|
if trigger.mute:
|
211 |
|
|
display = False
|
212 |
0fd19395
|
Vincent Le Goff
|
if trigger.mark and mark is None:
|
213 |
060e5a35
|
Vincent Le Goff
|
before = nl.join([l for l in no_ansi_lines])
|
214 |
|
|
mark = len(before) + len(nl)
|
215 |
c8dee3b4
|
Vincent Le Goff
|
|
216 |
0873e347
|
Vincent Le Goff
|
|
217 |
|
|
if trigger.substitution:
|
218 |
|
|
display = False
|
219 |
|
|
trigger.set_variables(match)
|
220 |
|
|
replacement = trigger.replace()
|
221 |
|
|
lines.extend(replacement.splitlines())
|
222 |
|
|
|
223 |
c8dee3b4
|
Vincent Le Goff
|
if display:
|
224 |
e9cc2ac8
|
Vincent Le Goff
|
if self.factory.strip_ansi:
|
225 |
f2eac2d3
|
Vincent Le Goff
|
lines.append(no_ansi_line)
|
226 |
|
|
else:
|
227 |
|
|
lines.append(line)
|
228 |
|
|
|
229 |
875753b1
|
Vincent Le Goff
|
if no_ansi_line.strip():
|
230 |
|
|
no_ansi_lines.append(no_ansi_line)
|
231 |
c8dee3b4
|
Vincent Le Goff
|
|
232 |
|
|
|
233 |
|
|
try:
|
234 |
e48771da
|
Vincent Le Goff
|
lines = [l for l in lines if l]
|
235 |
0fd19395
|
Vincent Le Goff
|
self.handle_message("\r\n".join(lines), mark=mark)
|
236 |
c8dee3b4
|
Vincent Le Goff
|
except Exception:
|
237 |
|
|
log = logger("client")
|
238 |
|
|
log.exception(
|
239 |
|
|
"An error occurred when handling a message")
|
240 |
|
|
|
241 |
|
|
|
242 |
0873e347
|
Vincent Le Goff
|
for trigger, match, line in triggers:
|
243 |
|
|
trigger.set_variables(match)
|
244 |
|
|
trigger.execute()
|
245 |
c8dee3b4
|
Vincent Le Goff
|
|
246 |
a4ca19ed
|
Vincent Le Goff
|
def handle_message(self, msg, force_TTS=False, screen=True,
|
247 |
0fd19395
|
Vincent Le Goff
|
speech=True, braille=True, mark=None):
|
248 |
11d4f23d
|
Vincent Le Goff
|
"""When the client receives a message.
|
249 |
|
|
|
250 |
e48771da
|
Vincent Le Goff
|
Args:
|
251 |
a4ca19ed
|
Vincent Le Goff
|
msg: the text to be displayed (str)
|
252 |
11d4f23d
|
Vincent Le Goff
|
force_TTS: should the text be spoken regardless?
|
253 |
a4ca19ed
|
Vincent Le Goff
|
screen: should the text appear on screen?
|
254 |
11d4f23d
|
Vincent Le Goff
|
speech: should the speech be enabled?
|
255 |
|
|
braille: should the braille be enabled?
|
256 |
e9cc2ac8
|
Vincent Le Goff
|
mark: the index where to move the cursor.
|
257 |
11d4f23d
|
Vincent Le Goff
|
|
258 |
|
|
"""
|
259 |
e9cc2ac8
|
Vincent Le Goff
|
if screen:
|
260 |
e48771da
|
Vincent Le Goff
|
if self.factory.engine.redirect_message:
|
261 |
|
|
self.factory.engine.redirect_message(msg)
|
262 |
|
|
else:
|
263 |
|
|
wx.CallAfter(pub.sendMessage, "message", client=self,
|
264 |
e1e0950a
|
Vincent Le Goff
|
message=msg, mark=mark)
|
265 |
e9cc2ac8
|
Vincent Le Goff
|
|
266 |
|
|
|
267 |
|
|
msg = ANSI_ESCAPE.sub('', msg)
|
268 |
|
|
panel = self.factory.panel
|
269 |
|
|
if self.factory.engine.TTS_on or force_TTS:
|
270 |
|
|
|
271 |
|
|
tts = False
|
272 |
|
|
if force_TTS:
|
273 |
|
|
tts = True
|
274 |
|
|
elif panel.inside and panel.focus:
|
275 |
|
|
tts = True
|
276 |
e48771da
|
Vincent Le Goff
|
elif not panel.inside and panel.engine.TTS_outside:
|
277 |
e9cc2ac8
|
Vincent Le Goff
|
tts = True
|
278 |
|
|
|
279 |
|
|
if tts:
|
280 |
|
|
interrupt = self.factory.engine.settings[
|
281 |
|
|
"options.TTS.interrupt"]
|
282 |
|
|
ScreenReader.talk(msg, speech=speech, braille=braille,
|
283 |
|
|
interrupt=interrupt)
|
284 |
e1d2d0dc
|
Vincent Le Goff
|
|
285 |
22c2e2e8
|
Vincent Le Goff
|
def write(self, text, alias=True):
|
286 |
fa348e33
|
Vincent Le Goff
|
"""Write text to the client."""
|
287 |
e9cc2ac8
|
Vincent Le Goff
|
|
288 |
|
|
settings = self.factory.engine.settings
|
289 |
|
|
stacking = settings["options.input.command_stacking"]
|
290 |
|
|
encoding = settings["options.general.encoding"]
|
291 |
f779b58f
|
Vincent Le Goff
|
chunks = [text]
|
292 |
e9cc2ac8
|
Vincent Le Goff
|
if stacking:
|
293 |
|
|
delimiter = re.escape(stacking)
|
294 |
96664dff
|
Vincent Le Goff
|
re_stacking = u"(?<!{s}){s}(?!{s})".format(s=delimiter)
|
295 |
4102863f
|
Vincent Le Goff
|
re_del = re.compile(re_stacking, re.UNICODE)
|
296 |
e9cc2ac8
|
Vincent Le Goff
|
chunks = re_del.split(text)
|
297 |
|
|
|
298 |
|
|
|
299 |
|
|
def reset_del(match):
|
300 |
|
|
return match.group(0)[1:]
|
301 |
|
|
|
302 |
|
|
for i, chunk in enumerate(chunks):
|
303 |
|
|
chunks[i] = re.sub(delimiter + "{2,}", reset_del, chunk)
|
304 |
|
|
|
305 |
|
|
with self.factory.world.lock:
|
306 |
|
|
for text in chunks:
|
307 |
|
|
|
308 |
|
|
if alias:
|
309 |
|
|
for alias in self.factory.world.aliases:
|
310 |
|
|
alias.sharp_engine = self.factory.sharp_engine
|
311 |
|
|
if alias.test(text):
|
312 |
|
|
return
|
313 |
|
|
|
314 |
4102863f
|
Vincent Le Goff
|
if not text.endswith("\r\n"):
|
315 |
|
|
text += "\r\n"
|
316 |
e9cc2ac8
|
Vincent Le Goff
|
|
317 |
de07fb33
|
Vincent Le Goff
|
self.transport.write(text.encode(encoding, errors="replace"))
|
318 |
fa348e33
|
Vincent Le Goff
|
|
319 |
28046bf5
|
Vincent Le Goff
|
def test_macros(self, key, modifiers):
|
320 |
|
|
"""Test the macros of this world."""
|
321 |
|
|
found = False
|
322 |
e9cc2ac8
|
Vincent Le Goff
|
with self.factory.world.lock:
|
323 |
|
|
for macro in self.factory.world.macros:
|
324 |
28046bf5
|
Vincent Le Goff
|
code = (macro.key, macro.modifiers)
|
325 |
e9cc2ac8
|
Vincent Le Goff
|
macro.sharp_engine = self.factory.sharp_engine
|
326 |
28046bf5
|
Vincent Le Goff
|
if code == (key, modifiers):
|
327 |
e9cc2ac8
|
Vincent Le Goff
|
macro.execute(self.factory.engine, self)
|
328 |
28046bf5
|
Vincent Le Goff
|
found = True
|
329 |
|
|
break
|
330 |
|
|
|
331 |
|
|
return found
|
332 |
|
|
|
333 |
|
|
|
334 |
e9cc2ac8
|
Vincent Le Goff
|
class CocoFactory(ReconnectingClientFactory):
|
335 |
e1d2d0dc
|
Vincent Le Goff
|
|
336 |
e9cc2ac8
|
Vincent Le Goff
|
"""Factory used by CocoMUD client to generate Telnet clients."""
|
337 |
e1d2d0dc
|
Vincent Le Goff
|
|
338 |
ee1d7ad7
|
Vincent Le Goff
|
def __init__(self, world, session, panel):
|
339 |
e9cc2ac8
|
Vincent Le Goff
|
self.world = world
|
340 |
ee1d7ad7
|
Vincent Le Goff
|
self.session = session
|
341 |
e9cc2ac8
|
Vincent Le Goff
|
self.panel = panel
|
342 |
|
|
self.engine = world.engine
|
343 |
ee1d7ad7
|
Vincent Le Goff
|
self.sharp_engine = session.sharp_engine
|
344 |
e9cc2ac8
|
Vincent Le Goff
|
self.commands = []
|
345 |
|
|
self.strip_ansi = False
|
346 |
e1d2d0dc
|
Vincent Le Goff
|
|
347 |
e9cc2ac8
|
Vincent Le Goff
|
def buildProtocol(self, addr):
|
348 |
|
|
client = Client()
|
349 |
|
|
client.factory = self
|
350 |
5e6fddad
|
Vincent Le Goff
|
client.run()
|
351 |
e9cc2ac8
|
Vincent Le Goff
|
self.panel.client = client
|
352 |
ded462db
|
Vincent Le Goff
|
self.sharp_engine.bind_client(client)
|
353 |
e9cc2ac8
|
Vincent Le Goff
|
return client |