Project

Profile

Help

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

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
(7-7/23)