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