github / src / client.py @ master
1 |
# Copyright (c) 2016-2020, LE GOFF Vincent |
---|---|
2 |
# All rights reserved.
|
3 |
|
4 |
# Redistribution and use in source and binary forms, with or without
|
5 |
# modification, are permitted provided that the following conditions are met:
|
6 |
|
7 |
# * Redistributions of source code must retain the above copyright notice, this
|
8 |
# list of conditions and the following disclaimer.
|
9 |
|
10 |
# * Redistributions in binary form must reproduce the above copyright notice,
|
11 |
# this list of conditions and the following disclaimer in the documentation
|
12 |
# and/or other materials provided with the distribution.
|
13 |
|
14 |
# * Neither the name of ytranslate nor the names of its
|
15 |
# contributors may be used to endorse or promote products derived from
|
16 |
# this software without specific prior written permission.
|
17 |
|
18 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19 |
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
20 |
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
21 |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
22 |
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
23 |
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
24 |
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
25 |
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
26 |
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
27 |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28 |
|
29 |
"""This file contains the client that can connect to a MUD.
|
30 |
|
31 |
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 |
|
35 |
"""
|
36 |
|
37 |
import os |
38 |
from random import randint |
39 |
import re |
40 |
import socket |
41 |
from telnetlib import Telnet, WONT, WILL, ECHO, NOP, AYT, IAC, GA |
42 |
import threading |
43 |
import time |
44 |
|
45 |
from twisted.internet import reactor |
46 |
from twisted.internet.error import ConnectionDone |
47 |
from twisted.internet.protocol import ReconnectingClientFactory |
48 |
from twisted.conch.telnet import Telnet |
49 |
import wx |
50 |
from wx.lib.pubsub import pub |
51 |
|
52 |
from log import logger |
53 |
from screenreader import ScreenReader |
54 |
from screenreader import ScreenReader |
55 |
|
56 |
# Constants
|
57 |
ANSI_ESCAPE = re.compile(r'\x1b[^m]*m')
|
58 |
|
59 |
class Client(Telnet): |
60 |
|
61 |
"""Class to receive data from the MUD using a Telnet protocol."""
|
62 |
|
63 |
def disconnect(self): |
64 |
"""Disconnect, close the client."""
|
65 |
if self.transport: |
66 |
self.transport.loseConnection()
|
67 |
|
68 |
self.running = False |
69 |
|
70 |
def connectionMade(self): |
71 |
"""Established connection, send the differed commands."""
|
72 |
self.has_GA = False |
73 |
self.queue = b"" |
74 |
self.defer = None |
75 |
self.anti_idle = None |
76 |
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 |
# Record commands
|
82 |
self.commandMap[GA] = self.handle_GA |
83 |
|
84 |
self.factory.resetDelay()
|
85 |
for command in self.factory.commands: |
86 |
self.transport.write(command.encode() + b"\r\n") |
87 |
|
88 |
self.factory.stopTrying()
|
89 |
|
90 |
def connectionLost(self, reason): |
91 |
"""The connection was lost."""
|
92 |
self.send_queue()
|
93 |
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 |
wx.CallAfter(pub.sendMessage, "disconnect", client=self, |
99 |
reason=reason) |
100 |
if reason.type is ConnectionDone: |
101 |
self.factory.stopTrying()
|
102 |
|
103 |
def applicationDataReceived(self, data): |
104 |
"""Receive something."""
|
105 |
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 |
# Cancel the deferred, if exists
|
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 |
|
144 |
def reverse_anti_idle(self, verbose=False, to_panel=False): |
145 |
"""Reverse anti-idle."""
|
146 |
if self.anti_idle: |
147 |
# Terminate
|
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 |
# Begin anti idle
|
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 |
def run(self): |
172 |
"""Run the thread."""
|
173 |
# Try to connect to the specified host and port
|
174 |
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 |
log = logger("client")
|
179 |
log.info("Connecting {protocol} client for {host}:{port}".format(
|
180 |
protocol=protocol, host=host, port=port)) |
181 |
self.running = True |
182 |
|
183 |
def handle_lines(self, msg): |
184 |
"""Handle multiple lines of text."""
|
185 |
mark = None
|
186 |
lines = [] |
187 |
no_ansi_lines = [] |
188 |
triggers = [] |
189 |
|
190 |
# Line breaks are different whether rich text is used or not
|
191 |
if self.factory.panel and self.factory.panel.rich: |
192 |
nl = "\n"
|
193 |
else:
|
194 |
nl = "\r\n"
|
195 |
|
196 |
for line in msg.splitlines(): |
197 |
no_ansi_line = ANSI_ESCAPE.sub('', line)
|
198 |
display = True
|
199 |
for trigger in self.factory.world.triggers: |
200 |
trigger.sharp_engine = self.factory.sharp_engine
|
201 |
try:
|
202 |
match = trigger.test(no_ansi_line) |
203 |
except Exception: |
204 |
log = logger("client")
|
205 |
log.exception("The trigger {} failed".format(
|
206 |
repr(trigger.readction)))
|
207 |
else:
|
208 |
if match:
|
209 |
triggers.append((trigger, match, no_ansi_line)) |
210 |
if trigger.mute:
|
211 |
display = False
|
212 |
if trigger.mark and mark is None: |
213 |
before = nl.join([l for l in no_ansi_lines]) |
214 |
mark = len(before) + len(nl) |
215 |
|
216 |
# Handle triggers with substitution
|
217 |
if trigger.substitution:
|
218 |
display = False
|
219 |
trigger.set_variables(match) |
220 |
replacement = trigger.replace() |
221 |
lines.extend(replacement.splitlines()) |
222 |
|
223 |
if display:
|
224 |
if self.factory.strip_ansi: |
225 |
lines.append(no_ansi_line) |
226 |
else:
|
227 |
lines.append(line) |
228 |
|
229 |
if no_ansi_line.strip():
|
230 |
no_ansi_lines.append(no_ansi_line) |
231 |
|
232 |
# Handle the remaining text
|
233 |
try:
|
234 |
lines = [l for l in lines if l] |
235 |
self.handle_message("\r\n".join(lines), mark=mark) |
236 |
except Exception: |
237 |
log = logger("client")
|
238 |
log.exception( |
239 |
"An error occurred when handling a message")
|
240 |
|
241 |
# Execute the triggers
|
242 |
for trigger, match, line in triggers: |
243 |
trigger.set_variables(match) |
244 |
trigger.execute() |
245 |
|
246 |
def handle_message(self, msg, force_TTS=False, screen=True, |
247 |
speech=True, braille=True, mark=None): |
248 |
"""When the client receives a message.
|
249 |
|
250 |
Args:
|
251 |
msg: the text to be displayed (str)
|
252 |
force_TTS: should the text be spoken regardless?
|
253 |
screen: should the text appear on screen?
|
254 |
speech: should the speech be enabled?
|
255 |
braille: should the braille be enabled?
|
256 |
mark: the index where to move the cursor.
|
257 |
|
258 |
"""
|
259 |
if screen:
|
260 |
if self.factory.engine.redirect_message: |
261 |
self.factory.engine.redirect_message(msg)
|
262 |
else:
|
263 |
wx.CallAfter(pub.sendMessage, "message", client=self, |
264 |
message=msg, mark=mark) |
265 |
|
266 |
# In any case, tries to find the TTS
|
267 |
msg = ANSI_ESCAPE.sub('', msg)
|
268 |
panel = self.factory.panel
|
269 |
if self.factory.engine.TTS_on or force_TTS: |
270 |
# If outside of the window
|
271 |
tts = False
|
272 |
if force_TTS:
|
273 |
tts = True
|
274 |
elif panel.inside and panel.focus: |
275 |
tts = True
|
276 |
elif not panel.inside and panel.engine.TTS_outside: |
277 |
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 |
|
285 |
def write(self, text, alias=True): |
286 |
"""Write text to the client."""
|
287 |
# Break in chunks based on the command stacking, if active
|
288 |
settings = self.factory.engine.settings
|
289 |
stacking = settings["options.input.command_stacking"]
|
290 |
encoding = settings["options.general.encoding"]
|
291 |
chunks = [text] |
292 |
if stacking:
|
293 |
delimiter = re.escape(stacking) |
294 |
re_stacking = u"(?<!{s}){s}(?!{s})".format(s=delimiter)
|
295 |
re_del = re.compile(re_stacking, re.UNICODE) |
296 |
chunks = re_del.split(text) |
297 |
|
298 |
# Reset ;; as ; (or other command stacking character)
|
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 |
# Test the aliases
|
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 |
if not text.endswith("\r\n"): |
315 |
text += "\r\n"
|
316 |
|
317 |
self.transport.write(text.encode(encoding, errors="replace")) |
318 |
|
319 |
def test_macros(self, key, modifiers): |
320 |
"""Test the macros of this world."""
|
321 |
found = False
|
322 |
with self.factory.world.lock: |
323 |
for macro in self.factory.world.macros: |
324 |
code = (macro.key, macro.modifiers) |
325 |
macro.sharp_engine = self.factory.sharp_engine
|
326 |
if code == (key, modifiers):
|
327 |
macro.execute(self.factory.engine, self) |
328 |
found = True
|
329 |
break
|
330 |
|
331 |
return found
|
332 |
|
333 |
|
334 |
class CocoFactory(ReconnectingClientFactory): |
335 |
|
336 |
"""Factory used by CocoMUD client to generate Telnet clients."""
|
337 |
|
338 |
def __init__(self, world, session, panel): |
339 |
self.world = world
|
340 |
self.session = session
|
341 |
self.panel = panel
|
342 |
self.engine = world.engine
|
343 |
self.sharp_engine = session.sharp_engine
|
344 |
self.commands = []
|
345 |
self.strip_ansi = False |
346 |
|
347 |
def buildProtocol(self, addr): |
348 |
client = Client() |
349 |
client.factory = self
|
350 |
client.run() |
351 |
self.panel.client = client
|
352 |
self.sharp_engine.bind_client(client)
|
353 |
return client
|