Mercurial > libervia-backend
diff libervia/backend/plugins/plugin_misc_room_game.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_misc_room_game.py@524856bd7b19 |
children | 0d7bb4df2343 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/plugins/plugin_misc_room_game.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,784 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from libervia.backend.core.i18n import _ +from libervia.backend.core.constants import Const as C +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) +from twisted.words.protocols.jabber import jid +from twisted.words.xish import domish +from twisted.internet import defer +from time import time +from wokkel import disco, iwokkel +from zope.interface import implementer +import copy + +try: + from twisted.words.protocols.xmlstream import XMPPHandler +except ImportError: + from wokkel.subprotocols import XMPPHandler + +# Don't forget to set it to False before you commit +_DEBUG = False + +PLUGIN_INFO = { + C.PI_NAME: "Room game", + C.PI_IMPORT_NAME: "ROOM-GAME", + C.PI_TYPE: "MISC", + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0249"], + C.PI_MAIN: "RoomGame", + C.PI_HANDLER: "no", # handler MUST be "no" (dynamic inheritance) + C.PI_DESCRIPTION: _("""Base class for MUC games"""), +} + + +# FIXME: this plugin is broken, need to be fixed + + +class RoomGame(object): + """This class is used to help launching a MUC game. + + bridge methods callbacks: _prepare_room, _player_ready, _create_game + Triggered methods: user_joined_trigger, user_left_trigger + Also called from subclasses: new_round + + For examples of messages sequences, please look in sub-classes. + """ + + # Values for self.invite_mode (who can invite after the game creation) + FROM_ALL, FROM_NONE, FROM_REFEREE, FROM_PLAYERS = range(0, 4) + # Values for self.wait_mode (for who we should wait before creating the game) + FOR_ALL, FOR_NONE = range(0, 2) + # Values for self.join_mode (who can join the game - NONE means solo game) + ALL, INVITED, NONE = range(0, 3) + # Values for ready_mode (how to turn a MUC user into a player) + ASK, FORCE = range(0, 2) + + MESSAGE = "/message" + REQUEST = '%s/%s[@xmlns="%s"]' + + def __init__(self, host): + """For other plugin to dynamically inherit this class, it is necessary to not use __init__ but _init_. + The subclass itself must be initialized this way: + + class MyGame(object): + + def inherit_from_room_game(self, host): + global RoomGame + RoomGame = host.plugins["ROOM-GAME"].__class__ + self.__class__ = type(self.__class__.__name__, (self.__class__, RoomGame, object), {}) + + def __init__(self, host): + self.inherit_from_room_game(host) + RoomGame._init_(self, host, ...) + + """ + self.host = host + + def _init_(self, host, plugin_info, ns_tag, game_init=None, player_init=None): + """ + @param host + @param plugin_info: PLUGIN_INFO map of the game plugin + @param ns_tag: couple (nameservice, tag) to construct the messages + @param game_init: dictionary for general game initialization + @param player_init: dictionary for player initialization, applicable to each player + """ + self.host = host + self.name = plugin_info["import_name"] + self.ns_tag = ns_tag + self.request = self.REQUEST % (self.MESSAGE, ns_tag[1], ns_tag[0]) + if game_init is None: + game_init = {} + if player_init is None: + player_init = {} + self.game_init = game_init + self.player_init = player_init + self.games = {} + self.invitations = {} # values are a couple (x, y) with x the time and y a list of users + + # These are the default settings, which can be overwritten by child class after initialization + self.invite_mode = self.FROM_PLAYERS if self.player_init == {} else self.FROM_NONE + self.wait_mode = self.FOR_NONE if self.player_init == {} else self.FOR_ALL + self.join_mode = self.INVITED + self.ready_mode = self.FORCE # TODO: asking for confirmation is not implemented + + # this has been added for testing purpose. It is sometimes needed to remove a dependence + # while building the synchronization data, for example to replace a call to time.time() + # by an arbitrary value. If needed, this attribute would be set to True from the testcase. + self.testing = False + + host.trigger.add("MUC user joined", self.user_joined_trigger) + host.trigger.add("MUC user left", self.user_left_trigger) + + def _create_or_invite(self, room_jid, other_players, profile): + """ + This is called only when someone explicitly wants to play. + + The game will not be created if one already exists in the room, + also its creation could be postponed until all the expected players + join the room (in that case it will be created from user_joined_trigger). + @param room (wokkel.muc.Room): the room + @param other_players (list[jid.JID]): list of the other players JID (bare) + """ + # FIXME: broken ! + raise NotImplementedError("To be fixed") + client = self.host.get_client(profile) + user_jid = self.host.get_jid_n_stream(profile)[0] + nick = self.host.plugins["XEP-0045"].get_room_nick(client, room_jid) + nicks = [nick] + if self._game_exists(room_jid): + if not self._check_join_auth(room_jid, user_jid, nick): + return + nicks.extend(self._invite_players(room_jid, other_players, nick, profile)) + self._update_players(room_jid, nicks, True, profile) + else: + self._init_game(room_jid, nick) + (auth, waiting, missing) = self._check_wait_auth(room_jid, other_players) + nicks.extend(waiting) + nicks.extend(self._invite_players(room_jid, missing, nick, profile)) + if auth: + self.create_game(room_jid, nicks, profile) + else: + self._update_players(room_jid, nicks, False, profile) + + def _init_game(self, room_jid, referee_nick): + """ + + @param room_jid (jid.JID): JID of the room + @param referee_nick (unicode): nickname of the referee + """ + # Important: do not add the referee to 'players' yet. For a + # <players /> message to be emitted whenever a new player is joining, + # it is necessary to not modify 'players' outside of _update_players. + referee_jid = jid.JID(room_jid.userhost() + "/" + referee_nick) + self.games[room_jid] = { + "referee": referee_jid, + "players": [], + "started": False, + "status": {}, + } + self.games[room_jid].update(copy.deepcopy(self.game_init)) + self.invitations.setdefault(room_jid, []) + + def _game_exists(self, room_jid, started=False): + """Return True if a game has been initialized/started. + @param started: if False, the game must be initialized to return True, + otherwise it must be initialized and started with create_game. + @return: True if a game is initialized/started in that room""" + return room_jid in self.games and (not started or self.games[room_jid]["started"]) + + def _check_join_auth(self, room_jid, user_jid=None, nick="", verbose=False): + """Checks if this profile is allowed to join the game. + + The parameter nick is used to check if the user is already + a player in that game. When this method is called from + user_joined_trigger, nick is also used to check the user + identity instead of user_jid_s (see TODO comment below). + @param room_jid (jid.JID): the JID of the room hosting the game + @param user_jid (jid.JID): JID of the user + @param nick (unicode): nick of the user + @return: True if this profile can join the game + """ + auth = False + if not self._game_exists(room_jid): + auth = False + elif self.join_mode == self.ALL or self.is_player(room_jid, nick): + auth = True + elif self.join_mode == self.INVITED: + # considering all the batches of invitations + for invitations in self.invitations[room_jid]: + if user_jid is not None: + if user_jid.userhostJID() in invitations[1]: + auth = True + break + else: + # TODO: that's not secure enough but what to do if + # wokkel.muc.User's 'entity' attribute is not set?! + if nick in [invited.user for invited in invitations[1]]: + auth = True + break + + if not auth and (verbose or _DEBUG): + log.debug( + _("%(user)s not allowed to join the game %(game)s in %(room)s") + % { + "user": user_jid.userhost() or nick, + "game": self.name, + "room": room_jid.userhost(), + } + ) + return auth + + def _update_players(self, room_jid, nicks, sync, profile): + """Update the list of players and signal to the room that some players joined the game. + If sync is True, the news players are synchronized with the game data they have missed. + Remark: self.games[room_jid]['players'] should not be modified outside this method. + @param room_jid (jid.JID): JID of the room + @param nicks (list[unicode]): list of players nicks in the room (referee included, in first position) + @param sync (bool): set to True to send synchronization data to the new players + @param profile (unicode): %(doc_profile)s + """ + if nicks == []: + return + # this is better than set(nicks).difference(...) as it keeps the order + new_nicks = [ + nick for nick in nicks if nick not in self.games[room_jid]["players"] + ] + if len(new_nicks) == 0: + return + + def setStatus(status): + for nick in new_nicks: + self.games[room_jid]["status"][nick] = status + + sync = ( + sync + and self._game_exists(room_jid, True) + and len(self.games[room_jid]["players"]) > 0 + ) + setStatus("desync" if sync else "init") + self.games[room_jid]["players"].extend(new_nicks) + self._synchronize_room(room_jid, [room_jid], profile) + if sync: + setStatus("init") + + def _synchronize_room(self, room_jid, recipients, profile): + """Communicate the list of players to the whole room or only to some users, + also send the synchronization data to the players who recently joined the game. + @param room_jid (jid.JID): JID of the room + @recipients (list[jid.JID]): list of JIDs, the recipients of the message could be: + - room JID + - room JID + "/" + user nick + @param profile (unicode): %(doc_profile)s + """ + if self._game_exists(room_jid, started=True): + element = self._create_start_element(self.games[room_jid]["players"]) + else: + element = self._create_start_element( + self.games[room_jid]["players"], name="players" + ) + elements = [(element, None, None)] + + sync_args = [] + sync_data = self._get_sync_data(room_jid) + for nick in sync_data: + user_jid = jid.JID(room_jid.userhost() + "/" + nick) + if user_jid in recipients: + user_elements = copy.deepcopy(elements) + for child in sync_data[nick]: + user_elements.append((child, None, None)) + recipients.remove(user_jid) + else: + user_elements = [(child, None, None) for child in sync_data[nick]] + sync_args.append(([user_jid, user_elements], {"profile": profile})) + + for recipient in recipients: + self._send_elements(recipient, elements, profile=profile) + for args, kwargs in sync_args: + self._send_elements(*args, **kwargs) + + def _get_sync_data(self, room_jid, force_nicks=None): + """The synchronization data are returned for each player who + has the state 'desync' or if he's been contained by force_nicks. + @param room_jid (jid.JID): JID of the room + @param force_nicks: force the synchronization for this list of the nicks + @return: a mapping between player nicks and a list of elements to + be sent by self._synchronize_room for the game to be synchronized. + """ + if not self._game_exists(room_jid): + return {} + data = {} + status = self.games[room_jid]["status"] + nicks = [nick for nick in status if status[nick] == "desync"] + if force_nicks is None: + force_nicks = [] + for nick in force_nicks: + if nick not in nicks: + nicks.append(nick) + for nick in nicks: + elements = self.get_sync_data_for_player(room_jid, nick) + if elements: + data[nick] = elements + return data + + def get_sync_data_for_player(self, room_jid, nick): + """This method may (and should probably) be overwritten by a child class. + @param room_jid (jid.JID): JID of the room + @param nick: the nick of the player to be synchronized + @return: a list of elements to synchronize this player with the game. + """ + return [] + + def _invite_players(self, room_jid, other_players, nick, profile): + """Invite players to a room, associated game may exist or not. + + @param other_players (list[jid.JID]): list of the players to invite + @param nick (unicode): nick of the user who send the invitation + @return: list[unicode] of room nicks for invited players who are already in the room + """ + raise NotImplementedError("Need to be fixed !") + # FIXME: this is broken and unsecure ! + if not self._check_invite_auth(room_jid, nick): + return [] + # TODO: remove invitation waiting for too long, using the time data + self.invitations[room_jid].append( + (time(), [player.userhostJID() for player in other_players]) + ) + nicks = [] + for player_jid in [player.userhostJID() for player in other_players]: + # TODO: find a way to make it secure + other_nick = self.host.plugins["XEP-0045"].getRoomEntityNick( + room_jid, player_jid, secure=self.testing + ) + if other_nick is None: + self.host.plugins["XEP-0249"].invite( + player_jid, room_jid, {"game": self.name}, profile + ) + else: + nicks.append(other_nick) + return nicks + + def _check_invite_auth(self, room_jid, nick, verbose=False): + """Checks if this user is allowed to invite players + + @param room_jid (jid.JID): JID of the room + @param nick: user nick in the room + @param verbose: display debug message + @return: True if the user is allowed to invite other players + """ + auth = False + if self.invite_mode == self.FROM_ALL or not self._game_exists(room_jid): + auth = True + elif self.invite_mode == self.FROM_NONE: + auth = not self._game_exists(room_jid, started=True) and self.is_referee( + room_jid, nick + ) + elif self.invite_mode == self.FROM_REFEREE: + auth = self.is_referee(room_jid, nick) + elif self.invite_mode == self.FROM_PLAYERS: + auth = self.is_player(room_jid, nick) + if not auth and (verbose or _DEBUG): + log.debug( + _("%(user)s not allowed to invite for the game %(game)s in %(room)s") + % {"user": nick, "game": self.name, "room": room_jid.userhost()} + ) + return auth + + def is_referee(self, room_jid, nick): + """Checks if the player with this nick is the referee for the game in this room" + @param room_jid (jid.JID): room JID + @param nick: user nick in the room + @return: True if the user is the referee of the game in this room + """ + if not self._game_exists(room_jid): + return False + return ( + jid.JID(room_jid.userhost() + "/" + nick) == self.games[room_jid]["referee"] + ) + + def is_player(self, room_jid, nick): + """Checks if the user with this nick is a player for the game in this room. + @param room_jid (jid.JID): JID of the room + @param nick: user nick in the room + @return: True if the user is a player of the game in this room + """ + if not self._game_exists(room_jid): + return False + # Important: the referee is not in the 'players' list right after + # the game initialization, that's why we do also check with is_referee + return nick in self.games[room_jid]["players"] or self.is_referee(room_jid, nick) + + def _check_wait_auth(self, room, other_players, verbose=False): + """Check if we must wait for other players before starting the game. + + @param room (wokkel.muc.Room): the room + @param other_players (list[jid.JID]): list of the players without the referee + @param verbose (bool): display debug message + @return: (x, y, z) with: + x: False if we must wait, True otherwise + y: the nicks of the players that have been checked and confirmed + z: the JID of the players that have not been checked or that are missing + """ + if self.wait_mode == self.FOR_NONE or other_players == []: + result = (True, [], other_players) + elif len(room.roster) < len(other_players): + # do not check the players until we may actually have them all + result = (False, [], other_players) + else: + # TODO: find a way to make it secure + (nicks, missing) = self.host.plugins["XEP-0045"].getRoomNicksOfUsers( + room, other_players, secure=False + ) + result = (len(nicks) == len(other_players), nicks, missing) + if not result[0] and (verbose or _DEBUG): + log.debug( + _( + "Still waiting for %(users)s before starting the game %(game)s in %(room)s" + ) + % { + "users": result[2], + "game": self.name, + "room": room.occupantJID.userhost(), + } + ) + return result + + def get_unique_name(self, muc_service=None, profile_key=C.PROF_KEY_NONE): + """Generate unique room name + + @param muc_service (jid.JID): you can leave empty to autofind the muc service + @param profile_key (unicode): %(doc_profile_key)s + @return: jid.JID (unique name for a new room to be created) + """ + client = self.host.get_client(profile_key) + # FIXME: jid.JID must be used instead of strings + room = self.host.plugins["XEP-0045"].get_unique_name(client, muc_service) + return jid.JID("sat_%s_%s" % (self.name.lower(), room.userhost())) + + def _prepare_room( + self, other_players=None, room_jid_s="", profile_key=C.PROF_KEY_NONE + ): + room_jid = jid.JID(room_jid_s) if room_jid_s else None + other_players = [jid.JID(player).userhostJID() for player in other_players] + return self.prepare_room(other_players, room_jid, profile_key) + + def prepare_room(self, other_players=None, room_jid=None, profile_key=C.PROF_KEY_NONE): + """Prepare the room for a game: create it if it doesn't exist and invite players. + + @param other_players (list[JID]): list of other players JID (bare) + @param room_jid (jid.JID): JID of the room, or None to generate a unique name + @param profile_key (unicode): %(doc_profile_key)s + """ + # FIXME: need to be refactored + client = self.host.get_client(profile_key) + log.debug(_("Preparing room for %s game") % self.name) + profile = self.host.memory.get_profile_name(profile_key) + if not profile: + log.error(_("Unknown profile")) + return defer.succeed(None) + if other_players is None: + other_players = [] + + # Create/join the given room, or a unique generated one if no room is specified. + if room_jid is None: + room_jid = self.get_unique_name(profile_key=profile_key) + else: + self.host.plugins["XEP-0045"].check_room_joined(client, room_jid) + self._create_or_invite(client, room_jid, other_players) + return defer.succeed(None) + + user_jid = self.host.get_jid_n_stream(profile)[0] + d = self.host.plugins["XEP-0045"].join(room_jid, user_jid.user, {}, profile) + return d.addCallback( + lambda __: self._create_or_invite(client, room_jid, other_players) + ) + + def user_joined_trigger(self, room, user, profile): + """This trigger is used to check if the new user can take part of a game, create the game if we were waiting for him or just update the players list. + + @room: wokkel.muc.Room object. room.roster is a dict{wokkel.muc.User.nick: wokkel.muc.User} + @user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID + @return: True to not interrupt the main process. + """ + room_jid = room.occupantJID.userhostJID() + profile_nick = room.occupantJID.resource + if not self.is_referee(room_jid, profile_nick): + return True # profile is not the referee + if not self._check_join_auth( + room_jid, user.entity if user.entity else None, user.nick + ): + # user not allowed but let him know that we are playing :p + self._synchronize_room( + room_jid, [jid.JID(room_jid.userhost() + "/" + user.nick)], profile + ) + return True + if self.wait_mode == self.FOR_ALL: + # considering the last batch of invitations + batch = len(self.invitations[room_jid]) - 1 + if batch < 0: + log.error( + "Invitations from %s to play %s in %s have been lost!" + % (profile_nick, self.name, room_jid.userhost()) + ) + return True + other_players = self.invitations[room_jid][batch][1] + (auth, nicks, __) = self._check_wait_auth(room, other_players) + if auth: + del self.invitations[room_jid][batch] + nicks.insert(0, profile_nick) # add the referee + self.create_game(room_jid, nicks, profile_key=profile) + return True + # let the room know that a new player joined + self._update_players(room_jid, [user.nick], True, profile) + return True + + def user_left_trigger(self, room, user, profile): + """This trigger is used to update or stop the game when a user leaves. + + @room: wokkel.muc.Room object. room.roster is a dict{wokkel.muc.User.nick: wokkel.muc.User} + @user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID + @return: True to not interrupt the main process. + """ + room_jid = room.occupantJID.userhostJID() + profile_nick = room.occupantJID.resource + if not self.is_referee(room_jid, profile_nick): + return True # profile is not the referee + if self.is_player(room_jid, user.nick): + try: + self.games[room_jid]["players"].remove(user.nick) + except ValueError: + pass + if len(self.games[room_jid]["players"]) == 0: + return True + if self.wait_mode == self.FOR_ALL: + # allow this user to join the game again + user_jid = user.entity.userhostJID() + if len(self.invitations[room_jid]) == 0: + self.invitations[room_jid].append((time(), [user_jid])) + else: + batch = 0 # add to the first batch of invitations + if user_jid not in self.invitations[room_jid][batch][1]: + self.invitations[room_jid][batch][1].append(user_jid) + return True + + def _check_create_game_and_init(self, room_jid, profile): + """Check if that profile can create the game. If the game can be created + but is not initialized yet, this method will also do the initialization. + + @param room_jid (jid.JID): JID of the room + @param profile + @return: a couple (create, sync) with: + - create: set to True to allow the game creation + - sync: set to True to advice a game synchronization + """ + user_nick = self.host.plugins["XEP-0045"].get_room_nick(room_jid, profile) + if not user_nick: + log.error( + "Internal error: profile %s has not joined the room %s" + % (profile, room_jid.userhost()) + ) + return False, False + if self._game_exists(room_jid): + is_referee = self.is_referee(room_jid, user_nick) + if self._game_exists(room_jid, started=True): + log.info( + _("%(game)s game already created in room %(room)s") + % {"game": self.name, "room": room_jid.userhost()} + ) + return False, is_referee + elif not is_referee: + log.info( + _("%(game)s game in room %(room)s can only be created by %(user)s") + % {"game": self.name, "room": room_jid.userhost(), "user": user_nick} + ) + return False, False + else: + self._init_game(room_jid, user_nick) + return True, False + + def _create_game(self, room_jid_s, nicks=None, profile_key=C.PROF_KEY_NONE): + self.create_game(jid.JID(room_jid_s), nicks, profile_key) + + def create_game(self, room_jid, nicks=None, profile_key=C.PROF_KEY_NONE): + """Create a new game. + + This can be called directly from a frontend and skips all the checks and invitation system, + but the game must not exist and all the players must be in the room already. + @param room_jid (jid.JID): JID of the room + @param nicks (list[unicode]): list of players nicks in the room (referee included, in first position) + @param profile_key (unicode): %(doc_profile_key)s + """ + log.debug( + _("Creating %(game)s game in room %(room)s") + % {"game": self.name, "room": room_jid} + ) + profile = self.host.memory.get_profile_name(profile_key) + if not profile: + log.error(_("profile %s is unknown") % profile_key) + return + (create, sync) = self._check_create_game_and_init(room_jid, profile) + if nicks is None: + nicks = [] + if not create: + if sync: + self._update_players(room_jid, nicks, True, profile) + return + self.games[room_jid]["started"] = True + self._update_players(room_jid, nicks, False, profile) + if self.player_init: + # specific data to each player (score, private data) + self.games[room_jid].setdefault("players_data", {}) + for nick in nicks: + # The dict must be COPIED otherwise it is shared between all users + self.games[room_jid]["players_data"][nick] = copy.deepcopy( + self.player_init + ) + + def _player_ready(self, player_nick, referee_jid_s, profile_key=C.PROF_KEY_NONE): + self.player_ready(player_nick, jid.JID(referee_jid_s), profile_key) + + def player_ready(self, player_nick, referee_jid, profile_key=C.PROF_KEY_NONE): + """Must be called when player is ready to start a new game + + @param player: the player nick in the room + @param referee_jid (jid.JID): JID of the referee + """ + profile = self.host.memory.get_profile_name(profile_key) + if not profile: + log.error(_("profile %s is unknown") % profile_key) + return + log.debug("new player ready: %s" % profile) + # TODO: we probably need to add the game and room names in the sent message + self.send(referee_jid, "player_ready", {"player": player_nick}, profile=profile) + + def new_round(self, room_jid, data, profile): + """Launch a new round (reinit the user data) + + @param room_jid: room userhost + @param data: a couple (common_data, msg_elts) with: + - common_data: backend initialization data for the new round + - msg_elts: dict to map each user to his specific initialization message + @param profile + """ + log.debug(_("new round for %s game") % self.name) + game_data = self.games[room_jid] + players = game_data["players"] + players_data = game_data["players_data"] + game_data["stage"] = "init" + + common_data, msg_elts = copy.deepcopy(data) if data is not None else (None, None) + + if isinstance(msg_elts, dict): + for player in players: + to_jid = jid.JID(room_jid.userhost() + "/" + player) # FIXME: gof: + elem = ( + msg_elts[player] + if isinstance(msg_elts[player], domish.Element) + else None + ) + self.send(to_jid, elem, profile=profile) + elif isinstance(msg_elts, domish.Element): + self.send(room_jid, msg_elts, profile=profile) + if common_data is not None: + for player in players: + players_data[player].update(copy.deepcopy(common_data)) + + def _create_game_elt(self, to_jid): + """Create a generic domish Element for the game messages + + @param to_jid: JID of the recipient + @return: the created element + """ + type_ = "normal" if to_jid.resource else "groupchat" + elt = domish.Element((None, "message")) + elt["to"] = to_jid.full() + elt["type"] = type_ + elt.addElement(self.ns_tag) + return elt + + def _create_start_element(self, players=None, name="started"): + """Create a domish Element listing the game users + + @param players: list of the players + @param name: element name: + - "started" to signal the players that the game has been started + - "players" to signal the list of players when the game is not started yet + @return the create element + """ + started_elt = domish.Element((None, name)) + if players is None: + return started_elt + idx = 0 + for player in players: + player_elt = domish.Element((None, "player")) + player_elt.addContent(player) + player_elt["index"] = str(idx) + idx += 1 + started_elt.addChild(player_elt) + return started_elt + + def _send_elements(self, to_jid, data, profile=None): + """ TODO + + @param to_jid: recipient JID + @param data: list of (elem, attr, content) with: + - elem: domish.Element, unicode or a couple: + - domish.Element to be directly added as a child to the message + - unicode name or couple (uri, name) to create a new domish.Element + and add it as a child to the message (see domish.Element.addElement) + - attrs: dictionary of attributes for the new child + - content: unicode that is appended to the child content + @param profile: the profile from which the message is sent + @return: a Deferred instance + """ + client = self.host.get_client(profile) + msg = self._create_game_elt(to_jid) + for elem, attrs, content in data: + if elem is not None: + if isinstance(elem, domish.Element): + msg.firstChildElement().addChild(elem) + else: + elem = msg.firstChildElement().addElement(elem) + if attrs is not None: + elem.attributes.update(attrs) + if content is not None: + elem.addContent(content) + client.send(msg) + return defer.succeed(None) + + def send(self, to_jid, elem=None, attrs=None, content=None, profile=None): + """ TODO + + @param to_jid: recipient JID + @param elem: domish.Element, unicode or a couple: + - domish.Element to be directly added as a child to the message + - unicode name or couple (uri, name) to create a new domish.Element + and add it as a child to the message (see domish.Element.addElement) + @param attrs: dictionary of attributes for the new child + @param content: unicode that is appended to the child content + @param profile: the profile from which the message is sent + @return: a Deferred instance + """ + return self._send_elements(to_jid, [(elem, attrs, content)], profile) + + def get_handler(self, client): + return RoomGameHandler(self) + + +@implementer(iwokkel.IDisco) +class RoomGameHandler(XMPPHandler): + + def __init__(self, plugin_parent): + self.plugin_parent = plugin_parent + self.host = plugin_parent.host + + def connectionInitialized(self): + self.xmlstream.addObserver( + self.plugin_parent.request, + self.plugin_parent.room_game_cmd, + profile=self.parent.profile, + ) + + def getDiscoInfo(self, requestor, target, nodeIdentifier=""): + return [disco.DiscoFeature(self.plugin_parent.ns_tag[0])] + + def getDiscoItems(self, requestor, target, nodeIdentifier=""): + return []