Project

Profile

Help

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

github / src / client.py @ 96664dff

1 2e7e9634 Vincent Le Goff
# Copyright (c) 2016, LE GOFF Vincent
2 4102863f Vincent Le Goff
# Copyright (c) 2016, LE GOFF Vincent
3 2e7e9634 Vincent Le Goff
# 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 e1d2d0dc Vincent Le Goff

32 e9cc2ac8 Vincent Le Goff
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 e1d2d0dc Vincent Le Goff

36
"""
37
38 4d2b7a46 Vincent Le Goff
import os
39 e1d2d0dc Vincent Le Goff
import re
40 c377606f Vincent Le Goff
import socket
41 de33964a Vincent Le Goff
from telnetlib import Telnet, WONT, WILL, ECHO, NOP, AYT, IAC
42 e1d2d0dc Vincent Le Goff
import threading
43
import time
44
45 e9cc2ac8 Vincent Le Goff
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 d0fd91d5 Vincent Le Goff
from log import logger
52 59b194ab Vincent Le Goff
from screenreader import ScreenReader
53 e9cc2ac8 Vincent Le Goff
from screenreader import ScreenReader
54 fa348e33 Vincent Le Goff
55 e1d2d0dc Vincent Le Goff
# Constants
56
ANSI_ESCAPE = re.compile(r'\x1b[^m]*m')
57
58 e9cc2ac8 Vincent Le Goff
class Client(Telnet):
59 e1d2d0dc Vincent Le Goff
60 e9cc2ac8 Vincent Le Goff
    """Class to receive data from the MUD using a Telnet protocol."""
61 e1d2d0dc Vincent Le Goff
62 a5b9bff4 Vincent Le Goff
    def disconnect(self):
63
        """Disconnect, close the client."""
64 e9cc2ac8 Vincent Le Goff
        if self.transport:
65
            self.transport.loseConnection()
66 a5b9bff4 Vincent Le Goff
67
        self.running = False
68
69 e9cc2ac8 Vincent Le Goff
    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 e1e0950a Vincent Le Goff
        wx.CallAfter(pub.sendMessage, "disconnect", client=self,
88
                reason=reason)
89 e9cc2ac8 Vincent Le Goff
        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 de33964a Vincent Le Goff
99 e1d2d0dc Vincent Le Goff
    def run(self):
100
        """Run the thread."""
101 6e131b55 Vincent Le Goff
        # Try to connect to the specified host and port
102 5e6fddad Vincent Le Goff
        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 e9cc2ac8 Vincent Le Goff
        log = logger("client")
107 5e6fddad Vincent Le Goff
        log.info("Connecting {protocol} client for {host}:{port}".format(
108
                protocol=protocol, host=host, port=port))
109 6e131b55 Vincent Le Goff
        self.running = True
110 058529ee Vincent Le Goff
111 c8dee3b4 Vincent Le Goff
    def handle_lines(self, msg):
112
        """Handle multiple lines of text."""
113 0fd19395 Vincent Le Goff
        mark = None
114 c8dee3b4 Vincent Le Goff
        lines = []
115 875753b1 Vincent Le Goff
        no_ansi_lines = []
116 c8dee3b4 Vincent Le Goff
        triggers = []
117 060e5a35 Vincent Le Goff
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 c8dee3b4 Vincent Le Goff
        for line in msg.splitlines():
125 6824bc76 Vincent Le Goff
            no_ansi_line = ANSI_ESCAPE.sub('', line)
126 c8dee3b4 Vincent Le Goff
            display = True
127 e9cc2ac8 Vincent Le Goff
            for trigger in self.factory.world.triggers:
128
                trigger.sharp_engine = self.factory.sharp_engine
129 c8dee3b4 Vincent Le Goff
                try:
130 0873e347 Vincent Le Goff
                    match = trigger.test(no_ansi_line)
131 c8dee3b4 Vincent Le Goff
                except Exception:
132
                    log = logger("client")
133
                    log.exception("The trigger {} failed".format(
134
                            repr(trigger.readction)))
135
                else:
136 0873e347 Vincent Le Goff
                    if match:
137
                        triggers.append((trigger, match, no_ansi_line))
138 c8dee3b4 Vincent Le Goff
                        if trigger.mute:
139
                            display = False
140 0fd19395 Vincent Le Goff
                        if trigger.mark and mark is None:
141 060e5a35 Vincent Le Goff
                            before = nl.join([l for l in no_ansi_lines])
142
                            mark = len(before) + len(nl)
143 c8dee3b4 Vincent Le Goff
144 0873e347 Vincent Le Goff
                        # 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 c8dee3b4 Vincent Le Goff
            if display:
152 e9cc2ac8 Vincent Le Goff
                if self.factory.strip_ansi:
153 f2eac2d3 Vincent Le Goff
                    lines.append(no_ansi_line)
154
                else:
155
                    lines.append(line)
156
157 875753b1 Vincent Le Goff
                if no_ansi_line.strip():
158
                    no_ansi_lines.append(no_ansi_line)
159 c8dee3b4 Vincent Le Goff
160
        # Handle the remaining text
161
        try:
162 f2eac2d3 Vincent Le Goff
            liens = [l for l in lines if l]
163 0fd19395 Vincent Le Goff
            self.handle_message("\r\n".join(lines), mark=mark)
164 c8dee3b4 Vincent Le Goff
        except Exception:
165
            log = logger("client")
166
            log.exception(
167
                    "An error occurred when handling a message")
168
169
        # Execute the triggers
170 0873e347 Vincent Le Goff
        for trigger, match, line in triggers:
171
            trigger.set_variables(match)
172
            trigger.execute()
173 c8dee3b4 Vincent Le Goff
174 a4ca19ed Vincent Le Goff
    def handle_message(self, msg, force_TTS=False, screen=True,
175 0fd19395 Vincent Le Goff
            speech=True, braille=True, mark=None):
176 11d4f23d Vincent Le Goff
        """When the client receives a message.
