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