Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_room_game.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/plugins/plugin_misc_room_game.py@0046283a285d |
children | 56f94936df1e |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT: a jabber client | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.i18n import _ | |
21 from sat.core.constants import Const as C | |
22 from sat.core.log import getLogger | |
23 log = getLogger(__name__) | |
24 from twisted.words.protocols.jabber import jid | |
25 from twisted.words.xish import domish | |
26 from twisted.internet import defer | |
27 from time import time | |
28 from wokkel import disco, iwokkel | |
29 from zope.interface import implements | |
30 import copy | |
31 try: | |
32 from twisted.words.protocols.xmlstream import XMPPHandler | |
33 except ImportError: | |
34 from wokkel.subprotocols import XMPPHandler | |
35 | |
36 # Don't forget to set it to False before you commit | |
37 _DEBUG = False | |
38 | |
39 PLUGIN_INFO = { | |
40 C.PI_NAME: "Room game", | |
41 C.PI_IMPORT_NAME: "ROOM-GAME", | |
42 C.PI_TYPE: "MISC", | |
43 C.PI_PROTOCOLS: [], | |
44 C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0249"], | |
45 C.PI_MAIN: "RoomGame", | |
46 C.PI_HANDLER: "no", # handler MUST be "no" (dynamic inheritance) | |
47 C.PI_DESCRIPTION: _("""Base class for MUC games""") | |
48 } | |
49 | |
50 | |
51 # FIXME: this plugin is broken, need to be fixed | |
52 | |
53 class RoomGame(object): | |
54 """This class is used to help launching a MUC game. | |
55 | |
56 Bridge methods callbacks: _prepareRoom, _playerReady, _createGame | |
57 Triggered methods: userJoinedTrigger, userLeftTrigger | |
58 Also called from subclasses: newRound | |
59 | |
60 For examples of messages sequences, please look in sub-classes. | |
61 """ | |
62 | |
63 # Values for self.invite_mode (who can invite after the game creation) | |
64 FROM_ALL, FROM_NONE, FROM_REFEREE, FROM_PLAYERS = xrange(0, 4) | |
65 # Values for self.wait_mode (for who we should wait before creating the game) | |
66 FOR_ALL, FOR_NONE = xrange(0, 2) | |
67 # Values for self.join_mode (who can join the game - NONE means solo game) | |
68 ALL, INVITED, NONE = xrange(0, 3) | |
69 # Values for ready_mode (how to turn a MUC user into a player) | |
70 ASK, FORCE = xrange(0, 2) | |
71 | |
72 MESSAGE = '/message' | |
73 REQUEST = '%s/%s[@xmlns="%s"]' | |
74 | |
75 def __init__(self, host): | |
76 """For other plugin to dynamically inherit this class, it is necessary to not use __init__ but _init_. | |
77 The subclass itself must be initialized this way: | |
78 | |
79 class MyGame(object): | |
80 | |
81 def inheritFromRoomGame(self, host): | |
82 global RoomGame | |
83 RoomGame = host.plugins["ROOM-GAME"].__class__ | |
84 self.__class__ = type(self.__class__.__name__, (self.__class__, RoomGame, object), {}) | |
85 | |
86 def __init__(self, host): | |
87 self.inheritFromRoomGame(host) | |
88 RoomGame._init_(self, host, ...) | |
89 | |
90 """ | |
91 self.host = host | |
92 | |
93 def _init_(self, host, plugin_info, ns_tag, game_init=None, player_init=None): | |
94 """ | |
95 @param host | |
96 @param plugin_info: PLUGIN_INFO map of the game plugin | |
97 @param ns_tag: couple (nameservice, tag) to construct the messages | |
98 @param game_init: dictionary for general game initialization | |
99 @param player_init: dictionary for player initialization, applicable to each player | |
100 """ | |
101 self.host = host | |
102 self.name = plugin_info["import_name"] | |
103 self.ns_tag = ns_tag | |
104 self.request = self.REQUEST % (self.MESSAGE, ns_tag[1], ns_tag[0]) | |
105 if game_init is None: | |
106 game_init = {} | |
107 if player_init is None: | |
108 player_init = {} | |
109 self.game_init = game_init | |
110 self.player_init = player_init | |
111 self.games = {} | |
112 self.invitations = {} # values are a couple (x, y) with x the time and y a list of users | |
113 | |
114 # These are the default settings, which can be overwritten by child class after initialization | |
115 self.invite_mode = self.FROM_PLAYERS if self.player_init == {} else self.FROM_NONE | |
116 self.wait_mode = self.FOR_NONE if self.player_init == {} else self.FOR_ALL | |
117 self.join_mode = self.INVITED | |
118 self.ready_mode = self.FORCE # TODO: asking for confirmation is not implemented | |
119 | |
120 # this has been added for testing purpose. It is sometimes needed to remove a dependence | |
121 # while building the synchronization data, for example to replace a call to time.time() | |
122 # by an arbitrary value. If needed, this attribute would be set to True from the testcase. | |
123 self.testing = False | |
124 | |
125 host.trigger.add("MUC user joined", self.userJoinedTrigger) | |
126 host.trigger.add("MUC user left", self.userLeftTrigger) | |
127 | |
128 def _createOrInvite(self, room_jid, other_players, profile): | |
129 """ | |
130 This is called only when someone explicitly wants to play. | |
131 | |
132 The game will not be created if one already exists in the room, | |
133 also its creation could be postponed until all the expected players | |
134 join the room (in that case it will be created from userJoinedTrigger). | |
135 @param room (wokkel.muc.Room): the room | |
136 @param other_players (list[jid.JID]): list of the other players JID (bare) | |
137 """ | |
138 # FIXME: broken ! | |
139 raise NotImplementedError("To be fixed") | |
140 client = self.host.getClient(profile) | |
141 user_jid = self.host.getJidNStream(profile)[0] | |
142 nick = self.host.plugins["XEP-0045"].getRoomNick(client, room_jid) | |
143 nicks = [nick] | |
144 if self._gameExists(room_jid): | |
145 if not self._checkJoinAuth(room_jid, user_jid, nick): | |
146 return | |
147 nicks.extend(self._invitePlayers(room_jid, other_players, nick, profile)) | |
148 self._updatePlayers(room_jid, nicks, True, profile) | |
149 else: | |
150 self._initGame(room_jid, nick) | |
151 (auth, waiting, missing) = self._checkWaitAuth(room_jid, other_players) | |
152 nicks.extend(waiting) | |
153 nicks.extend(self._invitePlayers(room_jid, missing, nick, profile)) | |
154 if auth: | |
155 self.createGame(room_jid, nicks, profile) | |
156 else: | |
157 self._updatePlayers(room_jid, nicks, False, profile) | |
158 | |
159 def _initGame(self, room_jid, referee_nick): | |
160 """ | |
161 | |
162 @param room_jid (jid.JID): JID of the room | |
163 @param referee_nick (unicode): nickname of the referee | |
164 """ | |
165 # Important: do not add the referee to 'players' yet. For a | |
166 # <players /> message to be emitted whenever a new player is joining, | |
167 # it is necessary to not modify 'players' outside of _updatePlayers. | |
168 referee_jid = jid.JID(room_jid.userhost() + '/' + referee_nick) | |
169 self.games[room_jid] = {'referee': referee_jid, 'players': [], 'started': False, 'status': {}} | |
170 self.games[room_jid].update(copy.deepcopy(self.game_init)) | |
171 self.invitations.setdefault(room_jid, []) | |
172 | |
173 def _gameExists(self, room_jid, started=False): | |
174 """Return True if a game has been initialized/started. | |
175 @param started: if False, the game must be initialized to return True, | |
176 otherwise it must be initialized and started with createGame. | |
177 @return: True if a game is initialized/started in that room""" | |
178 return room_jid in self.games and (not started or self.games[room_jid]['started']) | |
179 | |
180 def _checkJoinAuth(self, room_jid, user_jid=None, nick="", verbose=False): | |
181 """Checks if this profile is allowed to join the game. | |
182 | |
183 The parameter nick is used to check if the user is already | |
184 a player in that game. When this method is called from | |
185 userJoinedTrigger, nick is also used to check the user | |
186 identity instead of user_jid_s (see TODO comment below). | |
187 @param room_jid (jid.JID): the JID of the room hosting the game | |
188 @param user_jid (jid.JID): JID of the user | |
189 @param nick (unicode): nick of the user | |
190 @return: True if this profile can join the game | |
191 """ | |
192 auth = False | |
193 if not self._gameExists(room_jid): | |
194 auth = False | |
195 elif self.join_mode == self.ALL or self.isPlayer(room_jid, nick): | |
196 auth = True | |
197 elif self.join_mode == self.INVITED: | |
198 # considering all the batches of invitations | |
199 for invitations in self.invitations[room_jid]: | |
200 if user_jid is not None: | |
201 if user_jid.userhostJID() in invitations[1]: | |
202 auth = True | |
203 break | |
204 else: | |
205 # TODO: that's not secure enough but what to do if | |
206 # wokkel.muc.User's 'entity' attribute is not set?! | |
207 if nick in [invited.user for invited in invitations[1]]: | |
208 auth = True | |
209 break | |
210 | |
211 if not auth and (verbose or _DEBUG): | |
212 log.debug(_(u"%(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()}) | |
213 return auth | |
214 | |
215 def _updatePlayers(self, room_jid, nicks, sync, profile): | |
216 """Update the list of players and signal to the room that some players joined the game. | |
217 If sync is True, the news players are synchronized with the game data they have missed. | |
218 Remark: self.games[room_jid]['players'] should not be modified outside this method. | |
219 @param room_jid (jid.JID): JID of the room | |
220 @param nicks (list[unicode]): list of players nicks in the room (referee included, in first position) | |
221 @param sync (bool): set to True to send synchronization data to the new players | |
222 @param profile (unicode): %(doc_profile)s | |
223 """ | |
224 if nicks == []: | |
225 return | |
226 # this is better than set(nicks).difference(...) as it keeps the order | |
227 new_nicks = [nick for nick in nicks if nick not in self.games[room_jid]['players']] | |
228 if len(new_nicks) == 0: | |
229 return | |
230 | |
231 def setStatus(status): | |
232 for nick in new_nicks: | |
233 self.games[room_jid]['status'][nick] = status | |
234 | |
235 sync = sync and self._gameExists(room_jid, True) and len(self.games[room_jid]['players']) > 0 | |
236 setStatus('desync' if sync else 'init') | |
237 self.games[room_jid]['players'].extend(new_nicks) | |
238 self._synchronizeRoom(room_jid, [room_jid], profile) | |
239 if sync: | |
240 setStatus('init') | |
241 | |
242 def _synchronizeRoom(self, room_jid, recipients, profile): | |
243 """Communicate the list of players to the whole room or only to some users, | |
244 also send the synchronization data to the players who recently joined the game. | |
245 @param room_jid (jid.JID): JID of the room | |
246 @recipients (list[jid.JID]): list of JIDs, the recipients of the message could be: | |
247 - room JID | |
248 - room JID + "/" + user nick | |
249 @param profile (unicode): %(doc_profile)s | |
250 """ | |
251 if self._gameExists(room_jid, started=True): | |
252 element = self._createStartElement(self.games[room_jid]['players']) | |
253 else: | |
254 element = self._createStartElement(self.games[room_jid]['players'], name="players") | |
255 elements = [(element, None, None)] | |
256 | |
257 sync_args = [] | |
258 sync_data = self._getSyncData(room_jid) | |
259 for nick in sync_data: | |
260 user_jid = jid.JID(room_jid.userhost() + '/' + nick) | |
261 if user_jid in recipients: | |
262 user_elements = copy.deepcopy(elements) | |
263 for child in sync_data[nick]: | |
264 user_elements.append((child, None, None)) | |
265 recipients.remove(user_jid) | |
266 else: | |
267 user_elements = [(child, None, None) for child in sync_data[nick]] | |
268 sync_args.append(([user_jid, user_elements], {'profile': profile})) | |
269 | |
270 for recipient in recipients: | |
271 self._sendElements(recipient, elements, profile=profile) | |
272 for args, kwargs in sync_args: | |
273 self._sendElements(*args, **kwargs) | |
274 | |
275 def _getSyncData(self, room_jid, force_nicks=None): | |
276 """The synchronization data are returned for each player who | |
277 has the state 'desync' or if he's been contained by force_nicks. | |
278 @param room_jid (jid.JID): JID of the room | |
279 @param force_nicks: force the synchronization for this list of the nicks | |
280 @return: a mapping between player nicks and a list of elements to | |
281 be sent by self._synchronizeRoom for the game to be synchronized. | |
282 """ | |
283 if not self._gameExists(room_jid): | |
284 return {} | |
285 data = {} | |
286 status = self.games[room_jid]['status'] | |
287 nicks = [nick for nick in status if status[nick] == 'desync'] | |
288 if force_nicks is None: | |
289 force_nicks = [] | |
290 for nick in force_nicks: | |
291 if nick not in nicks: | |
292 nicks.append(nick) | |
293 for nick in nicks: | |
294 elements = self.getSyncDataForPlayer(room_jid, nick) | |
295 if elements: | |
296 data[nick] = elements | |
297 return data | |
298 | |
299 def getSyncDataForPlayer(self, room_jid, nick): | |
300 """This method may (and should probably) be overwritten by a child class. | |
301 @param room_jid (jid.JID): JID of the room | |
302 @param nick: the nick of the player to be synchronized | |
303 @return: a list of elements to synchronize this player with the game. | |
304 """ | |
305 return [] | |
306 | |
307 def _invitePlayers(self, room_jid, other_players, nick, profile): | |
308 """Invite players to a room, associated game may exist or not. | |
309 | |
310 @param other_players (list[jid.JID]): list of the players to invite | |
311 @param nick (unicode): nick of the user who send the invitation | |
312 @return: list[unicode] of room nicks for invited players who are already in the room | |
313 """ | |
314 raise NotImplementedError("Need to be fixed !") | |
315 # FIXME: this is broken and unsecure ! | |
316 if not self._checkInviteAuth(room_jid, nick): | |
317 return [] | |
318 # TODO: remove invitation waiting for too long, using the time data | |
319 self.invitations[room_jid].append((time(), [player.userhostJID() for player in other_players])) | |
320 nicks = [] | |
321 for player_jid in [player.userhostJID() for player in other_players]: | |
322 # TODO: find a way to make it secure | |
323 other_nick = self.host.plugins["XEP-0045"].getRoomEntityNick(room_jid, player_jid, secure=self.testing) | |
324 if other_nick is None: | |
325 self.host.plugins["XEP-0249"].invite(player_jid, room_jid, {"game": self.name}, profile) | |
326 else: | |
327 nicks.append(other_nick) | |
328 return nicks | |
329 | |
330 def _checkInviteAuth(self, room_jid, nick, verbose=False): | |
331 """Checks if this user is allowed to invite players | |
332 | |
333 @param room_jid (jid.JID): JID of the room | |
334 @param nick: user nick in the room | |
335 @param verbose: display debug message | |
336 @return: True if the user is allowed to invite other players | |
337 """ | |
338 auth = False | |
339 if self.invite_mode == self.FROM_ALL or not self._gameExists(room_jid): | |
340 auth = True | |
341 elif self.invite_mode == self.FROM_NONE: | |
342 auth = not self._gameExists(room_jid, started=True) and self.isReferee(room_jid, nick) | |
343 elif self.invite_mode == self.FROM_REFEREE: | |
344 auth = self.isReferee(room_jid, nick) | |
345 elif self.invite_mode == self.FROM_PLAYERS: | |
346 auth = self.isPlayer(room_jid, nick) | |
347 if not auth and (verbose or _DEBUG): | |
348 log.debug(_(u"%(user)s not allowed to invite for the game %(game)s in %(room)s") % {'user': nick, 'game': self.name, 'room': room_jid.userhost()}) | |
349 return auth | |
350 | |
351 def isReferee(self, room_jid, nick): | |
352 """Checks if the player with this nick is the referee for the game in this room" | |
353 @param room_jid (jid.JID): room JID | |
354 @param nick: user nick in the room | |
355 @return: True if the user is the referee of the game in this room | |
356 """ | |
357 if not self._gameExists(room_jid): | |
358 return False | |
359 return jid.JID(room_jid.userhost() + '/' + nick) == self.games[room_jid]['referee'] | |
360 | |
361 def isPlayer(self, room_jid, nick): | |
362 """Checks if the user with this nick is a player for the game in this room. | |
363 @param room_jid (jid.JID): JID of the room | |
364 @param nick: user nick in the room | |
365 @return: True if the user is a player of the game in this room | |
366 """ | |
367 if not self._gameExists(room_jid): | |
368 return False | |
369 # Important: the referee is not in the 'players' list right after | |
370 # the game initialization, that's why we do also check with isReferee | |
371 return nick in self.games[room_jid]['players'] or self.isReferee(room_jid, nick) | |
372 | |
373 def _checkWaitAuth(self, room, other_players, verbose=False): | |
374 """Check if we must wait for other players before starting the game. | |
375 | |
376 @param room (wokkel.muc.Room): the room | |
377 @param other_players (list[jid.JID]): list of the players without the referee | |
378 @param verbose (bool): display debug message | |
379 @return: (x, y, z) with: | |
380 x: False if we must wait, True otherwise | |
381 y: the nicks of the players that have been checked and confirmed | |
382 z: the JID of the players that have not been checked or that are missing | |
383 """ | |
384 if self.wait_mode == self.FOR_NONE or other_players == []: | |
385 result = (True, [], other_players) | |
386 elif len(room.roster) < len(other_players): | |
387 # do not check the players until we may actually have them all | |
388 result = (False, [], other_players) | |
389 else: | |
390 # TODO: find a way to make it secure | |
391 (nicks, missing) = self.host.plugins["XEP-0045"].getRoomNicksOfUsers(room, other_players, secure=False) | |
392 result = (len(nicks) == len(other_players), nicks, missing) | |
393 if not result[0] and (verbose or _DEBUG): | |
394 log.debug(_(u"Still waiting for %(users)s before starting the game %(game)s in %(room)s") % {'users': result[2], 'game': self.name, 'room': room.occupantJID.userhost()}) | |
395 return result | |
396 | |
397 def getUniqueName(self, muc_service=None, profile_key=C.PROF_KEY_NONE): | |
398 """Generate unique room name | |
399 | |
400 @param muc_service (jid.JID): you can leave empty to autofind the muc service | |
401 @param profile_key (unicode): %(doc_profile_key)s | |
402 @return: jid.JID (unique name for a new room to be created) | |
403 """ | |
404 client = self.host.getClient(profile_key) | |
405 # FIXME: jid.JID must be used instead of strings | |
406 room = self.host.plugins["XEP-0045"].getUniqueName(client, muc_service) | |
407 return jid.JID("sat_%s_%s" % (self.name.lower(), room.userhost())) | |
408 | |
409 def _prepareRoom(self, other_players=None, room_jid_s='', profile_key=C.PROF_KEY_NONE): | |
410 room_jid = jid.JID(room_jid_s) if room_jid_s else None | |
411 other_players = [jid.JID(player).userhostJID() for player in other_players] | |
412 return self.prepareRoom(other_players, room_jid, profile_key) | |
413 | |
414 def prepareRoom(self, other_players=None, room_jid=None, profile_key=C.PROF_KEY_NONE): | |
415 """Prepare the room for a game: create it if it doesn't exist and invite players. | |
416 | |
417 @param other_players (list[JID]): list of other players JID (bare) | |
418 @param room_jid (jid.JID): JID of the room, or None to generate a unique name | |
419 @param profile_key (unicode): %(doc_profile_key)s | |
420 """ | |
421 # FIXME: need to be refactored | |
422 client = self.host.getClient(profile_key) | |
423 log.debug(_(u'Preparing room for %s game') % self.name) | |
424 profile = self.host.memory.getProfileName(profile_key) | |
425 if not profile: | |
426 log.error(_("Unknown profile")) | |
427 return defer.succeed(None) | |
428 if other_players is None: | |
429 other_players = [] | |
430 | |
431 # Create/join the given room, or a unique generated one if no room is specified. | |
432 if room_jid is None: | |
433 room_jid = self.getUniqueName(profile_key=profile_key) | |
434 else: | |
435 self.host.plugins["XEP-0045"].checkRoomJoined(client, room_jid) | |
436 self._createOrInvite(client, room_jid, other_players) | |
437 return defer.succeed(None) | |
438 | |
439 user_jid = self.host.getJidNStream(profile)[0] | |
440 d = self.host.plugins["XEP-0045"].join(room_jid, user_jid.user, {}, profile) | |
441 return d.addCallback(lambda dummy: self._createOrInvite(client, room_jid, other_players)) | |
442 | |
443 def userJoinedTrigger(self, room, user, profile): | |
444 """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. | |
445 | |
446 @room: wokkel.muc.Room object. room.roster is a dict{wokkel.muc.User.nick: wokkel.muc.User} | |
447 @user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID | |
448 @return: True to not interrupt the main process. | |
449 """ | |
450 room_jid = room.occupantJID.userhostJID() | |
451 profile_nick = room.occupantJID.resource | |
452 if not self.isReferee(room_jid, profile_nick): | |
453 return True # profile is not the referee | |
454 if not self._checkJoinAuth(room_jid, user.entity if user.entity else None, user.nick): | |
455 # user not allowed but let him know that we are playing :p | |
456 self._synchronizeRoom(room_jid, [jid.JID(room_jid.userhost() + '/' + user.nick)], profile) | |
457 return True | |
458 if self.wait_mode == self.FOR_ALL: | |
459 # considering the last batch of invitations | |
460 batch = len(self.invitations[room_jid]) - 1 | |
461 if batch < 0: | |
462 log.error(u"Invitations from %s to play %s in %s have been lost!" % (profile_nick, self.name, room_jid.userhost())) | |
463 return True | |
464 other_players = self.invitations[room_jid][batch][1] | |
465 (auth, nicks, dummy) = self._checkWaitAuth(room, other_players) | |
466 if auth: | |
467 del self.invitations[room_jid][batch] | |
468 nicks.insert(0, profile_nick) # add the referee | |
469 self.createGame(room_jid, nicks, profile_key=profile) | |
470 return True | |
471 # let the room know that a new player joined | |
472 self._updatePlayers(room_jid, [user.nick], True, profile) | |
473 return True | |
474 | |
475 def userLeftTrigger(self, room, user, profile): | |
476 """This trigger is used to update or stop the game when a user leaves. | |
477 | |
478 @room: wokkel.muc.Room object. room.roster is a dict{wokkel.muc.User.nick: wokkel.muc.User} | |
479 @user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID | |
480 @return: True to not interrupt the main process. | |
481 """ | |
482 room_jid = room.occupantJID.userhostJID() | |
483 profile_nick = room.occupantJID.resource | |
484 if not self.isReferee(room_jid, profile_nick): | |
485 return True # profile is not the referee | |
486 if self.isPlayer(room_jid, user.nick): | |
487 try: | |
488 self.games[room_jid]['players'].remove(user.nick) | |
489 except ValueError: | |
490 pass | |
491 if len(self.games[room_jid]['players']) == 0: | |
492 return True | |
493 if self.wait_mode == self.FOR_ALL: | |
494 # allow this user to join the game again | |
495 user_jid = user.entity.userhostJID() | |
496 if len(self.invitations[room_jid]) == 0: | |
497 self.invitations[room_jid].append((time(), [user_jid])) | |
498 else: | |
499 batch = 0 # add to the first batch of invitations | |
500 if user_jid not in self.invitations[room_jid][batch][1]: | |
501 self.invitations[room_jid][batch][1].append(user_jid) | |
502 return True | |
503 | |
504 def _checkCreateGameAndInit(self, room_jid, profile): | |
505 """Check if that profile can create the game. If the game can be created | |
506 but is not initialized yet, this method will also do the initialization. | |
507 | |
508 @param room_jid (jid.JID): JID of the room | |
509 @param profile | |
510 @return: a couple (create, sync) with: | |
511 - create: set to True to allow the game creation | |
512 - sync: set to True to advice a game synchronization | |
513 """ | |
514 user_nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid, profile) | |
515 if not user_nick: | |
516 log.error(u'Internal error: profile %s has not joined the room %s' % (profile, room_jid.userhost())) | |
517 return False, False | |
518 if self._gameExists(room_jid): | |
519 is_referee = self.isReferee(room_jid, user_nick) | |
520 if self._gameExists(room_jid, started=True): | |
521 log.info(_(u"%(game)s game already created in room %(room)s") % {'game': self.name, 'room': room_jid.userhost()}) | |
522 return False, is_referee | |
523 elif not is_referee: | |
524 log.info(_(u"%(game)s game in room %(room)s can only be created by %(user)s") % {'game': self.name, 'room': room_jid.userhost(), 'user': user_nick}) | |
525 return False, False | |
526 else: | |
527 self._initGame(room_jid, user_nick) | |
528 return True, False | |
529 | |
530 def _createGame(self, room_jid_s, nicks=None, profile_key=C.PROF_KEY_NONE): | |
531 self.createGame(jid.JID(room_jid_s), nicks, profile_key) | |
532 | |
533 def createGame(self, room_jid, nicks=None, profile_key=C.PROF_KEY_NONE): | |
534 """Create a new game. | |
535 | |
536 This can be called directly from a frontend and skips all the checks and invitation system, | |
537 but the game must not exist and all the players must be in the room already. | |
538 @param room_jid (jid.JID): JID of the room | |
539 @param nicks (list[unicode]): list of players nicks in the room (referee included, in first position) | |
540 @param profile_key (unicode): %(doc_profile_key)s | |
541 """ | |
542 log.debug(_(u"Creating %(game)s game in room %(room)s") % {'game': self.name, 'room': room_jid}) | |
543 profile = self.host.memory.getProfileName(profile_key) | |
544 if not profile: | |
545 log.error(_(u"profile %s is unknown") % profile_key) | |
546 return | |
547 (create, sync) = self._checkCreateGameAndInit(room_jid, profile) | |
548 if nicks is None: | |
549 nicks = [] | |
550 if not create: | |
551 if sync: | |
552 self._updatePlayers(room_jid, nicks, True, profile) | |
553 return | |
554 self.games[room_jid]['started'] = True | |
555 self._updatePlayers(room_jid, nicks, False, profile) | |
556 if self.player_init: | |
557 # specific data to each player (score, private data) | |
558 self.games[room_jid].setdefault('players_data', {}) | |
559 for nick in nicks: | |
560 # The dict must be COPIED otherwise it is shared between all users | |
561 self.games[room_jid]['players_data'][nick] = copy.deepcopy(self.player_init) | |
562 | |
563 def _playerReady(self, player_nick, referee_jid_s, profile_key=C.PROF_KEY_NONE): | |
564 self.playerReady(player_nick, jid.JID(referee_jid_s), profile_key) | |
565 | |
566 def playerReady(self, player_nick, referee_jid, profile_key=C.PROF_KEY_NONE): | |
567 """Must be called when player is ready to start a new game | |
568 | |
569 @param player: the player nick in the room | |
570 @param referee_jid (jid.JID): JID of the referee | |
571 """ | |
572 profile = self.host.memory.getProfileName(profile_key) | |
573 if not profile: | |
574 log.error(_(u"profile %s is unknown") % profile_key) | |
575 return | |
576 log.debug(u'new player ready: %s' % profile) | |
577 # TODO: we probably need to add the game and room names in the sent message | |
578 self.send(referee_jid, 'player_ready', {'player': player_nick}, profile=profile) | |
579 | |
580 def newRound(self, room_jid, data, profile): | |
581 """Launch a new round (reinit the user data) | |
582 | |
583 @param room_jid: room userhost | |
584 @param data: a couple (common_data, msg_elts) with: | |
585 - common_data: backend initialization data for the new round | |
586 - msg_elts: dict to map each user to his specific initialization message | |
587 @param profile | |
588 """ | |
589 log.debug(_(u'new round for %s game') % self.name) | |
590 game_data = self.games[room_jid] | |
591 players = game_data['players'] | |
592 players_data = game_data['players_data'] | |
593 game_data['stage'] = "init" | |
594 | |
595 common_data, msg_elts = copy.deepcopy(data) if data is not None else (None, None) | |
596 | |
597 if isinstance(msg_elts, dict): | |
598 for player in players: | |
599 to_jid = jid.JID(room_jid.userhost() + "/" + player) # FIXME: gof: | |
600 elem = msg_elts[player] if isinstance(msg_elts[player], domish.Element) else None | |
601 self.send(to_jid, elem, profile=profile) | |
602 elif isinstance(msg_elts, domish.Element): | |
603 self.send(room_jid, msg_elts, profile=profile) | |
604 if common_data is not None: | |
605 for player in players: | |
606 players_data[player].update(copy.deepcopy(common_data)) | |
607 | |
608 def _createGameElt(self, to_jid): | |
609 """Create a generic domish Element for the game messages | |
610 | |
611 @param to_jid: JID of the recipient | |
612 @return: the created element | |
613 """ | |
614 type_ = "normal" if to_jid.resource else "groupchat" | |
615 elt = domish.Element((None, 'message')) | |
616 elt["to"] = to_jid.full() | |
617 elt["type"] = type_ | |
618 elt.addElement(self.ns_tag) | |
619 return elt | |
620 | |
621 def _createStartElement(self, players=None, name="started"): | |
622 """Create a domish Element listing the game users | |
623 | |
624 @param players: list of the players | |
625 @param name: element name: | |
626 - "started" to signal the players that the game has been started | |
627 - "players" to signal the list of players when the game is not started yet | |
628 @return the create element | |
629 """ | |
630 started_elt = domish.Element((None, name)) | |
631 if players is None: | |
632 return started_elt | |
633 idx = 0 | |
634 for player in players: | |
635 player_elt = domish.Element((None, 'player')) | |
636 player_elt.addContent(player) | |
637 player_elt['index'] = str(idx) | |
638 idx += 1 | |
639 started_elt.addChild(player_elt) | |
640 return started_elt | |
641 | |
642 def _sendElements(self, to_jid, data, profile=None): | |
643 """ TODO | |
644 | |
645 @param to_jid: recipient JID | |
646 @param data: list of (elem, attr, content) with: | |
647 - elem: domish.Element, unicode or a couple: | |
648 - domish.Element to be directly added as a child to the message | |
649 - unicode name or couple (uri, name) to create a new domish.Element | |
650 and add it as a child to the message (see domish.Element.addElement) | |
651 - attrs: dictionary of attributes for the new child | |
652 - content: unicode that is appended to the child content | |
653 @param profile: the profile from which the message is sent | |
654 @return: a Deferred instance | |
655 """ | |
656 client = self.host.getClient(profile) | |
657 msg = self._createGameElt(to_jid) | |
658 for elem, attrs, content in data: | |
659 if elem is not None: | |
660 if isinstance(elem, domish.Element): | |
661 msg.firstChildElement().addChild(elem) | |
662 else: | |
663 elem = msg.firstChildElement().addElement(elem) | |
664 if attrs is not None: | |
665 elem.attributes.update(attrs) | |
666 if content is not None: | |
667 elem.addContent(content) | |
668 client.send(msg) | |
669 return defer.succeed(None) | |
670 | |
671 def send(self, to_jid, elem=None, attrs=None, content=None, profile=None): | |
672 """ TODO | |
673 | |
674 @param to_jid: recipient JID | |
675 @param elem: domish.Element, unicode or a couple: | |
676 - domish.Element to be directly added as a child to the message | |
677 - unicode name or couple (uri, name) to create a new domish.Element | |
678 and add it as a child to the message (see domish.Element.addElement) | |
679 @param attrs: dictionary of attributes for the new child | |
680 @param content: unicode that is appended to the child content | |
681 @param profile: the profile from which the message is sent | |
682 @return: a Deferred instance | |
683 """ | |
684 return self._sendElements(to_jid, [(elem, attrs, content)], profile) | |
685 | |
686 def getHandler(self, client): | |
687 return RoomGameHandler(self) | |
688 | |
689 | |
690 class RoomGameHandler (XMPPHandler): | |
691 implements(iwokkel.IDisco) | |
692 | |
693 def __init__(self, plugin_parent): | |
694 self.plugin_parent = plugin_parent | |
695 self.host = plugin_parent.host | |
696 | |
697 def connectionInitialized(self): | |
698 self.xmlstream.addObserver(self.plugin_parent.request, self.plugin_parent.room_game_cmd, profile=self.parent.profile) | |
699 | |
700 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
701 return [disco.DiscoFeature(self.plugin_parent.ns_tag[0])] | |
702 | |
703 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
704 return [] |