177

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

186
        """
187 e9cc2ac8 Vincent Le Goff
        if screen:
188 e1e0950a Vincent Le Goff
            wx.CallAfter(pub.sendMessage, "message", client=self,
189
                    message=msg, mark=mark)
190 e9cc2ac8 Vincent Le Goff
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 e1d2d0dc Vincent Le Goff
211 22c2e2e8 Vincent Le Goff
    def write(self, text, alias=True):
212 fa348e33 Vincent Le Goff
        """Write text to the client."""
213 e9cc2ac8 Vincent Le Goff
        # 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 96664dff Vincent Le Goff
            re_stacking = u"(?<!{s}){s}(?!{s})".format(s=delimiter)
220 4102863f Vincent Le Goff
            re_del = re.compile(re_stacking, re.UNICODE)
221 e9cc2ac8 Vincent Le Goff
            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 fa348e33 Vincent Le Goff
        else:
233 e9cc2ac8 Vincent Le Goff
            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 4102863f Vincent Le Goff
                if not text.endswith("\r\n"):
245
                    text += "\r\n"
246 e9cc2ac8 Vincent Le Goff
247 4102863f Vincent Le Goff
                self.transport.write(text)
248 fa348e33 Vincent Le Goff
249 28046bf5 Vincent Le Goff
    def test_macros(self, key, modifiers):
250
        """Test the macros of this world."""
251
        found = False
252 e9cc2ac8 Vincent Le Goff
        with self.factory.world.lock:
253
            for macro in self.factory.world.macros:
254 28046bf5 Vincent Le Goff
                code = (macro.key, macro.modifiers)
255 e9cc2ac8 Vincent Le Goff
                macro.sharp_engine = self.factory.sharp_engine
256 28046bf5 Vincent Le Goff
                if code == (key, modifiers):
257 e9cc2ac8 Vincent Le Goff
                    macro.execute(self.factory.engine, self)
258 28046bf5 Vincent Le Goff
                    found = True
259
                    break
260
261
        return found
262
263
264 e9cc2ac8 Vincent Le Goff
class CocoFactory(ReconnectingClientFactory):
265 e1d2d0dc Vincent Le Goff
266 e9cc2ac8 Vincent Le Goff
    """Factory used by CocoMUD client to generate Telnet clients."""
267 e1d2d0dc Vincent Le Goff
268 e9cc2ac8 Vincent Le Goff
    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 e1d2d0dc Vincent Le Goff
276 e9cc2ac8 Vincent Le Goff
    def buildProtocol(self, addr):
277
        client = Client()
278
        client.factory = self
279 5e6fddad Vincent Le Goff
        client.run()
280 e9cc2ac8 Vincent Le Goff
        self.panel.client = client
281 ded462db Vincent Le Goff
        self.sharp_engine.bind_client(client)
282 e9cc2ac8 Vincent Le Goff
        return client