Project

Profile

Help

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

github / src / client.py @ 05b173a7

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

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

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

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

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

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

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

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

37
"""
38

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

    
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
        host = self.transport.getPeer().host
73
        port = self.transport.getPeer().port
74
        log = logger("client")
75
        log.info("Connected to {host}:{port}".format(
76
                host=host, port=port))
77
        self.factory.resetDelay()
78
        for command in self.factory.commands:
79
            self.transport.write(command.encode() + b"\r\n")
80

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
229
            for i, chunk in enumerate(chunks):
230
                chunks[i] = re.sub(delimiter + "{2,}", reset_del, chunk)
231

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

    
241
                if not text.endswith("\r\n"):
242
                    text += "\r\n"
243

    
244
                self.transport.write(text.encode(encoding, errors="replace"))
245

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

    
258
        return found
259

    
260

    
261
class CocoFactory(ReconnectingClientFactory):
262

    
263
    """Factory used by CocoMUD client to generate Telnet clients."""
264

    
265
    def __init__(self, world, panel):
266
        self.world = world
267
        self.panel = panel
268
        self.engine = world.engine
269
        self.sharp_engine = world.sharp_engine
270
        self.commands = []
271
        self.strip_ansi = False
272

    
273
    def buildProtocol(self, addr):
274
        client = Client()
275
        client.factory = self
276
        client.run()
277
        self.panel.client = client
278
        self.sharp_engine.bind_client(client)
279
        return client
(5-5/20)