Project

Profile

Help

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

github / src / client.py @ 96664dff

1
# Copyright (c) 2016, LE GOFF Vincent
2
# Copyright (c) 2016, LE GOFF Vincent
3
# All rights reserved.
4

    
5
# Redistribution and use in source and binary forms, with or without
6
# modification, are permitted provided that the following conditions are met:
7

    
8
# * Redistributions of source code must retain the above copyright notice, this
9
#   list of conditions and the following disclaimer.
10

    
11
# * Redistributions in binary form must reproduce the above copyright notice,
12
#   this list of conditions and the following disclaimer in the documentation
13
#   and/or other materials provided with the distribution.
14

    
15
# * Neither the name of ytranslate nor the names of its
16
#   contributors may be used to endorse or promote products derived from
17
#   this software without specific prior written permission.
18

    
19
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29

    
30
"""This file contains the client that can connect to a MUD.
31

32
Starting from CocoMUD 45, the network is handled by Twisted.  The
33
Client class inherits from 'twisted.conch.telnet.Telnet', which
34
already handles a great deal of the Telnet protocol.
35

36
"""
37

    
38
import os
39
import re
40
import socket
41
from telnetlib import Telnet, WONT, WILL, ECHO, NOP, AYT, IAC
42
import threading
43
import time
44

    
45
from twisted.internet.error import ConnectionDone
46
from twisted.internet.protocol import ReconnectingClientFactory
47
from twisted.conch.telnet import Telnet
48
import wx
49
from wx.lib.pubsub import pub, setupkwargs
50

    
51
from log import logger
52
from screenreader import ScreenReader
53
from screenreader import ScreenReader
54

    
55
# Constants
56
ANSI_ESCAPE = re.compile(r'\x1b[^m]*m')
57

    
58
class Client(Telnet):
59

    
60
    """Class to receive data from the MUD using a Telnet protocol."""
61

    
62
    def disconnect(self):
63
        """Disconnect, close the client."""
64
        if self.transport:
65
            self.transport.loseConnection()
66

    
67
        self.running = False
68

    
69
    def connectionMade(self):
70
        """Established connection, send the differed commands."""
71
        host = self.transport.getPeer().host
72
        port = self.transport.getPeer().port
73
        log = logger("client")
74
        log.info("Connected to {host}:{port}".format(
75
                host=host, port=port))
76
        self.factory.resetDelay()
77
        for command in self.factory.commands:
78
            self.transport.write(command + "\r\n")
79

    
80
    def connectionLost(self, reason):
81
        """The connection was lost."""
82
        host = self.transport.getPeer().host
83
        port = self.transport.getPeer().port
84
        log = logger("client")
85
        log.info("Lost Connection on {host}:{port}: {reason}".format(
86
                host=host, port=port, reason=reason.type))
87
        wx.CallAfter(pub.sendMessage, "disconnect", client=self,
88
                reason=reason)
89
        if reason.type is ConnectionDone:
90
            self.factory.stopTrying()
91

    
92
    def applicationDataReceived(self, data):
93
        """Receive something."""
94
        encoding = self.factory.engine.settings["options.general.encoding"]
95
        msg = data.decode(encoding, errors="replace")
96
        with self.factory.world.lock:
97
            self.handle_lines(msg)
98

    
99
    def run(self):
100
        """Run the thread."""
101
        # Try to connect to the specified host and port
102
        host = self.factory.world.hostname
103
        port = self.factory.world.port
104
        protocol = self.factory.world.protocol.lower()
105
        protocol = "SSL" if protocol == "ssl" else "telnet"
106
        log = logger("client")
107
        log.info("Connecting {protocol} client for {host}:{port}".format(
108
                protocol=protocol, host=host, port=port))
109
        self.running = True
110

    
111
    def handle_lines(self, msg):
112
        """Handle multiple lines of text."""
113
        mark = None
114
        lines = []
115
        no_ansi_lines = []
116
        triggers = []
117

    
118
        # Line breaks are different whether rich text is used or not
119
        if self.factory.panel and self.factory.panel.rich:
120
            nl = "\n"
121
        else:
122
            nl = "\r\n"
123

    
124
        for line in msg.splitlines():
125
            no_ansi_line = ANSI_ESCAPE.sub('', line)
126
            display = True
127
            for trigger in self.factory.world.triggers:
128
                trigger.sharp_engine = self.factory.sharp_engine
129
                try:
130
                    match = trigger.test(no_ansi_line)
131
                except Exception:
132
                    log = logger("client")
133
                    log.exception("The trigger {} failed".format(
134
                            repr(trigger.readction)))
135
                else:
136
                    if match:
137
                        triggers.append((trigger, match, no_ansi_line))
138
                        if trigger.mute:
139
                            display = False
140
                        if trigger.mark and mark is None:
141
                            before = nl.join([l for l in no_ansi_lines])
