Project

Profile

Help

Download (12.6 KB) Statistics View on GitHub Reload from mirrored respository
| Branch: | Tag: | Revision:

github / src / client.py @ master

1 7efcc006 Vincent Le Goff
# Copyright (c) 2016-2020, LE GOFF Vincent
2 2e7e9634 Vincent Le Goff
# 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 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
# Constants
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
        # Record commands
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
            # 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 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
            # 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 e1d2d0dc Vincent Le Goff
    def run(self):
172
        """Run the thread."""
173 6e131b55 Vincent Le Goff
        # Try to connect to the specified host and port
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
        # 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 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
                        # 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 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
        # Handle the remaining text
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
        # Execute the triggers
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
        # 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 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
        # 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 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
            # 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 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