Changeset View
Changeset View
Standalone View
Standalone View
source/tools/lobbybots/XpartaMuPP/XpartaMuPP.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# -*- coding: utf-8 -*- | # Copyright (C) 2021 Wildfire Games. | ||||
"""Copyright (C) 2018 Wildfire Games. | # This file is part of 0 A.D. | ||||
* This file is part of 0 A.D. | # | ||||
* | # 0 A.D. is free software: you can redistribute it and/or modify | ||||
* 0 A.D. is free software: you can redistribute it and/or modify | # it under the terms of the GNU General Public License as published by | ||||
* it under the terms of the GNU General Public License as published by | # the Free Software Foundation, either version 2 of the License, or | ||||
* the Free Software Foundation, either version 2 of the License, or | # (at your option) any later version. | ||||
* (at your option) any later version. | # | ||||
* | # 0 A.D. is distributed in the hope that it will be useful, | ||||
* 0 A.D. is distributed in the hope that it will be useful, | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | # GNU General Public License for more details. | ||||
* GNU General Public License for more details. | # | ||||
* | # You should have received a copy of the GNU General Public License | ||||
* You should have received a copy of the GNU General Public License | # along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. | ||||
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. | |||||
""" | """0ad XMPP-bot responsible for managing game listings.""" | ||||
import logging, time, traceback | import argparse | ||||
from optparse import OptionParser | import logging | ||||
import time | |||||
import sys | |||||
import sleekxmpp | import sleekxmpp | ||||
from sleekxmpp.stanza import Iq | from sleekxmpp.stanza import Iq | ||||
from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin, ET | |||||
from sleekxmpp.xmlstream.handler import Callback | from sleekxmpp.xmlstream.handler import Callback | ||||
from sleekxmpp.xmlstream.matcher import StanzaPath | from sleekxmpp.xmlstream.matcher import StanzaPath | ||||
from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin | |||||
from xpartamupp.stanzas import GameListXmppPlugin | |||||
from xpartamupp.utils import LimitedSizeDict | |||||
class Games(object): | |||||
"""Class to tracks all games in the lobby.""" | |||||
## Class to tracks all games in the lobby ## | |||||
class GameList(): | |||||
def __init__(self): | def __init__(self): | ||||
self.gameList = {} | """Initialize with empty games.""" | ||||
def addGame(self, JID, data): | self.games = LimitedSizeDict(size_limit=2**7) | ||||
""" | |||||
Add a game | def add_game(self, jid, data): | ||||
"""Add a game. | |||||
Arguments: | |||||
jid (sleekxmpp.jid.JID): JID of the player who started the | |||||
game | |||||
data (dict): information about the game | |||||
Returns: | |||||
True if adding the game succeeded, False if not | |||||
""" | """ | ||||
try: | |||||
data['players-init'] = data['players'] | data['players-init'] = data['players'] | ||||
data['nbp-init'] = data['nbp'] | data['nbp-init'] = data['nbp'] | ||||
data['state'] = 'init' | data['state'] = 'init' | ||||
self.gameList[str(JID)] = data | except (KeyError, TypeError, ValueError): | ||||
def removeGame(self, JID): | logging.warning("Received invalid data for add game from 0ad: %s", data) | ||||
""" | return False | ||||
Remove a game attached to a JID | else: | ||||
""" | self.games[jid] = data | ||||
del self.gameList[str(JID)] | return True | ||||
def getAllGames(self): | |||||
""" | def remove_game(self, jid): | ||||
Returns all games | """Remove a game attached to a JID. | ||||
Arguments: | |||||
jid (sleekxmpp.jid.JID): JID of the player whose game to | |||||
remove. | |||||
Returns: | |||||
True if removing the game succeeded, False if not | |||||
""" | """ | ||||
return self.gameList | try: | ||||
def changeGameState(self, JID, data): | del self.games[jid] | ||||
except KeyError: | |||||
logging.warning("Game for jid %s didn't exist", jid) | |||||
return False | |||||
else: | |||||
return True | |||||
def get_all_games(self): | |||||
"""Return all games. | |||||
Returns: | |||||
dict containing all games with the JID of the player who | |||||
started the game as key. | |||||
""" | """ | ||||
Switch game state between running and waiting | return self.games | ||||
def change_game_state(self, jid, data): | |||||
"""Switch game state between running and waiting. | |||||
Arguments: | |||||
jid (sleekxmpp.jid.JID): JID of the player whose game to | |||||
change | |||||
data (dict): information about the game | |||||
Returns: | |||||
True if changing the game state succeeded, False if not | |||||
""" | """ | ||||
JID = str(JID) | if jid not in self.games: | ||||
if JID in self.gameList: | logging.warning("Tried to change state for non-existent game %s", jid) | ||||
if self.gameList[JID]['nbp-init'] > data['nbp']: | return False | ||||
logging.debug("change game (%s) state from %s to %s", JID, self.gameList[JID]['state'], 'waiting') | |||||
self.gameList[JID]['state'] = 'waiting' | try: | ||||
if self.games[jid]['nbp-init'] > data['nbp']: | |||||
logging.debug("change game (%s) state from %s to %s", jid, | |||||
self.games[jid]['state'], 'waiting') | |||||
self.games[jid]['state'] = 'waiting' | |||||
else: | else: | ||||
logging.debug("change game (%s) state from %s to %s", JID, self.gameList[JID]['state'], 'running') | logging.debug("change game (%s) state from %s to %s", jid, | ||||
self.gameList[JID]['state'] = 'running' | self.games[jid]['state'], 'running') | ||||
self.gameList[JID]['nbp'] = data['nbp'] | self.games[jid]['state'] = 'running' | ||||
self.gameList[JID]['players'] = data['players'] | self.games[jid]['nbp'] = data['nbp'] | ||||
if 'startTime' not in self.gameList[JID]: | self.games[jid]['players'] = data['players'] | ||||
self.gameList[JID]['startTime'] = str(round(time.time())) | except (KeyError, ValueError): | ||||
logging.warning("Received invalid data for change game state from 0ad: %s", data) | |||||
## Class for custom player stanza extension ## | return False | ||||
class PlayerXmppPlugin(ElementBase): | else: | ||||
name = 'query' | if 'startTime' not in self.games[jid]: | ||||
namespace = 'jabber:iq:player' | self.games[jid]['startTime'] = str(round(time.time())) | ||||
interfaces = set(('online')) | return True | ||||
sub_interfaces = interfaces | |||||
plugin_attrib = 'player' | |||||
def addPlayerOnline(self, player): | |||||
playerXml = ET.fromstring("<online>%s</online>" % player) | |||||
self.xml.append(playerXml) | |||||
## Class for custom gamelist stanza extension ## | |||||
class GameListXmppPlugin(ElementBase): | |||||
name = 'query' | |||||
namespace = 'jabber:iq:gamelist' | |||||
interfaces = set(('game', 'command')) | |||||
sub_interfaces = interfaces | |||||
plugin_attrib = 'gamelist' | |||||
def addGame(self, data): | |||||
itemXml = ET.Element("game", data) | |||||
self.xml.append(itemXml) | |||||
def getGame(self): | |||||
""" | |||||
Required to parse incoming stanzas with this | |||||
extension. | |||||
""" | |||||
game = self.xml.find('{%s}game' % self.namespace) | |||||
data = {} | |||||
for key, item in game.items(): | |||||
data[key] = item | |||||
return data | |||||
## Class for custom boardlist and ratinglist stanza extension ## | |||||
class BoardListXmppPlugin(ElementBase): | |||||
name = 'query' | |||||
namespace = 'jabber:iq:boardlist' | |||||
interfaces = set(('board', 'command', 'recipient')) | |||||
sub_interfaces = interfaces | |||||
plugin_attrib = 'boardlist' | |||||
def addCommand(self, command): | |||||
commandXml = ET.fromstring("<command>%s</command>" % command) | |||||
self.xml.append(commandXml) | |||||
def addRecipient(self, recipient): | |||||
recipientXml = ET.fromstring("<recipient>%s</recipient>" % recipient) | |||||
self.xml.append(recipientXml) | |||||
def addItem(self, name, rating): | |||||
itemXml = ET.Element("board", {"name": name, "rating": rating}) | |||||
self.xml.append(itemXml) | |||||
## Class for custom gamereport stanza extension ## | |||||
class GameReportXmppPlugin(ElementBase): | |||||
name = 'report' | |||||
namespace = 'jabber:iq:gamereport' | |||||
plugin_attrib = 'gamereport' | |||||
interfaces = ('game', 'sender') | |||||
sub_interfaces = interfaces | |||||
def addSender(self, sender): | |||||
senderXml = ET.fromstring("<sender>%s</sender>" % 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 | |||||
extension. | |||||
""" | |||||
game = self.xml.find('{%s}game' % self.namespace) | |||||
data = {} | |||||
for key, item in game.items(): | |||||
data[key] = item | |||||
return data | |||||
## Class for custom profile ## | |||||
class ProfileXmppPlugin(ElementBase): | |||||
name = 'query' | |||||
namespace = 'jabber:iq:profile' | |||||
interfaces = set(('profile', 'command', 'recipient')) | |||||
sub_interfaces = interfaces | |||||
plugin_attrib = 'profile' | |||||
def addCommand(self, command): | |||||
commandXml = ET.fromstring("<command>%s</command>" % command) | |||||
self.xml.append(commandXml) | |||||
def addRecipient(self, recipient): | |||||
recipientXml = ET.fromstring("<recipient>%s</recipient>" % 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, | |||||
"losses" : losses}) | |||||
self.xml.append(itemXml) | |||||
## Main class which handles IQ data and sends new data ## | |||||
class XpartaMuPP(sleekxmpp.ClientXMPP): | class XpartaMuPP(sleekxmpp.ClientXMPP): | ||||
"""Main class which handles IQ data and sends new data.""" | |||||
bb: 2**7=128, that might be a bit few... Also should add some overflow_callback | |||||
Not Done Inline Actionsthis comment seems wrong, since we don't do it here bb: this comment seems wrong, since we don't do it here | |||||
def __init__(self, sjid, password, room, nick): | |||||
"""Initialize XpartaMuPP. | |||||
Arguments: | |||||
sjid (sleekxmpp.jid.JID): JID to use for authentication | |||||
password (str): password to use for authentication | |||||
room (str): XMPP MUC room to join | |||||
nick (str): Nick to use in MUC | |||||
""" | """ | ||||
A simple list provider | |||||
""" | |||||
def __init__(self, sjid, password, room, nick, ratingsbot): | |||||
sleekxmpp.ClientXMPP.__init__(self, sjid, password) | sleekxmpp.ClientXMPP.__init__(self, sjid, password) | ||||
self.sjid = sjid | self.whitespace_keepalive = False | ||||
self.room = room | self.room = room | ||||
self.nick = nick | self.nick = nick | ||||
self.ratingsBotWarned = False | |||||
self.ratingsBot = ratingsbot | self.games = Games() | ||||
# Game collection | |||||
self.gameList = GameList() | |||||
# Store mapping of nicks and XmppIDs, attached via presence stanza | register_stanza_plugin(Iq, GameListXmppPlugin) | ||||
self.nicks = {} | |||||
self.presences = {} # Obselete when XEP-0060 is implemented. | |||||
self.lastLeft = "" | self.register_handler(Callback('Iq Gamelist', StanzaPath('iq@type=set/gamelist'), | ||||
self._iq_game_list_handler)) | |||||
register_stanza_plugin(Iq, PlayerXmppPlugin) | self.add_event_handler('session_start', self._session_start) | ||||
register_stanza_plugin(Iq, GameListXmppPlugin) | self.add_event_handler('muc::%s::got_online' % self.room, self._muc_online) | ||||
Not Done Inline Actionssame bb: same | |||||
register_stanza_plugin(Iq, BoardListXmppPlugin) | self.add_event_handler('muc::%s::got_offline' % self.room, self._muc_offline) | ||||
register_stanza_plugin(Iq, GameReportXmppPlugin) | self.add_event_handler('groupchat_message', self._muc_message) | ||||
register_stanza_plugin(Iq, ProfileXmppPlugin) | |||||
def _session_start(self, event): # pylint: disable=unused-argument | |||||
self.register_handler(Callback('Iq Player', | """Join MUC channel and announce presence. | ||||
StanzaPath('iq/player'), | |||||
self.iqhandler, | Arguments: | ||||
instream=True)) | event (dict): empty dummy dict | ||||
self.register_handler(Callback('Iq Gamelist', | |||||
StanzaPath('iq/gamelist'), | |||||
self.iqhandler, | |||||
instream=True)) | |||||
self.register_handler(Callback('Iq Boardlist', | |||||
StanzaPath('iq/boardlist'), | |||||
self.iqhandler, | |||||
instream=True)) | |||||
self.register_handler(Callback('Iq GameReport', | |||||
StanzaPath('iq/gamereport'), | |||||
self.iqhandler, | |||||
instream=True)) | |||||
self.register_handler(Callback('Iq Profile', | |||||
StanzaPath('iq/profile'), | |||||
self.iqhandler, | |||||
instream=True)) | |||||
self.add_event_handler("session_start", self.start) | |||||
self.add_event_handler("muc::%s::got_online" % self.room, self.muc_online) | |||||
self.add_event_handler("muc::%s::got_offline" % self.room, self.muc_offline) | |||||
self.add_event_handler("groupchat_message", self.muc_message) | |||||
self.add_event_handler("changed_status", self.presence_change) | |||||
def start(self, event): | |||||
""" | |||||
Process the session_start event | |||||
""" | """ | ||||
self.plugin['xep_0045'].joinMUC(self.room, self.nick) | self.plugin['xep_0045'].joinMUC(self.room, self.nick) | ||||
self.send_presence() | self.send_presence() | ||||
self.get_roster() | self.get_roster() | ||||
logging.info("XpartaMuPP started") | logging.info("XpartaMuPP started") | ||||
def muc_online(self, presence): | def _muc_online(self, presence): | ||||
""" | """Add joining players to the list of players. | ||||
Process presence stanza from a chat room. | |||||
Also send a list of games to them, so they see which games | |||||
are currently there. | |||||
Arguments: | |||||
presence (sleekxmpp.stanza.presence.Presence): Received | |||||
presence stanza. | |||||
""" | """ | ||||
if self.ratingsBot in self.nicks: | nick = str(presence['muc']['nick']) | ||||
self.relayRatingListRequest(self.ratingsBot) | jid = sleekxmpp.jid.JID(presence['muc']['jid']) | ||||
self.relayPlayerOnline(presence['muc']['jid']) | |||||
if presence['muc']['nick'] != self.nick: | if nick == self.nick: | ||||
# If it doesn't already exist, store player JID mapped to their nick. | return | ||||
if str(presence['muc']['jid']) not in self.nicks: | |||||
self.nicks[str(presence['muc']['jid'])] = presence['muc']['nick'] | if jid.resource not in ['0ad', 'CC']: | ||||
self.presences[str(presence['muc']['jid'])] = "available" | return | ||||
Done Inline Actionsspace after you're user1: space after `you're` | |||||
# Check the jid isn't already in the lobby. | |||||
Done Inline Actionsspace after I'm user1: space after `I'm` | |||||
# Send Gamelist to new player. | self._send_game_list(jid) | ||||
self.sendGameList(presence['muc']['jid']) | |||||
logging.debug("Client '%s' connected with a nick of '%s'." %(presence['muc']['jid'], presence['muc']['nick'])) | logging.debug("Client '%s' connected with a nick '%s'.", jid, nick) | ||||
def muc_offline(self, presence): | def _muc_offline(self, presence): | ||||
""" | """Remove leaving players from the list of players. | ||||
Process presence stanza from a chat room. | |||||
""" | Also remove the potential game this player was hosting, so we | ||||
# Clean up after a player leaves | don't end up with stale games. | ||||
if presence['muc']['nick'] != self.nick: | |||||
# Delete any games they were hosting. | Arguments: | ||||
for JID in self.gameList.getAllGames(): | presence (sleekxmpp.stanza.presence.Presence): Received | ||||
if JID == str(presence['muc']['jid']): | presence stanza. | ||||
self.gameList.removeGame(JID) | |||||
self.sendGameList() | |||||
break | |||||
# Remove them from the local player list. | |||||
self.lastLeft = str(presence['muc']['jid']) | |||||
if str(presence['muc']['jid']) in self.nicks: | |||||
del self.nicks[str(presence['muc']['jid'])] | |||||
del self.presences[str(presence['muc']['jid'])] | |||||
if presence['muc']['nick'] == self.ratingsBot: | |||||
self.ratingsBotWarned = False | |||||
def muc_message(self, msg): | |||||
""" | """ | ||||
Process new messages from the chatroom. | nick = str(presence['muc']['nick']) | ||||
jid = sleekxmpp.jid.JID(presence['muc']['jid']) | |||||
if nick == self.nick: | |||||
return | |||||
if self.games.remove_game(jid): | |||||
self._send_game_list() | |||||
logging.debug("Client '%s' with nick '%s' disconnected", jid, nick) | |||||
def _muc_message(self, msg): | |||||
"""Process messages in the MUC room. | |||||
Respond to messages highlighting the bots name with an | |||||
informative message. | |||||
Arguments: | |||||
msg (sleekxmpp.stanza.message.Message): Received MUC | |||||
message | |||||
""" | """ | ||||
if msg['mucnick'] != self.nick and self.nick.lower() in msg['body'].lower(): | if msg['mucnick'] != self.nick and self.nick.lower() in msg['body'].lower(): | ||||
self.send_message(mto=msg['from'].bare, | self.send_message(mto=msg['from'].bare, | ||||
mbody="I am the administrative bot in this lobby and cannot participate in any games.", | mbody="I am just a bot and I'm responsible to ensure that your're" | ||||
"able to see the list of games in here. Aside from that I'm" | |||||
"just chilling.", | |||||
mtype='groupchat') | mtype='groupchat') | ||||
def presence_change(self, presence): | def _iq_game_list_handler(self, iq): | ||||
""" | """Handle game state change requests. | ||||
Processes presence change | |||||
""" | |||||
prefix = "%s/" % self.room | |||||
nick = str(presence['from']).replace(prefix, "") | |||||
for JID in self.nicks: | |||||
if self.nicks[JID] == nick: | |||||
if self.presences[JID] == 'dnd' and (str(presence['type']) == "available" or str(presence['type']) == "away"): | |||||
self.sendGameList(JID) | |||||
self.relayBoardListRequest(JID) | |||||
self.presences[JID] = str(presence['type']) | |||||
break | |||||
Arguments: | |||||
iq (sleekxmpp.stanza.iq.IQ): Received IQ stanza | |||||
def iqhandler(self, iq): | |||||
""" | |||||
Handle the custom stanzas | |||||
This method should be very robust because we could receive anything | |||||
""" | |||||
if iq['type'] == 'error': | |||||
logging.error('iqhandler error' + iq['error']['condition']) | |||||
#self.disconnect() | |||||
elif iq['type'] == 'get': | |||||
""" | |||||
Request lists. | |||||
""" | |||||
# Send lists/register on leaderboard; depreciated once muc_online | |||||
# can send lists/register automatically on joining the room. | |||||
if 'boardlist' in iq.loaded_plugins: | |||||
command = iq['boardlist']['command'] | |||||
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.loaded_plugins: | |||||
command = iq['profile']['command'] | |||||
try: | |||||
self.relayProfileRequest(iq['from'], command) | |||||
except: | |||||
pass | |||||
else: | |||||
logging.error("Unknown 'get' type stanza request from %s" % iq['from'].bare) | |||||
elif iq['type'] == 'result': | |||||
""" | |||||
Iq successfully received | |||||
""" | |||||
if 'boardlist' in iq.loaded_plugins: | |||||
recipient = iq['boardlist']['recipient'] | |||||
self.relayBoardList(iq['boardlist'], recipient) | |||||
elif 'profile' in iq.loaded_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.loaded_plugins: | |||||
""" | |||||
Register-update / unregister a game | |||||
""" | """ | ||||
if iq['from'].resource != '0ad': | |||||
return | |||||
command = iq['gamelist']['command'] | command = iq['gamelist']['command'] | ||||
if command == 'register': | if command == 'register': | ||||
# Add game | success = self.games.add_game(iq['from'], iq['gamelist']['game']) | ||||
try: | |||||
if iq['from'] in self.nicks: | |||||
self.gameList.addGame(iq['from'], iq['gamelist']['game']) | |||||
self.sendGameList() | |||||
except: | |||||
traceback.print_exc() | |||||
logging.error("Failed to process game registration data") | |||||
elif command == 'unregister': | elif command == 'unregister': | ||||
# Remove game | success = self.games.remove_game(iq['from']) | ||||
try: | |||||
self.gameList.removeGame(iq['from']) | |||||
self.sendGameList() | |||||
except: | |||||
traceback.print_exc() | |||||
logging.error("Failed to process game unregistration data") | |||||
elif command == 'changestate': | elif command == 'changestate': | ||||
# Change game status (waiting/running) | success = self.games.change_game_state(iq['from'], iq['gamelist']['game']) | ||||
try: | |||||
self.gameList.changeGameState(iq['from'], iq['gamelist']['game']) | |||||
self.sendGameList() | |||||
except: | |||||
traceback.print_exc() | |||||
logging.error("Failed to process changestate data. Trying to add game") | |||||
try: | |||||
if iq['from'] in self.nicks: | |||||
self.gameList.addGame(iq['from'], iq['gamelist']['game']) | |||||
self.sendGameList() | |||||
except: | |||||
pass | |||||
else: | |||||
logging.error("Failed to process command '%s' received from %s" % command, iq['from'].bare) | |||||
elif 'gamereport' in iq.loaded_plugins: | |||||
""" | |||||
Client is reporting end of game statistics | |||||
""" | |||||
try: | |||||
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) | |||||
def sendGameList(self, to = ""): | |||||
""" | |||||
Send a massive stanza with the whole game list. | |||||
If no target is passed the gamelist is broadcasted | |||||
to all clients. | |||||
""" | |||||
games = self.gameList.getAllGames() | |||||
stz = GameListXmppPlugin() | |||||
## Pull games and add each to the stanza | |||||
for JIDs in games: | |||||
g = games[JIDs] | |||||
stz.addGame(g) | |||||
## Set additional IQ attributes | |||||
iq = self.Iq() | |||||
iq['type'] = 'result' | |||||
iq.setPayload(stz) | |||||
if to == "": | |||||
for JID in list(self.presences): | |||||
if self.presences[JID] != "available" and self.presences[JID] != "away": | |||||
continue | |||||
iq['to'] = JID | |||||
## Try sending the stanza | |||||
try: | |||||
iq.send(block=False, now=True) | |||||
except: | |||||
logging.error("Failed to send game list") | |||||
else: | else: | ||||
## Check recipient exists | logging.info('Received unknown game command: "%s"', command) | ||||
if str(to) not in self.nicks: | |||||
logging.error("No player with the XmPP ID '%s' known to send gamelist to." % str(to)) | |||||
return | return | ||||
iq['to'] = to | |||||
## Try sending the stanza | if success: | ||||
try: | try: | ||||
iq.send(block=False, now=True) | self._send_game_list() | ||||
except: | except Exception: | ||||
logging.error("Failed to send game list") | logging.exception('Failed to send game list after "%s" command', command) | ||||
def relayBoardListRequest(self, recipient): | def _send_game_list(self, to=None): | ||||
""" | """Send a massive stanza with the whole game list. | ||||
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): | If no target is passed the gamelist is broadcasted to all | ||||
""" | clients. | ||||
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): | Arguments: | ||||
to (sleekxmpp.jid.JID): Player to send the game list to. | |||||
If None, the game list will be broadcasted | |||||
""" | """ | ||||
Send a profileRequest to EcheLOn. | games = self.games.get_all_games() | ||||
""" | |||||
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): | stanza = GameListXmppPlugin() | ||||
""" | for jid in games: | ||||
Tells EcheLOn that someone comes online. | stanza.add_game(games[jid]) | ||||
""" | |||||
## 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): | if not to: | ||||
""" | for nick in self.plugin['xep_0045'].getRoster(self.room): | ||||
Relay a game report to EcheLOn. | if nick == self.nick: | ||||
""" | |||||
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. | |||||
""" | |||||
iq = self.Iq() | |||||
iq['type'] = 'result' | |||||
iq.setPayload(boardList) | |||||
## Check recipient exists | |||||
if to == "": | |||||
# Rating List | |||||
for JID in list(self.presences): | |||||
if self.presences[JID] != "available" and self.presences[JID] != "away": | |||||
continue | continue | ||||
## Set additional IQ attributes | jid_str = self.plugin['xep_0045'].getJidProperty(self.room, nick, 'jid') | ||||
iq['to'] = JID | jid = sleekxmpp.jid.JID(jid_str) | ||||
## Try sending the stanza | iq = self.make_iq_result(ito=jid) | ||||
try: | iq.set_payload(stanza) | ||||
iq.send(block=False, now=True) | try: | ||||
except: | iq.send(block=False) | ||||
logging.error("Failed to send rating list") | except Exception: | ||||
logging.exception("Failed to send game list to %s", jid) | |||||
else: | else: | ||||
# Leaderboard or targeted rating list | iq = self.make_iq_result(ito=to) | ||||
if str(to) not in self.nicks: | iq.set_payload(stanza) | ||||
logging.error("No player with the XmPP ID '%s' known to send boardlist to" % str(to)) | |||||
return | |||||
## Set additional IQ attributes | |||||
iq['to'] = to | |||||
## Try sending the stanza | |||||
try: | try: | ||||
iq.send(block=False, now=True) | iq.send(block=False) | ||||
except: | except Exception: | ||||
logging.error("Failed to send leaderboard list") | logging.exception("Failed to send game list to %s", to) | ||||
def relayProfile(self, data, player, to): | |||||
""" | |||||
Send the player profile to a specified target. | |||||
""" | |||||
if to == "": | |||||
logging.error("Failed to send profile, target unspecified") | |||||
return | |||||
iq = self.Iq() | def parse_args(args): | ||||
iq['type'] = 'result' | """Parse command line arguments. | ||||
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)) | |||||
return | |||||
## Set additional IQ attributes | Arguments: | ||||
iq['to'] = to | args (dict): Raw command line arguments given to the script | ||||
## Try sending the stanza | Returns: | ||||
try: | Parsed command line arguments | ||||
iq.send(block=False, now=True) | |||||
except: | |||||
traceback.print_exc() | |||||
logging.error("Failed to send profile") | |||||
def warnRatingsBotOffline(self): | |||||
""" | """ | ||||
Warns that the ratings bot is offline. | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, | ||||
""" | description="XpartaMuPP - XMPP Multiplayer Game Manager") | ||||
if not self.ratingsBotWarned: | |||||
logging.warn("Ratings bot '%s' is offline" % str(self.ratingsBot)) | |||||
self.ratingsBotWarned = True | |||||
## Main Program ## | log_settings = parser.add_mutually_exclusive_group() | ||||
if __name__ == '__main__': | log_settings.add_argument('-q', '--quiet', help="only log errors", action='store_const', | ||||
# Setup the command line arguments. | dest='log_level', const=logging.ERROR) | ||||
optp = OptionParser() | log_settings.add_argument('-d', '--debug', help="log debug messages", action='store_const', | ||||
dest='log_level', const=logging.DEBUG) | |||||
log_settings.add_argument('-v', '--verbose', help="log more informative messages", | |||||
action='store_const', dest='log_level', const=logging.INFO) | |||||
log_settings.set_defaults(log_level=logging.WARNING) | |||||
# Output verbosity options. | parser.add_argument('-m', '--domain', help="XMPP server to connect to", | ||||
optp.add_option('-q', '--quiet', help='set logging to ERROR', | default='lobby.wildfiregames.com') | ||||
action='store_const', dest='loglevel', | parser.add_argument('-l', '--login', help="username for login", default='xpartamupp') | ||||
const=logging.ERROR, default=logging.INFO) | parser.add_argument('-p', '--password', help="password for login", default='XXXXXX') | ||||
optp.add_option('-d', '--debug', help='set logging to DEBUG', | parser.add_argument('-n', '--nickname', help="nickname shown to players", default='WFGBot') | ||||
action='store_const', dest='loglevel', | parser.add_argument('-r', '--room', help="XMPP MUC room to join", default='arena') | ||||
const=logging.DEBUG, default=logging.INFO) | parser.add_argument('-s', '--server', help='address of the ejabberd server', | ||||
optp.add_option('-v', '--verbose', help='set logging to COMM', | action='store', dest='xserver', default=None) | ||||
action='store_const', dest='loglevel', | parser.add_argument('-t', '--disable-tls', help='Pass this argument to connect without TLS encryption', | ||||
const=5, default=logging.INFO) | action='store_true', dest='xdisabletls', default=False) | ||||
# XpartaMuPP configuration options | |||||
optp.add_option('-m', '--domain', help='set xpartamupp domain', | |||||
action='store', dest='xdomain', | |||||
default="lobby.wildfiregames.com") | |||||
optp.add_option('-l', '--login', help='set xpartamupp login', | |||||
action='store', dest='xlogin', | |||||
default="xpartamupp") | |||||
optp.add_option('-p', '--password', help='set xpartamupp password', | |||||
action='store', dest='xpassword', | |||||
default="XXXXXX") | |||||
optp.add_option('-n', '--nickname', help='set xpartamupp nickname', | |||||
action='store', dest='xnickname', | |||||
default="WFGbot") | |||||
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") | |||||
# ejabberd server options | |||||
optp.add_option('-s', '--server', help='address of the ejabberd server', | |||||
action='store', dest='xserver', | |||||
default="localhost") | |||||
optp.add_option('-t', '--disable-tls', help='Pass this argument to connect without TLS encryption', | |||||
action='store_true', dest='xdisabletls', | |||||
default=False) | |||||
opts, args = optp.parse_args() | |||||
# Setup logging. | |||||
logging.basicConfig(level=opts.loglevel, | |||||
format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') | |||||
# XpartaMuPP | return parser.parse_args(args) | ||||
xmpp = XpartaMuPP(opts.xlogin+'@'+opts.xdomain+'/CC', opts.xpassword, opts.xroom+'@conference.'+opts.xdomain, opts.xnickname, opts.xratingsbot+'@'+opts.xdomain+'/CC') | |||||
def main(): | |||||
"""Entry point a console script.""" | |||||
args = parse_args(sys.argv[1:]) | |||||
logging.basicConfig(level=args.log_level, | |||||
format='%(asctime)s %(levelname)-8s %(message)s', | |||||
datefmt='%Y-%m-%d %H:%M:%S') | |||||
xmpp = XpartaMuPP(sleekxmpp.jid.JID('%s@%s/%s' % (args.login, args.domain, 'CC')), | |||||
args.password, args.room + '@conference.' + args.domain, args.nickname) | |||||
xmpp.register_plugin('xep_0030') # Service Discovery | xmpp.register_plugin('xep_0030') # Service Discovery | ||||
xmpp.register_plugin('xep_0004') # Data Forms | xmpp.register_plugin('xep_0004') # Data Forms | ||||
xmpp.register_plugin('xep_0045') # Multi-User Chat # used | xmpp.register_plugin('xep_0045') # Multi-User Chat | ||||
xmpp.register_plugin('xep_0060') # PubSub | xmpp.register_plugin('xep_0060') # Publish-Subscribe | ||||
xmpp.register_plugin('xep_0199') # XMPP Ping | xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping | ||||
if xmpp.connect((opts.xserver, 5222), True, not opts.xdisabletls): | if xmpp.connect((args.xserver, 5222) if args.xserver else None, True, not args.xdisabletls): | ||||
xmpp.process(threaded=False) | xmpp.process() | ||||
else: | else: | ||||
logging.error("Unable to connect") | logging.error("Unable to connect") | ||||
if __name__ == '__main__': | |||||
Not Done Inline ActionsDid you ditch those defaults on purpose? bb: Did you ditch those defaults on purpose? | |||||
main() |
Wildfire Games · Phabricator
2**7=128, that might be a bit few... Also should add some overflow_callback