diff --git a/source/tools/XpartaMuPP/README b/source/tools/XpartaMuPP/README index a471ffceb4..cdc65a71df 100644 --- a/source/tools/XpartaMuPP/README +++ b/source/tools/XpartaMuPP/README @@ -61,7 +61,7 @@ To enable the bot to send the game list to players it needs the JIDs of the play Run XpartaMuPP - XMPP Multiplayer Game Manager ============================================== -You need to have python 3 and SleekXmpp installed +You need to have python 3 and SleekXmpp installed (tested for 1.0-beta5) $ sudo apt-get install python3 python3-sleekxmpp If you would like to run the leaderboard database, diff --git a/source/tools/XpartaMuPP/XpartaMuPP.py b/source/tools/XpartaMuPP/XpartaMuPP.py index b3c43a5645..aff14458ba 100644 --- a/source/tools/XpartaMuPP/XpartaMuPP.py +++ b/source/tools/XpartaMuPP/XpartaMuPP.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""Copyright (C) 2014 Wildfire Games. +"""Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -26,251 +26,6 @@ from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin, ET from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sqlalchemy import func - -from LobbyRanking import session as db, Game, Player, PlayerInfo -from ELO import get_rating_adjustment -# Rating that new players should be inserted into the -# database with, before they've played any games. -leaderboard_default_rating = 1200 - -## Class that contains and manages leaderboard data ## -class LeaderboardList(): - def __init__(self, room): - self.room = room - self.lastRated = "" - - def getProfile(self, JID): - """ - Retrieves the profile for the specified JID - """ - stats = {} - player = db.query(Player).filter(Player.jid.ilike(str(JID))) - if not player.first(): - return - if player.first().rating != -1: - stats['rating'] = str(player.first().rating) - - if player.first().highest_rating != -1: - stats['highestRating'] = str(player.first().highest_rating) - - playerID = player.first().id - players = db.query(Player).filter(Player.rating != -1).order_by(Player.rating.desc()).all() - - for rank, user in enumerate(players): - if (user.jid.lower() == JID.lower()): - stats['rank'] = str(rank+1) - break - - stats['totalGamesPlayed'] = str(db.query(PlayerInfo).filter_by(player_id=playerID).count()) - stats['wins'] = str(db.query(Game).filter_by(winner_id=playerID).count()) - stats['losses'] = str(db.query(PlayerInfo).filter_by(player_id=playerID).count() - db.query(Game).filter_by(winner_id=playerID).count()) - return stats - - def getOrCreatePlayer(self, JID): - """ - Stores a player(JID) in the database if they don't yet exist. - Returns either the newly created instance of - the Player model, or the one that already - exists in the database. - """ - players = db.query(Player).filter_by(jid=str(JID)) - if not players.first(): - player = Player(jid=str(JID), rating=-1) - db.add(player) - db.commit() - return player - return players.first() - - def removePlayer(self, JID): - """ - Remove a player(JID) from database. - Returns the player that was removed, or None - if that player didn't exist. - """ - players = db.query(Player).filter_by(jid=JID) - player = players.first() - if not player: - return None - players.delete() - return player - - def addGame(self, gamereport): - """ - Adds a game to the database and updates the data - on a player(JID) from game results. - Returns the created Game object, or None if - the creation failed for any reason. - Side effects: - Inserts a new Game instance into the database. - """ - # Discard any games still in progress. - if any(map(lambda state: state == 'active', - dict.values(gamereport['playerStates']))): - return None - - players = map(lambda jid: db.query(Player).filter(Player.jid.ilike(str(jid))).first(), - dict.keys(gamereport['playerStates'])) - - winning_jid = list(dict.keys({jid: state for jid, state in - gamereport['playerStates'].items() - if state == 'won'}))[0] - def get(stat, jid): - return gamereport[stat][jid] - - singleStats = {'timeElapsed', 'mapName', 'teamsLocked', 'matchID'} - totalScoreStats = {'economyScore', 'militaryScore', 'totalScore'} - resourceStats = {'foodGathered', 'foodUsed', 'woodGathered', 'woodUsed', - 'stoneGathered', 'stoneUsed', 'metalGathered', 'metalUsed', 'vegetarianFoodGathered', - 'treasuresCollected', 'lootCollected', 'tributesSent', 'tributesReceived'} - unitsStats = {'totalUnitsTrained', 'totalUnitsLost', 'enemytotalUnitsKilled', 'infantryUnitsTrained', - 'infantryUnitsLost', 'enemyInfantryUnitsKilled', 'workerUnitsTrained', 'workerUnitsLost', - 'enemyWorkerUnitsKilled', 'femaleUnitsTrained', 'femaleUnitsLost', 'enemyFemaleUnitsKilled', - 'cavalryUnitsTrained', 'cavalryUnitsLost', 'enemyCavalryUnitsKilled', 'championUnitsTrained', - 'championUnitsLost', 'enemyChampionUnitsKilled', 'heroUnitsTrained', 'heroUnitsLost', - 'enemyHeroUnitsKilled', 'shipUnitsTrained', 'shipUnitsLost', 'enemyShipUnitsKilled', 'traderUnitsTrained', - 'traderUnitsLost', 'enemyTraderUnitsKilled'} - buildingsStats = {'totalBuildingsConstructed', 'totalBuildingsLost', 'enemytotalBuildingsDestroyed', - 'civCentreBuildingsConstructed', 'civCentreBuildingsLost', 'enemyCivCentreBuildingsDestroyed', - 'houseBuildingsConstructed', 'houseBuildingsLost', 'enemyHouseBuildingsDestroyed', - 'economicBuildingsConstructed', 'economicBuildingsLost', 'enemyEconomicBuildingsDestroyed', - 'outpostBuildingsConstructed', 'outpostBuildingsLost', 'enemyOutpostBuildingsDestroyed', - 'militaryBuildingsConstructed', 'militaryBuildingsLost', 'enemyMilitaryBuildingsDestroyed', - 'fortressBuildingsConstructed', 'fortressBuildingsLost', 'enemyFortressBuildingsDestroyed', - 'wonderBuildingsConstructed', 'wonderBuildingsLost', 'enemyWonderBuildingsDestroyed'} - marketStats = {'woodBought', 'foodBought', 'stoneBought', 'metalBought', 'tradeIncome'} - miscStats = {'civs', 'teams', 'percentMapExplored'} - - stats = totalScoreStats | resourceStats | unitsStats | buildingsStats | marketStats | miscStats - playerInfos = [] - for player in players: - jid = player.jid - playerinfo = PlayerInfo(player=player) - for reportname in stats: - setattr(playerinfo, reportname, get(reportname, jid.lower())) - playerInfos.append(playerinfo) - - game = Game(map=gamereport['mapName'], duration=int(gamereport['timeElapsed']), teamsLocked=bool(gamereport['teamsLocked']), matchID=gamereport['matchID']) - game.players.extend(players) - game.player_info.extend(playerInfos) - game.winner = db.query(Player).filter(Player.jid.ilike(str(winning_jid))).first() - db.add(game) - db.commit() - return game - - def verifyGame(self, gamereport): - """ - Returns a boolean based on whether the game should be rated. - Here, we can specify the criteria for rated games. - """ - winning_jids = list(dict.keys({jid: state for jid, state in - gamereport['playerStates'].items() - if state == 'won'})) - # We only support 1v1s right now. TODO: Support team games. - if len(winning_jids) * 2 > len(dict.keys(gamereport['playerStates'])): - # More than half the people have won. This is not a balanced team game or duel. - return False - if len(dict.keys(gamereport['playerStates'])) != 2: - return False - return True - - def rateGame(self, game): - """ - Takes a game with 2 players and alters their ratings - based on the result of the game. - Returns self. - Side effects: - Changes the game's players' ratings in the database. - """ - player1 = game.players[0] - player2 = game.players[1] - # TODO: Support draws. Since it's impossible to draw in the game currently, - # the database model, and therefore this code, requires a winner. - # The Elo implementation does not, however. - result = 1 if player1 == game.winner else -1 - # Player's ratings are -1 unless they have played a rated game. - if player1.rating == -1: - player1.rating = leaderboard_default_rating - if player2.rating == -1: - player2.rating = leaderboard_default_rating - - rating_adjustment1 = int(get_rating_adjustment(player1.rating, player2.rating, - len(player1.games), len(player2.games), result)) - rating_adjustment2 = int(get_rating_adjustment(player2.rating, player1.rating, - len(player2.games), len(player1.games), result * -1)) - if result == 1: - resultQualitative = "won" - elif result == 0: - resultQualitative = "drew" - else: - resultQualitative = "lost" - name1 = '@'.join(player1.jid.split('@')[:-1]) - name2 = '@'.join(player2.jid.split('@')[:-1]) - self.lastRated = "A rated game has ended. %s %s against %s. Rating Adjustment: %s (%s -> %s) and %s (%s -> %s)."%(name1, - resultQualitative, name2, name1, player1.rating, player1.rating + rating_adjustment1, - name2, player2.rating, player2.rating + rating_adjustment2) - player1.rating += rating_adjustment1 - player2.rating += rating_adjustment2 - if not player1.highest_rating: - player1.highest_rating = -1 - if not player2.highest_rating: - player2.highest_rating = -1 - if player1.rating > player1.highest_rating: - player1.highest_rating = player1.rating - if player2.rating > player2.highest_rating: - player2.highest_rating = player2.rating - db.commit() - return self - - def getLastRatedMessage(self): - """ - Gets the string of the last rated game. Triggers an update - chat for the bot. - """ - return self.lastRated - - def addAndRateGame(self, gamereport): - """ - Calls addGame and if the game has only two - players, also calls rateGame. - Returns the result of addGame. - """ - game = self.addGame(gamereport) - if game and self.verifyGame(gamereport): - self.rateGame(game) - else: - self.lastRated = "" - return game - - def getBoard(self): - """ - Returns a dictionary of player rankings to - JIDs for sending. - """ - board = {} - players = db.query(Player).filter(Player.rating != -1).order_by(Player.rating.desc()).limit(100).all() - for rank, player in enumerate(players): - board[player.jid] = {'name': '@'.join(player.jid.split('@')[:-1]), 'rating': str(player.rating)} - return board - def getRatingList(self, nicks): - """ - Returns a rating list of players - currently in the lobby by nick - because the client can't link - JID to nick conveniently. - """ - ratinglist = {} - for JID in list(nicks): - players = db.query(Player.jid, Player.rating).filter(Player.jid.ilike(str(JID))) - if players.first(): - if players.first().rating == -1: - ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': ''} - else: - ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': str(players.first().rating)} - else: - ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': ''} - return ratinglist - ## Class to tracks all games in the lobby ## class GameList(): def __init__(self): @@ -310,101 +65,17 @@ class GameList(): if 'startTime' not in self.gameList[JID]: self.gameList[JID]['startTime'] = str(round(time.time())) -## Class which manages different game reports from clients ## -## and calls leaderboard functions as appropriate. ## -class ReportManager(): - def __init__(self, leaderboard): - self.leaderboard = leaderboard - self.interimReportTracker = [] - self.interimJIDTracker = [] +## Class for custom player stanza extension ## +class PlayerXmppPlugin(ElementBase): + name = 'query' + namespace = 'jabber:iq:player' + interfaces = set(('online')) + sub_interfaces = interfaces + plugin_attrib = 'player' - def addReport(self, JID, rawGameReport): - """ - Adds a game to the interface between a raw report - and the leaderboard database. - """ - # cleanRawGameReport is a copy of rawGameReport with all reporter specific information removed. - cleanRawGameReport = rawGameReport.copy() - del cleanRawGameReport["playerID"] - - if cleanRawGameReport not in self.interimReportTracker: - # Store the game. - appendIndex = len(self.interimReportTracker) - self.interimReportTracker.append(cleanRawGameReport) - # Initilize the JIDs and store the initial JID. - numPlayers = self.getNumPlayers(rawGameReport) - JIDs = [None] * numPlayers - if numPlayers - int(rawGameReport["playerID"]) > -1: - JIDs[int(rawGameReport["playerID"])-1] = str(JID).lower() - self.interimJIDTracker.append(JIDs) - else: - # We get the index at which the JIDs coresponding to the game are stored. - index = self.interimReportTracker.index(cleanRawGameReport) - # We insert the new report JID into the acending list of JIDs for the game. - JIDs = self.interimJIDTracker[index] - if len(JIDs) - int(rawGameReport["playerID"]) > -1: - JIDs[int(rawGameReport["playerID"])-1] = str(JID).lower() - self.interimJIDTracker[index] = JIDs - - self.checkFull() - - def expandReport(self, rawGameReport, JIDs): - """ - Takes an raw game report and re-formats it into - Python data structures leaving JIDs empty. - Returns a processed gameReport of type dict. - """ - processedGameReport = {} - for key in rawGameReport: - if rawGameReport[key].find(",") == -1: - processedGameReport[key] = rawGameReport[key] - else: - split = rawGameReport[key].split(",") - # Remove the false split positive. - split.pop() - statToJID = {} - for i, part in enumerate(split): - statToJID[JIDs[i]] = part - processedGameReport[key] = statToJID - return processedGameReport - - def checkFull(self): - """ - Searches internal database to check if enough - reports have been submitted to add a game to - the leaderboard. If so, the report will be - interpolated and addAndRateGame will be - called with the result. - """ - i = 0 - length = len(self.interimReportTracker) - while(i < length): - numPlayers = self.getNumPlayers(self.interimReportTracker[i]) - numReports = 0 - for JID in self.interimJIDTracker[i]: - if JID != None: - numReports += 1 - if numReports == numPlayers: - self.leaderboard.addAndRateGame(self.expandReport(self.interimReportTracker[i], self.interimJIDTracker[i])) - del self.interimJIDTracker[i] - del self.interimReportTracker[i] - length -= 1 - else: - i += 1 - self.leaderboard.lastRated = "" - - def getNumPlayers(self, rawGameReport): - """ - Computes the number of players in a raw gameReport. - Returns int, the number of players. - """ - # Find a key in the report which holds values for multiple players. - for key in rawGameReport: - if rawGameReport[key].find(",") != -1: - # Count the number of values, minus one for the false split positive. - return len(rawGameReport[key].split(","))-1 - # Return -1 in case of failure. - return -1 + def addPlayerOnline(self, player): + playerXml = ET.fromstring("%s" % player) + self.xml.append(playerXml) ## Class for custom gamelist stanza extension ## class GameListXmppPlugin(ElementBase): @@ -433,12 +104,15 @@ class GameListXmppPlugin(ElementBase): class BoardListXmppPlugin(ElementBase): name = 'query' namespace = 'jabber:iq:boardlist' - interfaces = set(('board', 'command')) + interfaces = set(('board', 'command', 'recipient')) sub_interfaces = interfaces plugin_attrib = 'boardlist' def addCommand(self, command): commandXml = ET.fromstring("%s" % command) self.xml.append(commandXml) + def addRecipient(self, recipient): + recipientXml = ET.fromstring("%s" % recipient) + self.xml.append(recipientXml) def addItem(self, name, rating): itemXml = ET.Element("board", {"name": name, "rating": rating}) self.xml.append(itemXml) @@ -448,9 +122,14 @@ class GameReportXmppPlugin(ElementBase): name = 'report' namespace = 'jabber:iq:gamereport' plugin_attrib = 'gamereport' - interfaces = ('game') + interfaces = ('game', 'sender') sub_interfaces = interfaces - + def addSender(self, sender): + senderXml = ET.fromstring("%s" % sender) + self.xml.append(senderXml) + def addGame(self, gr): + game = ET.fromstring(str(gr)).find('{%s}game' % self.namespace) + self.xml.append(game) def getGame(self): """ Required to parse incoming stanzas with this @@ -466,12 +145,15 @@ class GameReportXmppPlugin(ElementBase): class ProfileXmppPlugin(ElementBase): name = 'query' namespace = 'jabber:iq:profile' - interfaces = set(('profile', 'command')) + interfaces = set(('profile', 'command', 'recipient')) sub_interfaces = interfaces plugin_attrib = 'profile' def addCommand(self, command): commandXml = ET.fromstring("%s" % command) self.xml.append(commandXml) + def addRecipient(self, recipient): + recipientXml = ET.fromstring("%s" % recipient) + self.xml.append(recipientXml) def addItem(self, player, rating, highestRating, rank, totalGamesPlayed, wins, losses): itemXml = ET.Element("profile", {"player": player, "rating": rating, "highestRating": highestRating, "rank" : rank, "totalGamesPlayed" : totalGamesPlayed, "wins" : wins, @@ -483,32 +165,32 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): """ A simple list provider """ - def __init__(self, sjid, password, room, nick): + def __init__(self, sjid, password, room, nick, ratingsbot): sleekxmpp.ClientXMPP.__init__(self, sjid, password) self.sjid = sjid self.room = room self.nick = nick + self.ratingsBotWarned = False - self.ratingListUpdate = False + self.ratingsBot = ratingsbot # Game collection self.gameList = GameList() - # Init leaderboard object - self.leaderboard = LeaderboardList(room) - - # gameReport to leaderboard abstraction - self.reportManager = ReportManager(self.leaderboard) - # Store mapping of nicks and XmppIDs, attached via presence stanza self.nicks = {} self.lastLeft = "" + register_stanza_plugin(Iq, PlayerXmppPlugin) register_stanza_plugin(Iq, GameListXmppPlugin) register_stanza_plugin(Iq, BoardListXmppPlugin) register_stanza_plugin(Iq, GameReportXmppPlugin) register_stanza_plugin(Iq, ProfileXmppPlugin) + self.register_handler(Callback('Iq Player', + StanzaPath('iq/player'), + self.iqhandler, + instream=True)) self.register_handler(Callback('Iq Gamelist', StanzaPath('iq/gamelist'), self.iqhandler, @@ -521,7 +203,6 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): StanzaPath('iq/gamereport'), self.iqhandler, instream=True)) - self.register_handler(Callback('Iq Profile', StanzaPath('iq/profile'), self.iqhandler, @@ -545,7 +226,9 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): """ Process presence stanza from a chat room. """ - self.ratingListUpdate = True + if self.ratingsBot in self.nicks: + self.relayRatingListRequest(self.ratingsBot) + self.relayPlayerOnline(presence['muc']['jid']) if presence['muc']['nick'] != self.nick: # If it doesn't already exist, store player JID mapped to their nick. if str(presence['muc']['jid']) not in self.nicks: @@ -553,12 +236,6 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): # Check the jid isn't already in the lobby. # Send Gamelist to new player. self.sendGameList(presence['muc']['jid']) - # Following two calls make sqlalchemy complain about using objects in the - # incorrect thread. TODO: Figure out how to fix this. - # Send Leaderboard to new player. - #self.sendBoardList(presence['muc']['jid']) - # Register on leaderboard. - #self.leaderboard.getOrCreatePlayer(presence['muc']['jid']) logging.debug("Client '%s' connected with a nick of '%s'." %(presence['muc']['jid'], presence['muc']['nick'])) def muc_offline(self, presence): @@ -577,6 +254,8 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): self.lastLeft = str(presence['muc']['jid']) if str(presence['muc']['jid']) in self.nicks: del self.nicks[str(presence['muc']['jid'])] + if presence['muc']['nick'] == self.ratingsBot: + self.ratingsBotWarned = False def muc_message(self, msg): """ @@ -609,31 +288,32 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): logging.error("Failed to process gamelist request from %s" % iq['from'].bare) elif 'boardlist' in iq.plugins: command = iq['boardlist']['command'] - if command == 'getleaderboard': - try: - self.leaderboard.getOrCreatePlayer(iq['from']) - self.sendBoardList(iq['from']) - except: - traceback.print_exc() - logging.error("Failed to process leaderboardlist request from %s" % iq['from'].bare) - else: - logging.error("Failed to process boardlist request from %s" % iq['from'].bare) + try: + self.relayBoardListRequest(iq['from']) + except: + traceback.print_exc() + logging.error("Failed to process leaderboardlist request from %s" % iq['from'].bare) elif 'profile' in iq.plugins: command = iq['profile']['command'] try: - self.sendProfile(iq['from'], command) + self.relayProfileRequest(iq['from'], command) except: - try: - self.sendProfileNotFound(iq['from'], command) - except: - logging.debug("No record found for %s" % command) + pass else: logging.error("Unknown 'get' type stanza request from %s" % iq['from'].bare) elif iq['type'] == 'result': """ Iq successfully received """ - pass + if 'boardlist' in iq.plugins: + recipient = iq['boardlist']['recipient'] + self.relayBoardList(iq['boardlist'], recipient) + elif 'profile' in iq.plugins: + recipient = iq['profile']['recipient'] + player = iq['profile']['command'] + self.relayProfile(iq['profile'], player, recipient) + else: + pass elif iq['type'] == 'set': if 'gamelist' in iq.plugins: """ @@ -672,20 +352,12 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): Client is reporting end of game statistics """ try: - self.reportManager.addReport(iq['from'], iq['gamereport']['game']) - if self.leaderboard.getLastRatedMessage() != "": - self.send_message(mto=self.room, mbody=self.leaderboard.getLastRatedMessage(), mtype="groupchat", - mnick=self.nick) - self.sendBoardList() - self.sendRatingList() + self.relayGameReport(iq['gamereport'], iq['from']) except: traceback.print_exc() logging.error("Failed to update game statistics for %s" % iq['from'].bare) else: logging.error("Failed to process stanza type '%s' received from %s" % iq['type'], iq['from'].bare) - if self.ratingListUpdate == True: - self.sendRatingList() - self.ratingListUpdate = False def sendGameList(self, to = ""): """ @@ -738,22 +410,129 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): except: logging.error("Failed to send game list") - def sendBoardList(self, to = ""): + def relayBoardListRequest(self, recipient): + """ + Send a boardListRequest to EcheLOn. + """ + to = self.ratingsBot + if to not in self.nicks: + self.warnRatingsBotOffline() + return + stz = BoardListXmppPlugin() + iq = self.Iq() + iq['type'] = 'get' + stz.addCommand('getleaderboard') + stz.addRecipient(recipient) + iq.setPayload(stz) + ## Set additional IQ attributes + iq['to'] = to + ## Try sending the stanza + try: + iq.send(block=False, now=True) + except: + logging.error("Failed to send leaderboard list request") + + def relayRatingListRequest(self, recipient): + """ + Send a ratingListRequest to EcheLOn. + """ + to = self.ratingsBot + if to not in self.nicks: + self.warnRatingsBotOffline() + return + stz = BoardListXmppPlugin() + iq = self.Iq() + iq['type'] = 'get' + stz.addCommand('getratinglist') + iq.setPayload(stz) + ## Set additional IQ attributes + iq['to'] = to + ## Try sending the stanza + try: + iq.send(block=False, now=True) + except: + logging.error("Failed to send rating list request") + + def relayProfileRequest(self, recipient, player): + """ + Send a profileRequest to EcheLOn. + """ + to = self.ratingsBot + if to not in self.nicks: + self.warnRatingsBotOffline() + return + stz = ProfileXmppPlugin() + iq = self.Iq() + iq['type'] = 'get' + stz.addCommand(player) + stz.addRecipient(recipient) + iq.setPayload(stz) + ## Set additional IQ attributes + iq['to'] = to + ## Try sending the stanza + try: + iq.send(block=False, now=True) + except: + logging.error("Failed to send profile request") + + def relayPlayerOnline(self, jid): + """ + Tells EcheLOn that someone comes online. + """ + ## Check recipient exists + to = self.ratingsBot + if to not in self.nicks: + return + stz = PlayerXmppPlugin() + iq = self.Iq() + iq['type'] = 'set' + stz.addPlayerOnline(jid) + iq.setPayload(stz) + ## Set additional IQ attributes + iq['to'] = to + ## Try sending the stanza + try: + iq.send(block=False, now=True) + except: + logging.error("Failed to send player muc online") + + def relayGameReport(self, data, sender): + """ + Relay a game report to EcheLOn. + """ + to = self.ratingsBot + if to not in self.nicks: + self.warnRatingsBotOffline() + return + stz = GameReportXmppPlugin() + stz.addGame(data) + stz.addSender(sender) + iq = self.Iq() + iq['type'] = 'set' + iq.setPayload(stz) + ## Set additional IQ attributes + iq['to'] = to + ## Try sending the stanza + try: + iq.send(block=False, now=True) + except: + logging.error("Failed to send game report request") + + def relayBoardList(self, boardList, to = ""): """ Send the whole leaderboard list. If no target is passed the boardlist is broadcasted to all clients. """ - ## Pull leaderboard data and add it to the stanza - board = self.leaderboard.getBoard() - stz = BoardListXmppPlugin() iq = self.Iq() iq['type'] = 'result' - for i in board: + """for i in board: stz.addItem(board[i]['name'], board[i]['rating']) - stz.addCommand('boardlist') - iq.setPayload(stz) - if to == "": + stz.addCommand('boardlist')""" + iq.setPayload(boardList) + ## Check recipient exists + if to == "": + # Rating List for JID in list(self.nicks): ## Set additional IQ attributes iq['to'] = JID @@ -761,9 +540,9 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): try: iq.send(block=False, now=True) except: - logging.error("Failed to send leaderboard list") + logging.error("Failed to send rating list") else: - ## Check recipient exists + # Leaderboard if str(to) not in self.nicks: logging.error("No player with the XmPP ID '%s' known to send boardlist to" % str(to)) return @@ -775,68 +554,17 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): except: logging.error("Failed to send leaderboard list") - def sendRatingList(self, to = ""): + def relayProfile(self, data, player, to): """ - Send the rating list. - If no target is passed the rating list is broadcasted - to all clients. - """ - ## Pull rating list data and add it to the stanza - ratinglist = self.leaderboard.getRatingList(self.nicks) - stz = BoardListXmppPlugin() - iq = self.Iq() - iq['type'] = 'result' - for i in ratinglist: - stz.addItem(ratinglist[i]['name'], ratinglist[i]['rating']) - stz.addCommand('ratinglist') - iq.setPayload(stz) - if to == "": - for JID in list(self.nicks): - ## Set additional IQ attributes - iq['to'] = JID - ## Try sending the stanza - try: - iq.send(block=False, now=True) - except: - logging.error("Failed to send rating list") - else: - ## Check recipient exists - if str(to) not in self.nicks: - logging.error("No player with the XmPP ID '%s' known to send ratinglist to" % str(to)) - return - ## Set additional IQ attributes - iq['to'] = to - ## Try sending the stanza - try: - iq.send(block=False, now=True) - except: - logging.error("Failed to send rating list") - - def sendProfile(self, to, player): - """ - Send the profile to a specified player. + Send the player profile to a specified target. """ if to == "": - logging.error("Failed to send profile") + logging.error("Failed to send profile, target unspecified") return - online = False; - ## Pull stats and add it to the stanza - for JID in list(self.nicks): - if self.nicks[JID] == player: - stats = self.leaderboard.getProfile(JID) - online = True - break - - if online == False: - stats = self.leaderboard.getProfile(player + "@" + str(to).split('@')[1]) - stz = ProfileXmppPlugin() iq = self.Iq() iq['type'] = 'result' - - stz.addItem(player, stats['rating'], stats['highestRating'], stats['rank'], stats['totalGamesPlayed'], stats['wins'], stats['losses']) - stz.addCommand(player) - iq.setPayload(stz) + iq.setPayload(data) ## Check recipient exists if str(to) not in self.nicks: logging.error("No player with the XmPP ID '%s' known to send profile to" % str(to)) @@ -852,32 +580,13 @@ class XpartaMuPP(sleekxmpp.ClientXMPP): traceback.print_exc() logging.error("Failed to send profile") - def sendProfileNotFound(self, to, player): + def warnRatingsBotOffline(self): """ - Send a profile not-found error to a specified player. + Warns that the ratings bot is offline. """ - stz = ProfileXmppPlugin() - iq = self.Iq() - iq['type'] = 'result' - - filler = str(0) - stz.addItem(player, str(-2), filler, filler, filler, filler, filler) - stz.addCommand(player) - iq.setPayload(stz) - ## Check recipient exists - if str(to) not in self.nicks: - logging.error("No player with the XmPP ID '%s' known to send profile to" % str(to)) - return - - ## Set additional IQ attributes - iq['to'] = to - - ## Try sending the stanza - try: - iq.send(block=False, now=True) - except: - traceback.print_exc() - logging.error("Failed to send profile") + if not self.ratingsBotWarned: + logging.warn("Ratings bot '%s' is offline" % str(self.ratingsBot)) + self.ratingsBotWarned = True ## Main Program ## if __name__ == '__main__': @@ -911,6 +620,9 @@ if __name__ == '__main__': optp.add_option('-r', '--room', help='set muc room to join', action='store', dest='xroom', default="arena") + optp.add_option('-e', '--elo', help='set rating bot username', + action='store', dest='xratingsbot', + default="disabled") opts, args = optp.parse_args() @@ -919,7 +631,7 @@ if __name__ == '__main__': format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') # XpartaMuPP - xmpp = XpartaMuPP(opts.xlogin+'@'+opts.xdomain+'/CC', opts.xpassword, opts.xroom+'@conference.'+opts.xdomain, opts.xnickname) + xmpp = XpartaMuPP(opts.xlogin+'@'+opts.xdomain+'/CC', opts.xpassword, opts.xroom+'@conference.'+opts.xdomain, opts.xnickname, opts.xratingsbot+'@'+opts.xdomain+'/CC') xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0045') # Multi-User Chat # used