142
                            mark = len(before) + len(nl)
143

    
144
                        # Handle triggers with substitution
145
                        if trigger.substitution:
146
                            display = False
147
                            trigger.set_variables(match)
148
                            replacement = trigger.replace()
149
                            lines.extend(replacement.splitlines())
150

    
151
            if display:
152
                if self.factory.strip_ansi:
153
                    lines.append(no_ansi_line)
154
                else:
155
                    lines.append(line)
156

    
157
                if no_ansi_line.strip():
158
                    no_ansi_lines.append(no_ansi_line)
159

    
160
        # Handle the remaining text
161
        try:
162
            liens = [l for l in lines if l]
163
            self.handle_message("\r\n".join(lines), mark=mark)
164
        except Exception:
165
            log = logger("client")
166
            log.exception(
167
                    "An error occurred when handling a message")
168

    
169
        # Execute the triggers
170
        for trigger, match, line in triggers:
171
            trigger.set_variables(match)
172
            trigger.execute()
173

    
174
    def handle_message(self, msg, force_TTS=False, screen=True,
175
            speech=True, braille=True, mark=None):
176
        """When the client receives a message.
177

178
        Parameters
179
            msg: the text to be displayed (str)
180
            force_TTS: should the text be spoken regardless?
181
            screen: should the text appear on screen?
182
            speech: should the speech be enabled?
183
            braille: should the braille be enabled?
184
            mark: the index where to move the cursor.
185

186
        """
187
        if screen:
188
            wx.CallAfter(pub.sendMessage, "message", client=self,
189
                    message=msg, mark=mark)
190

    
191
        # In any case, tries to find the TTS
192
        msg = ANSI_ESCAPE.sub('', msg)
193
        panel = self.factory.panel
194
        if self.factory.engine.TTS_on or force_TTS:
195
            # If outside of the window
196
            tts = False
197
            if force_TTS:
198
                tts = True
199
            elif panel.inside and panel.focus:
200
                tts = True
201
            elif not panel.inside and self.factory.engine.settings[
202
                    "options.TTS.outside"]:
203
                tts = True
204

    
205
            if tts:
206
                interrupt = self.factory.engine.settings[
207
                        "options.TTS.interrupt"]
208
                ScreenReader.talk(msg, speech=speech, braille=braille,
209
                        interrupt=interrupt)
210

    
211
    def write(self, text, alias=True):
212
        """Write text to the client."""
213
        # Break in chunks based on the command stacking, if active
214
        settings = self.factory.engine.settings
215
        stacking = settings["options.input.command_stacking"]
216
        encoding = settings["options.general.encoding"]
217
        if stacking:
218
            delimiter = re.escape(stacking)
219
            re_stacking = u"(?<!{s}){s}(?!{s})".format(s=delimiter)
220
            re_del = re.compile(re_stacking, re.UNICODE)
221
            chunks = re_del.split(text)
222

    
223
            # Reset ;; as ; (or other command stacking character)
224
            def reset_del(match):
225
                return match.group(0)[1:]
226

    
227
            for i, chunk in enumerate(chunks):
228
                chunks[i] = re.sub(delimiter + "{2,}", reset_del, chunk)
229
                if isinstance(chunks[i], unicode):
230
                    chunks[i] = chunks[i].encode(encoding,
231
                            errors="replace")
232
        else:
233
            chunks = [text.encode(encoding, "replace")]
234

    
235
        with self.factory.world.lock:
236
            for text in chunks:
237
                # Test the aliases
238
                if alias:
239
                    for alias in self.factory.world.aliases:
240
                        alias.sharp_engine = self.factory.sharp_engine
241
                        if alias.test(text):
242
                            return
243

    
244
                if not text.endswith("\r\n"):
245
                    text += "\r\n"
246

    
247
                self.transport.write(text)
248

    
249
    def test_macros(self, key, modifiers):
250
        """Test the macros of this world."""
251
        found = False
252
        with self.factory.world.lock:
253
            for macro in self.factory.world.macros:
254
                code = (macro.key, macro.modifiers)
255
                macro.sharp_engine = self.factory.sharp_engine
256
                if code == (key, modifiers):
257
                    macro.execute(self.factory.engine, self)
258
                    found = True
259
                    break
260

    
261
        return found
262

    
263

    
264
class CocoFactory(ReconnectingClientFactory):
265

    
266
    """Factory used by CocoMUD client to generate Telnet clients."""
267

    
268
    def __init__(self, world, panel):
269
        self.world = world
270
        self.panel = panel
271
        self.engine = world.engine
272
        self.sharp_engine = world.sharp_engine
273
        self.commands = []
274
        self.strip_ansi = False
275

    
276
    def buildProtocol(self, addr):
277
        client = Client()
278
        client.factory = self
279
        client.run()
280
        self.panel.client = client
281
        self.sharp_engine.bind_client(client)
282
        return client
(5-5/19)