Index: ps/trunk/source/tools/lobbybots/xpartamupp/stanzas.py
===================================================================
--- ps/trunk/source/tools/lobbybots/xpartamupp/stanzas.py (revision 25659)
+++ ps/trunk/source/tools/lobbybots/xpartamupp/stanzas.py (revision 25660)
@@ -1,156 +1,159 @@
# Copyright (C) 2021 Wildfire Games.
# This file is part of 0 A.D.
#
# 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
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# 0 A.D. is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with 0 A.D. If not, see .
"""0ad-specific XMPP-stanzas."""
from sleekxmpp.xmlstream import ElementBase, ET
class BoardListXmppPlugin(ElementBase):
"""Class for custom boardlist and ratinglist stanza extension."""
name = 'query'
namespace = 'jabber:iq:boardlist'
interfaces = {'board', 'command'}
sub_interfaces = interfaces
plugin_attrib = 'boardlist'
def add_command(self, command):
"""Add a command to the extension.
Arguments:
command (str): Command to add
"""
self.xml.append(ET.fromstring('%s' % command))
def add_item(self, name, rating):
"""Add an item to the extension.
Arguments:
name (str): Name of the player to add
rating (int): Rating of the player to add
"""
self.xml.append(ET.Element('board', {'name': name, 'rating': str(rating)}))
class GameListXmppPlugin(ElementBase):
"""Class for custom gamelist stanza extension."""
name = 'query'
namespace = 'jabber:iq:gamelist'
interfaces = {'game', 'command'}
sub_interfaces = interfaces
plugin_attrib = 'gamelist'
def add_game(self, data):
"""Add a game to the extension.
Arguments:
data (dict): game data to add
"""
+ try: del data['ip'] # Don't send the IP address with the gamelist.
+ except: pass
+
self.xml.append(ET.Element('game', data))
def get_game(self):
"""Get game from stanza.
Required to parse incoming stanzas with this extension.
Returns:
dict with game data
"""
game = self.xml.find('{%s}game' % self.namespace)
data = {}
if game is not None:
for key, item in game.items():
data[key] = item
return data
class GameReportXmppPlugin(ElementBase):
"""Class for custom gamereport stanza extension."""
name = 'report'
namespace = 'jabber:iq:gamereport'
plugin_attrib = 'gamereport'
interfaces = 'game'
sub_interfaces = interfaces
def add_game(self, game_report):
"""Add a game to the extension.
Arguments:
game_report (dict): a report about a game
"""
self.xml.append(ET.fromstring(str(game_report)).find('{%s}game' % self.namespace))
def get_game(self):
"""Get game from stanza.
Required to parse incoming stanzas with this extension.
Returns:
dict with game information
"""
game = self.xml.find('{%s}game' % self.namespace)
data = {}
if game is not None:
for key, item in game.items():
data[key] = item
return data
class ProfileXmppPlugin(ElementBase):
"""Class for custom profile."""
name = 'query'
namespace = 'jabber:iq:profile'
interfaces = {'profile', 'command'}
sub_interfaces = interfaces
plugin_attrib = 'profile'
def add_command(self, player_nick):
"""Add a command to the extension.
Arguments:
player_nick (str): the nick of the player the profile is about
"""
self.xml.append(ET.fromstring('%s' % player_nick))
def add_item(self, player, rating, highest_rating=0, # pylint: disable=too-many-arguments
rank=0, total_games_played=0, wins=0, losses=0):
"""Add an item to the extension.
Arguments:
player (str): Name of the player
rating (int): Current rating of the player
highest_rating (int): Highest rating the player had
rank (int): Rank of the player
total_games_played (int): Total number of games the player
played
wins (int): Number of won games the player had
losses (int): Number of lost games the player had
"""
item_xml = ET.Element('profile', {'player': player, 'rating': str(rating),
'highestRating': str(highest_rating), 'rank': str(rank),
'totalGamesPlayed': str(total_games_played),
'wins': str(wins), 'losses': str(losses)})
self.xml.append(item_xml)
Index: ps/trunk/source/tools/lobbybots/xpartamupp/xpartamupp.py
===================================================================
--- ps/trunk/source/tools/lobbybots/xpartamupp/xpartamupp.py (revision 25659)
+++ ps/trunk/source/tools/lobbybots/xpartamupp/xpartamupp.py (revision 25660)
@@ -1,359 +1,364 @@
#!/usr/bin/env python3
# Copyright (C) 2021 Wildfire Games.
# This file is part of 0 A.D.
#
# 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
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# 0 A.D. is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with 0 A.D. If not, see .
"""0ad XMPP-bot responsible for managing game listings."""
import argparse
import logging
import time
import sys
import sleekxmpp
from sleekxmpp.stanza import Iq
from sleekxmpp.xmlstream.handler import Callback
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."""
def __init__(self):
"""Initialize with empty games."""
self.games = LimitedSizeDict(size_limit=2**7)
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['nbp-init'] = data['nbp']
data['state'] = 'init'
except (KeyError, TypeError, ValueError):
logging.warning("Received invalid data for add game from 0ad: %s", data)
return False
else:
self.games[jid] = data
return True
def remove_game(self, jid):
"""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
"""
try:
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.
"""
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
"""
if jid not in self.games:
logging.warning("Tried to change state for non-existent game %s", jid)
return False
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:
logging.debug("change game (%s) state from %s to %s", jid,
self.games[jid]['state'], 'running')
self.games[jid]['state'] = 'running'
self.games[jid]['nbp'] = data['nbp']
self.games[jid]['players'] = data['players']
except (KeyError, ValueError):
logging.warning("Received invalid data for change game state from 0ad: %s", data)
return False
else:
if 'startTime' not in self.games[jid]:
self.games[jid]['startTime'] = str(round(time.time()))
return True
class XpartaMuPP(sleekxmpp.ClientXMPP):
"""Main class which handles IQ data and sends new data."""
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
"""
sleekxmpp.ClientXMPP.__init__(self, sjid, password)
self.whitespace_keepalive = False
self.room = room
self.nick = nick
self.games = Games()
register_stanza_plugin(Iq, GameListXmppPlugin)
self.register_handler(Callback('Iq Gamelist', StanzaPath('iq@type=set/gamelist'),
self._iq_game_list_handler))
self.add_event_handler('session_start', self._session_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)
def _session_start(self, event): # pylint: disable=unused-argument
"""Join MUC channel and announce presence.
Arguments:
event (dict): empty dummy dict
"""
self.plugin['xep_0045'].joinMUC(self.room, self.nick)
self.send_presence()
self.get_roster()
logging.info("XpartaMuPP started")
def _muc_online(self, presence):
"""Add joining players to the list of players.
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.
"""
nick = str(presence['muc']['nick'])
jid = sleekxmpp.jid.JID(presence['muc']['jid'])
if nick == self.nick:
return
if jid.resource not in ['0ad', 'CC']:
return
self._send_game_list(jid)
logging.debug("Client '%s' connected with a nick '%s'.", jid, nick)
def _muc_offline(self, presence):
"""Remove leaving players from the list of players.
Also remove the potential game this player was hosting, so we
don't end up with stale games.
Arguments:
presence (sleekxmpp.stanza.presence.Presence): Received
presence stanza.
"""
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():
self.send_message(mto=msg['from'].bare,
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')
def _iq_game_list_handler(self, iq):
"""Handle game state change requests.
Arguments:
iq (sleekxmpp.stanza.iq.IQ): Received IQ stanza
"""
if iq['from'].resource != '0ad':
return
+ success = False
+
command = iq['gamelist']['command']
if command == 'register':
success = self.games.add_game(iq['from'], iq['gamelist']['game'])
elif command == 'unregister':
success = self.games.remove_game(iq['from'])
elif command == 'changestate':
success = self.games.change_game_state(iq['from'], iq['gamelist']['game'])
else:
logging.info('Received unknown game command: "%s"', command)
- return
+
+ iq.reply(clear=not success)
+ if not success: iq['error']['condition'] = "undefined-condition"
+ iq.send()
if success:
try:
self._send_game_list()
except Exception:
logging.exception('Failed to send game list after "%s" command', command)
def _send_game_list(self, to=None):
"""Send a massive stanza with the whole game list.
If no target is passed the gamelist is broadcasted to all
clients.
Arguments:
to (sleekxmpp.jid.JID): Player to send the game list to.
If None, the game list will be broadcasted
"""
games = self.games.get_all_games()
stanza = GameListXmppPlugin()
for jid in games:
stanza.add_game(games[jid])
if not to:
for nick in self.plugin['xep_0045'].getRoster(self.room):
if nick == self.nick:
continue
jid_str = self.plugin['xep_0045'].getJidProperty(self.room, nick, 'jid')
jid = sleekxmpp.jid.JID(jid_str)
iq = self.make_iq_result(ito=jid)
iq.set_payload(stanza)
try:
iq.send(block=False)
except Exception:
logging.exception("Failed to send game list to %s", jid)
else:
iq = self.make_iq_result(ito=to)
iq.set_payload(stanza)
try:
iq.send(block=False)
except Exception:
logging.exception("Failed to send game list to %s", to)
def parse_args(args):
"""Parse command line arguments.
Arguments:
args (dict): Raw command line arguments given to the script
Returns:
Parsed command line arguments
"""
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="XpartaMuPP - XMPP Multiplayer Game Manager")
log_settings = parser.add_mutually_exclusive_group()
log_settings.add_argument('-q', '--quiet', help="only log errors", action='store_const',
dest='log_level', const=logging.ERROR)
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)
parser.add_argument('-m', '--domain', help="XMPP server to connect to",
default='lobby.wildfiregames.com')
parser.add_argument('-l', '--login', help="username for login", default='xpartamupp')
parser.add_argument('-p', '--password', help="password for login", default='XXXXXX')
parser.add_argument('-n', '--nickname', help="nickname shown to players", default='WFGBot')
parser.add_argument('-r', '--room', help="XMPP MUC room to join", default='arena')
parser.add_argument('-s', '--server', help='address of the ejabberd server',
action='store', dest='xserver', default=None)
parser.add_argument('-t', '--disable-tls', help='Pass this argument to connect without TLS encryption',
action='store_true', dest='xdisabletls', default=False)
return parser.parse_args(args)
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_0004') # Data Forms
xmpp.register_plugin('xep_0045') # Multi-User Chat
xmpp.register_plugin('xep_0060') # Publish-Subscribe
xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping
if xmpp.connect((args.xserver, 5222) if args.xserver else None, True, not args.xdisabletls):
xmpp.process()
else:
logging.error("Unable to connect")
if __name__ == '__main__':
main()