1import re
2from math import ceil
3import time
4import operator
5
6from gi.repository import GLib, GObject
7
8from pychess.System.Log import log
9from pychess.ic import GAME_TYPES, BLKCMD_ALLOBSERVERS
10
11titles = r"(?:\([A-Z*]+\))*"
12names = "([A-Za-z]+)" + titles
13titlesC = re.compile(titles)
14namesC = re.compile(names)
15ratings = r"\(\s*([0-9\ \-\+]{1,4}[P E]?|UNR)\)"
16
17CHANNEL_SHOUT = "shout"
18CHANNEL_CSHOUT = "cshout"
19
20
21class ChatManager(GObject.GObject):
22
23    __gsignals__ = {
24        'channelMessage': (GObject.SignalFlags.RUN_FIRST, None,
25                           (str, bool, bool, str, str)),
26        'kibitzMessage': (GObject.SignalFlags.RUN_FIRST, None,
27                          (str, int, str)),
28        'whisperMessage': (GObject.SignalFlags.RUN_FIRST, None,
29                           (str, int, str)),
30        'privateMessage': (GObject.SignalFlags.RUN_FIRST, None,
31                           (str, str, bool, str)),
32        'bughouseMessage': (GObject.SignalFlags.RUN_FIRST, None, (str, str)),
33        'announcement': (GObject.SignalFlags.RUN_FIRST, None, (str, )),
34        'arrivalNotification': (GObject.SignalFlags.RUN_FIRST, None,
35                                (object, )),
36        'departedNotification': (GObject.SignalFlags.RUN_FIRST, None,
37                                 (object, )),
38        'channelAdd': (GObject.SignalFlags.RUN_FIRST, None, (str, )),
39        'channelRemove': (GObject.SignalFlags.RUN_FIRST, None, (str, )),
40        'channelJoinError': (GObject.SignalFlags.RUN_FIRST, None, (str, str)),
41        'channelsListed': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
42        'channelLog': (GObject.SignalFlags.RUN_FIRST, None,
43                       (str, int, str, str)),
44        'toldChannel': (GObject.SignalFlags.RUN_FIRST, None, (str, int)),
45        'receivedChannels': (GObject.SignalFlags.RUN_FIRST, None,
46                             (str, object)),
47        'receivedNames': (GObject.SignalFlags.RUN_FIRST, None, (str, object)),
48        'observers_received': (GObject.SignalFlags.RUN_FIRST, None,
49                               (str, str)),
50    }
51
52    def __init__(self, connection):
53        GObject.GObject.__init__(self)
54        self.connection = connection
55
56        self.connection.expect_line(
57            self.onPrivateMessage,
58            r"%s(\*)?(?:\[\d+\])? (?:tells you|says): (.*)" % names)
59        self.connection.expect_line(self.onAnnouncement,
60                                    r"\s*\*\*ANNOUNCEMENT\*\* (.*)")
61        self.connection.expect_line(self.onChannelMessage,
62                                    r"%s(\*)?\((\d+)\): (.*)" % names)
63        self.connection.expect_line(self.onShoutMessage,
64                                    r"%s(\*)? (c-)?shouts: (.*)" % names)
65        self.connection.expect_line(self.onShoutMessage,
66                                    r"--> %s(\*)?:? (.*)" % names)
67        self.connection.expect_line(self.onKibitzMessage,
68                                    r"%s%s\[(\d+)\] kibitzes: (.*)" %
69                                    (names, ratings))
70        self.connection.expect_line(self.onWhisperMessage,
71                                    r"%s%s\[(\d+)\] whispers: (.*)" %
72                                    (names, ratings))
73
74        self.connection.expect_line(self.onArrivalNotification,
75                                    r"Notification: %s has arrived\." % names)
76        self.connection.expect_line(self.onDepartedNotification,
77                                    r"Notification: %s has departed\." % names)
78
79        self.connection.expect_fromto(
80            self.onChannelList, "channels only for their designated topics.",
81            "SPECIAL NOTE")
82
83        self.connection.expect_line(
84            lambda m: GLib.idle_add(self.emit, 'channelAdd', m.groups()[0]),
85            r"\[(\d+)\] added to your channel list.")
86        self.connection.expect_line(
87            lambda m: GLib.idle_add(self.emit, 'channelRemove', m.groups()[0]),
88            r"\[(\d+)\] removed to your channel list.")
89
90        self.connection.expect_line(
91            lambda m: GLib.idle_add(self.emit, 'channelJoinError', *m.groups()),
92            r"Only (.+?) may join channel (\d+)\.")
93
94        self.connection.expect_line(self.getNoChannelPlayers,
95                                    r"Channel (\d+) is empty\.")
96        self.connection.expect_fromto(
97            self.getChannelPlayers, r"Channel (\d+)(?: \"(\w+)\")?: (.+)",
98            r"(\d+) player(?: is|s are) in channel \d+\.")
99
100        self.connection.expect_fromto(self.gotPlayerChannels,
101                                      "%s is in the following channels:" %
102                                      names, r"(?!(?:\d+\s+)+)")
103
104        # self.connection.expect_line (self.toldChannel,
105        #        '\(told (\d+) players in channel (\d+) ".+"\)')
106        # (told Chronatog)
107
108        # Only chess advisers and admins may join channel 63.
109        # Only (.+?) may sey send tells to channel (\d+).
110        # Only admins may send tells to channel 0.
111        # Only registered users may send tells to channels other than 4, 7 and 53.
112
113        self.currentLogChannel = None
114        self.connection.expect_line(
115            self.onChannelLogStart,
116            r":Channel (\d+|shout|c-shout) log for the last \d+ minutes:$")
117        self.connection.expect_line(
118            self.onChannelLogLine,
119            r":\[(\d+):(\d+):(\d+)\] (?:(?:--> )?%s(?: shouts)?)\S* (.+)$" %
120            names)
121        self.connection.expect_line(self.onChannelLogBreak,
122                                    ":Use \"tell chLog Next\" to print more.$")
123
124        # TODO handling of this case is nessesary for console:
125        # fics% tell 1 hi
126        # You are not in channel 1, auto-adding you if possible.
127
128        # Setting 'Lang' is a workaround for
129        # http://code.google.com/p/pychess/issues/detail?id=376
130        # and can be removed when conversion to FICS block mode is done
131        self.connection.client.run_command("set Lang English")
132
133        self.connection.client.run_command("set height 240")
134
135        self.connection.client.run_command("inchannel %s" %
136                                           self.connection.username)
137        self.connection.client.run_command("help channel_list")
138        self.channels = {}
139
140        # Observing 112 [DrStupp vs. hajaK]: pgg (1 user)
141        self.connection.expect_line(
142            self.get_allob_list,
143            r'(?:Observing|Examining)\s+(\d+).+: (.+) \(')
144
145        self.connection.expect_line(self.on_allob_no,
146                                    r"No one is observing game (\d+).")
147
148    def get_allob_list(self, match):
149        """ Description: Processes the returning pattern matched of the FICS allob command
150                         extracts out the gameno and a list of observers before emmiting them for collection
151                         by the observers view
152            match: (re.reg-ex) is the complied matching pattern for processing
153        """
154
155        obs_dic = {}
156        gameno = match.group(1)
157        observers = match.group(2)
158        oblist = observers.split()
159        for player in oblist:
160            if '(U)' not in player:  # deals with unregistered players
161                try:
162                    if '(' in player:  # deals with admin and multi titled players
163                        player, rest = player.split('(', 1)
164                    ficsplayer = self.connection.players.get(player)
165                    obs_dic[player] = ficsplayer.getRatingByGameType(
166                        GAME_TYPES['standard'])
167                except KeyError:
168                    obs_dic[player] = 0
169                    # print("player %s is not in self.connection.players" % player)
170            else:
171                obs_dic[player] = 0
172        obs_sorted = sorted(obs_dic.items(),
173                            key=operator.itemgetter(1),
174                            reverse=True)
175        obs_str = ""
176        for toople in obs_sorted:
177            player, rating = toople
178            if rating == 0:
179                obs_str += "%s " % player  # Don't print ratings for guest accounts
180            else:
181                obs_str += "%s(%s) " % (player, rating)
182        self.emit('observers_received', gameno, obs_str)
183
184    get_allob_list.BLKCMD = BLKCMD_ALLOBSERVERS
185
186    def on_allob_no(self, match):
187        gameno = match.group(1)
188        self.emit('observers_received', gameno, "")
189
190    on_allob_no.BLKCMD = BLKCMD_ALLOBSERVERS
191
192    def getChannels(self):
193        return self.channels
194
195    def getJoinedChannels(self):
196        channels = self.connection.lvm.getList("channel")
197        return channels
198
199    channelListItem = re.compile(r"((?:\d+,?)+)\s*(.*)")
200
201    def onChannelList(self, matchlist):
202        self.channels = [(CHANNEL_SHOUT, _("Shout")),
203                         (CHANNEL_CSHOUT, _("Chess Shout"))]
204        numbers = set(range(256))  # TODO: Use limits->Server->Channels
205        for line in matchlist[1:-1]:
206            match = self.channelListItem.match(line)
207            if not match:
208                continue
209            ids, desc = match.groups()
210            for id in ids.split(","):
211                numbers.remove(int(id))
212                self.channels.append((id, desc))
213        for i in numbers:
214            self.channels.append((str(i), _("Unofficial channel %d" % i)))
215
216        GLib.idle_add(self.emit, 'channelsListed', self.channels)
217
218    def getNoChannelPlayers(self, match):
219        channel = match.groups()[0]
220        GLib.idle_add(self.emit, 'receivedNames', channel, [])
221
222    def getChannelPlayers(self, matchlist):
223        channel, name, people = matchlist[0].groups()
224        people += " " + " ".join(matchlist[1:-1])
225        people = namesC.findall(titlesC.sub("", people))
226        GLib.idle_add(self.emit, 'receivedNames', channel, people)
227
228    def gotPlayerChannels(self, matchlist):
229        list = []
230        for line in matchlist[1:-1]:
231            list += line.split()
232
233    def onPrivateMessage(self, match):
234        name, isadmin, text = match.groups()
235        text = self.entityDecode(text)
236        GLib.idle_add(self.emit, "privateMessage", name, "title", isadmin,
237                      text)
238
239    def onAnnouncement(self, match):
240        text = match.groups()[0]
241        text = self.entityDecode(text)
242        GLib.idle_add(self.emit, "announcement", text)
243
244    def onChannelMessage(self, match):
245        name, isadmin, channel, text = match.groups()
246        text = self.entityDecode(text)
247        isme = name.lower() == self.connection.username.lower()
248        GLib.idle_add(self.emit, "channelMessage", name, isadmin, isme,
249                      channel, text)
250
251    def onShoutMessage(self, match):
252        if len(match.groups()) == 4:
253            name, isadmin, type, text = match.groups()
254        elif len(match.groups()) == 3:
255            name, isadmin, text = match.groups()
256            type = ""
257        text = self.entityDecode(text)
258        isme = name.lower() == self.connection.username.lower()
259        # c-shout should be used ONLY for chess-related messages, such as
260        # questions about chess or announcing being open for certain kinds of
261        # chess matches. Use "shout" for non-chess messages.
262
263        # t-shout is used to invite to tournaments
264        if type == "c-":
265            GLib.idle_add(self.emit, "channelMessage", name, isadmin, isme,
266                          CHANNEL_CSHOUT, text)
267        else:
268            GLib.idle_add(self.emit, "channelMessage", name, isadmin, isme,
269                          CHANNEL_SHOUT, text)
270
271    def onKibitzMessage(self, match):
272        name, rating, gameno, text = match.groups()
273        text = self.entityDecode(text)
274        GLib.idle_add(self.emit, "kibitzMessage", name, int(gameno), text)
275
276    def onWhisperMessage(self, match):
277        name, rating, gameno, text = match.groups()
278        text = self.entityDecode(text)
279        GLib.idle_add(self.emit, "whisperMessage", name, int(gameno), text)
280
281    def onArrivalNotification(self, match):
282        name = match.groups()[0]
283        try:
284            player = self.connection.players.get(name)
285        except KeyError:
286            return
287        if player.name not in self.connection.notify_users:
288            self.connection.notify_users.append(player.name)
289        GLib.idle_add(self.emit, "arrivalNotification", player)
290
291    def onDepartedNotification(self, match):
292        name = match.groups()[0]
293        try:
294            player = self.connection.players.get(name)
295        except KeyError:
296            return
297        GLib.idle_add(self.emit, "departedNotification", player)
298
299    def toldChannel(self, match):
300        amount, channel = match.groups()
301        GLib.idle_add(self.emit, "toldChannel", channel, int(amount))
302
303    def onChannelLogStart(self, match):
304        channel, = match.groups()
305        self.currentLogChannel = channel
306
307    def onChannelLogLine(self, match):
308        if not self.currentLogChannel:
309            log.warning("Received log line before channel was set")
310            return
311        hour, minutes, secs, handle, text = match.groups()
312        conv_time = self.convTime(int(hour), int(minutes), int(secs))
313        text = self.entityDecode(text)
314        GLib.idle_add(self.emit, "channelLog", self.currentLogChannel, conv_time,
315                      handle, text)
316
317    def onChannelLogBreak(self, match):
318        self.connection.client.run_command("xtell chlog Next")
319
320    def convTime(self, h, m, s):
321        # Convert to timestamp
322        t1, t2, t3, t4, t5, t6, t7, t8, t9 = time.localtime()
323        tstamp = time.mktime((t1, t2, t3, h, m, s, 0, 0, 0))
324        # Difference to now in hours
325        dif = (tstamp - time.time()) / 60. / 60.
326        # As we know there is maximum 30 minutes in difference, we can guess when the
327        # message was sent, without knowing the sending time zone
328        return tstamp - ceil(dif) * 60 * 60
329
330    entityExpr = re.compile("&#x([a-f0-9]+);")
331
332    def entityDecode(self, text):
333        return self.entityExpr.sub(lambda m: chr(int(m.groups()[0], 16)),
334                                   text)
335
336    def entityEncode(self, text):
337        buf = []
338        for char in text:
339            if not 32 <= ord(char) <= 127:
340                char = "&#" + hex(ord(char))[1:] + ";"
341            buf.append(char)
342        return "".join(buf)
343
344    def getChannelLog(self, channel, minutes=30):
345        """ Channel can be channel_id, shout or c-shout """
346        assert 1 <= minutes <= 120
347        # Using the chlog bot
348        self.connection.client.run_command("xtell chlog show %s -t %d" %
349                                           (channel, minutes))
350
351    def getPlayersChannels(self, player):
352        self.connection.client.run_command("inchannel %s" % player)
353
354    def getPeopleInChannel(self, channel):
355        if channel in (CHANNEL_SHOUT, CHANNEL_CSHOUT):
356            people = self.connection.players.get_online_playernames()
357            GLib.idle_add(self.emit, 'receivedNames', channel, people)
358        self.connection.client.run_command("inchannel %s" % channel)
359
360    def joinChannel(self, channel):
361        self.connection.client.run_command("+channel %s" % channel)
362
363    def removeChannel(self, channel):
364        self.connection.client.run_command("-channel %s" % channel)
365
366    def mayTellChannel(self, channel):
367        if self.connection.isRegistred() or channel in ("4", "7", "53"):
368            return True
369        return False
370
371    def tellPlayer(self, player, message):
372        message = self.entityEncode(message)
373        self.connection.client.run_command("tell %s %s" % (player, message))
374
375    def tellChannel(self, channel, message):
376        message = self.entityEncode(message)
377        if channel == CHANNEL_SHOUT:
378            self.connection.client.run_command("shout %s" % message)
379        elif channel == CHANNEL_CSHOUT:
380            self.connection.client.run_command("cshout %s" % message)
381        else:
382            self.connection.client.run_command("tell %s %s" %
383                                               (channel, message))
384
385    def tellAll(self, message):
386        message = self.entityEncode(message)
387        self.connection.client.run_command("shout %s" % message)
388
389    def tellGame(self, gameno, message):
390        message = self.entityEncode(message)
391        self.connection.client.run_command("xkibitz %s %s" % (gameno, message))
392
393    def tellOpponent(self, message):
394        message = self.entityEncode(message)
395        self.connection.client.run_command("say %s" % message)
396
397    def tellBughousePartner(self, message):
398        message = self.stripChars(message)
399        self.connection.client.run_command("ptell %s" % message)
400
401    def tellUser(self, player, message):
402        for line in message.strip().split("\n"):
403            self.tellPlayer(player, line)
404
405    def whisper(self, message):
406        message = self.entityEncode(message)
407        self.connection.client.run_command("whisper %s" % message)
408