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 []