diff --git a/data/glade/roster_contact_context_menu.glade b/data/glade/roster_contact_context_menu.glade index f873e7f77..2a1955007 100644 --- a/data/glade/roster_contact_context_menu.glade +++ b/data/glade/roster_contact_context_menu.glade @@ -91,6 +91,13 @@ + + + True + Play Tic Tac Toe + True + + True diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 1a005f47d..bd806f9fd 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -50,7 +50,8 @@ if dbus_support.supported: from music_track_listener import MusicTrackListener # XXX interface leaking into the back end? -import session +from session import ChatControlSession +import tictactoe STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] @@ -768,6 +769,7 @@ class ConnectionDisco: q.addChild('feature', attrs = {'var': common.xmpp.NS_MOOD}) q.addChild('feature', attrs = {'var': common.xmpp.NS_MOOD + '+notify'}) q.addChild('feature', attrs = {'var': common.xmpp.NS_ESESSION_INIT}) + q.addChild('feature', attrs = {'var': 'http://jabber.org/protocol/games'}) if (node is None or extension == 'cstates') and gajim.config.get('outgoing_chat_state_notifactions') != 'disabled': q.addChild('feature', attrs = {'var': common.xmpp.NS_CHATSTATES}) @@ -779,6 +781,9 @@ class ConnectionDisco: q.addChild('feature', attrs = {'var': common.xmpp.NS_PING}) q.addChild('feature', attrs = {'var': common.xmpp.NS_TIME_REVISED}) + if node == 'http://jabber.org/protocol/games': + q.addChild('feature', attrs = {'var': 'http://jabber.org/protocol/games/tictactoe'}) + if q.getChildren(): self.connection.send(iq) raise common.xmpp.NodeProcessed @@ -1538,7 +1543,20 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if not mtype: mtype = 'normal' - if not mtype == 'groupchat': + game_invite = msg.getTag('invite', namespace='http://jabber.org/protocol/games') + if game_invite: + game = game_invite.getTag('game') + + if game.getAttr('var') == 'http://jabber.org/protocol/games/tictactoe': + klass = tictactoe.TicTacToeSession + + # this assumes that the invitation came with a thread_id we haven't seen + session = self.make_new_session(frm, thread_id, klass=klass) + + session.invited(msg) + + return + elif mtype != 'groupchat': session = self.get_session(frm, thread_id, mtype) if thread_id and not session.received_thread_id: @@ -1550,27 +1568,23 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, return # check if the message is a xep70-confirmation-request - if msg.getTag('confirm') and msg.getTag('confirm').namespace == \ - common.xmpp.NS_HTTP_AUTH: + if msg.getTag('confirm', namespace=common.xmpp.NS_HTTP_AUTH): self._HttpAuthCB(con, msg) return # check if the message is a XEP 0020 feature negotiation request - if msg.getTag('feature') and msg.getTag('feature').namespace == \ - common.xmpp.NS_FEATURE: + if msg.getTag('feature', namespace=common.xmpp.NS_FEATURE): if gajim.HAVE_PYCRYPTO: self._FeatureNegCB(con, msg, session) return # check if the message is initiating an ESession negotiation - if msg.getTag('init') and msg.getTag('init').namespace == \ - common.xmpp.NS_ESESSION_INIT: + if msg.getTag('init', namespace=common.xmpp.NS_ESESSION_INIT): self._InitE2ECB(con, msg, session) encrypted = False - e2e_tag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO) - if e2e_tag: + if msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO): encrypted = True try: @@ -1701,12 +1715,16 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if treat_as: mtype = treat_as - session.received(frm, msgtxt, tim, encrypted, mtype, subject, chatstate, - msg_id, composing_xep, user_nick, msghtml, form_node) + # XXX horrible hack + if isinstance(session, ChatControlSession): + session.received(frm, msgtxt, tim, encrypted, mtype, subject, chatstate, + msg_id, composing_xep, user_nick, msghtml, form_node) + else: + session.received(msg) # END messageCB # process and dispatch an error message - def dispatch_error_message(self, msg, msgtxt, session, frm, tim, subject) + def dispatch_error_message(self, msg, msgtxt, session, frm, tim, subject): error_msg = msg.getError() if not error_msg: @@ -1761,7 +1779,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, except exceptions.PysqliteOperationalError, e: self.dispatch('ERROR', (_('Disk Write Error'), str(e))) - def dispatch_invite_message(self, invite, frm) + def dispatch_invite_message(self, invite, frm): item = invite.getTag('invite') jid_from = item.getAttr('from') @@ -1833,7 +1851,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, '''finds all of the sessions between us and jid that jid hasn't sent a thread_id in yet. returns the session that we last sent a message to.''' - + sessions_with_jid = self.sessions[jid].values() no_threadid_sessions = filter(lambda s: not s.received_thread_id, sessions_with_jid) @@ -1843,8 +1861,11 @@ returns the session that we last sent a message to.''' else: return None - def make_new_session(self, jid, thread_id = None, type = 'chat'): - sess = session.ChatControlSession(self, common.xmpp.JID(jid), thread_id, type) + def make_new_session(self, jid, thread_id=None, type='chat', klass=None): + if not klass: + klass = ChatControlSession + + sess = klass(self, common.xmpp.JID(jid), thread_id, type) if not jid in self.sessions: self.sessions[jid] = {} diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 84034dc9e..bee4ae98f 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -32,10 +32,23 @@ class StanzaSession(object): else: self.thread_id = self.generate_thread_id() + self.loggable = True + self.last_send = 0 self.status = None self.negotiated = {} + def is_loggable(self): + account = self.conn.name + no_log_for = gajim.config.get_per('accounts', account, 'no_log_for') + + if not no_log_for: + no_log_for = '' + + no_log_for = no_log_for.split() + + return self.loggable and account not in no_log_for and self.jid not in no_log_for + def generate_thread_id(self): return "".join([random.choice(string.ascii_letters) for x in xrange(0,32)]) @@ -132,8 +145,6 @@ class EncryptedStanzaSession(StanzaSession): def __init__(self, conn, jid, thread_id, type = 'chat'): StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat') - self.loggable = True - self.xes = {} self.es = {} @@ -891,17 +902,6 @@ otherwise, list the fields we haven't implemented''' # preventing falsified messages from going through. self.km_o = '' - def is_loggable(self): - account = self.conn.name - no_log_for = gajim.config.get_per('accounts', account, 'no_log_for') - - if not no_log_for: - no_log_for = '' - - no_log_for = no_log_for.split() - - return self.loggable and account not in no_log_for and self.jid not in no_log_for - def cancelled_negotiation(self): StanzaSession.cancelled_negotiation(self) self.enable_encryption = False diff --git a/src/roster_window.py b/src/roster_window.py index 54ce52e09..f884fe015 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -2345,6 +2345,9 @@ class RosterWindow: contacts, account, self.on_execute_command)) else: # one resource + tictactoe_menuitem = xml.get_widget( 'tictactoe_menuitem') + tictactoe_menuitem.connect('activate', self.play_tictactoe, contact, account, contact.resource) + start_chat_menuitem.connect('activate', self.on_open_chat_window, contact, account) execute_command_menuitem.connect('activate', self.on_execute_command, @@ -4278,6 +4281,17 @@ class RosterWindow: return True return False + def play_tictactoe(self, widget, contact, account, resource=None): + jid = contact.jid + + if resource is not None: + jid = jid + u'/' + resource + + import tictactoe + + sess = gajim.connections[account].make_new_session(jid, klass=tictactoe.TicTacToeSession) + sess.begin() + def on_execute_command(self, widget, contact, account, resource=None): '''Execute command. Full JID needed; if it is other contact, resource is necessary. Widget is unnecessary, only to be diff --git a/src/session.py b/src/session.py index 494be99ef..643c7eaf7 100644 --- a/src/session.py +++ b/src/session.py @@ -16,6 +16,7 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): self.control = None + # dispatch a received stanza def received(self, full_jid_with_resource, message, tim, encrypted, msg_type, subject, chatstate, msg_id, composing_xep, user_nick, xhtml, form_node): jid = gajim.get_jid_without_resource(full_jid_with_resource) @@ -114,6 +115,7 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): encrypted, msg_type, subject, chatstate, msg_id, composing_xep, user_nick, xhtml, form_node])) + # display the message or show notification in the roster def roster_message(self, jid, msg, tim, encrypted=False, msg_type='', subject=None, resource='', msg_id=None, user_nick='', advanced_notif_num=None, xhtml=None, form_node=None): diff --git a/src/tictactoe.py b/src/tictactoe.py new file mode 100644 index 000000000..4477b1b10 --- /dev/null +++ b/src/tictactoe.py @@ -0,0 +1,276 @@ +from common import stanza_session +from common import xmpp + +import pygtk +pygtk.require('2.0') +import gtk +from gtk import gdk +import cairo + +# implements + +class InvalidMove(Exception): + pass + +class TicTacToeSession(stanza_session.StanzaSession): + def begin(self, rows = 3, cols = 3, role_s = 'x'): + self.rows = rows + self.cols = cols + + self.role_s = role_s + + if self.role_s == 'x': + self.role_o = 'o' + else: + self.role_o = 'x' + + msg = xmpp.Message() + + invite = msg.NT.invite + invite.setNamespace('http://jabber.org/protocol/games') + + game = invite.NT.game + game.setAttr('var', 'http://jabber.org/protocol/games/tictactoe') + + x = xmpp.DataForm(typ='submit') + + game.addChild(node=x) + + self.send(msg) + + self.next_move_id = 1 + self.state = 'sent_invite' + + # received an invitation + def invited(self, msg): + invite = msg.getTag('invite', namespace='http://jabber.org/protocol/games') + game = invite.getTag('game') + x = game.getTag('x', namespace='jabber:x:data') + + form = xmpp.DataForm(node=x) + + if form.getField('role'): + self.role_o = form.getField('role').getValues()[0] + + if form.getField('rows'): + self.rows = int(form.getField('rows').getValues()[0]) + + if form.getField('cols'): + self.cols = int(form.getField('cols').getValues()[0]) + + # XXX 'strike' + + if not hasattr(self, 'rows'): + self.rows = 3 + + if not hasattr(self, 'cols'): + self.cols = 3 + + # the number of the move about to be made + self.next_move_id = 1 + + self.board = TicTacToeBoard(self, self.rows, self.cols) + + # accept the invitation, join the game + response = xmpp.Message() + + join = response.NT.join + join.setNamespace('http://jabber.org/protocol/games') + + self.send(response) + + if not hasattr(self, 'role_o') or self.role_o == 'x': + self.role_s = 'o' + self.role_o = 'x' + + self.their_turn() + else: + self.role_s = 'x' + self.role_o = 'o' + + self.our_turn() + + def is_my_turn(self): + return self.state == 'get_input' + + def received(self, msg): + # just sent an invitation, expecting a reply + if self.state == 'sent_invite': + if msg.getTag('join', namespace='http://jabber.org/protocol/games'): + self.board = TicTacToeBoard(self, self.rows, self.cols) + + if self.role_s == 'x': + self.our_turn() + else: + self.their_turn() + + return + + # ignore messages unless we're expecting a move + if self.state != 'waiting': + return + + turn = msg.getTag('turn', namespace='http://jabber.org/protocol/games') + + move = turn.getTag('move', namespace='http://jabber.org/protocol/games/tictactoe') + + row = int(move.getAttr('row')) + col = int(move.getAttr('col')) + id = int(move.getAttr('id')) + + if id != self.next_move_id: + print 'unexpected move id, lost a move somewhere?' + raise + + try: + self.board.mark(row, col, self.role_o) + except InvalidMove, e: + print 'received invalid move' + return + + # XXX check win conditions + + self.next_move_id += 1 + + self.our_turn() + + def our_turn(self): + self.state = 'get_input' + self.board.win.set_title(self.board.title + ': your turn') + + def their_turn(self): + self.state = 'waiting' + self.board.win.set_title(self.board.title + ': their turn') + + # called when the board receives input + def move(self, row, column): + try: + self.board.mark(row, column, self.role_s) + except InvalidMove, e: + print 'invalid move' + return + + self.send_move(row, column) + + # XXX check win conditions + + def send_move(self, row, column): + msg = xmpp.Message() + + turn = msg.NT.turn + turn.setNamespace('http://jabber.org/protocol/games') + + move = turn.NT.move + move.setNamespace('http://jabber.org/protocol/games/tictactoe') + + move.setAttr('row', str(row)) + move.setAttr('col', str(column)) + move.setAttr('id', str(self.next_move_id)) + + self.send(msg) + + self.next_move_id += 1 + + self.their_turn() + +class TicTacToeBoard: + def __init__(self, session, rows, cols): + self.session = session + + self.rows = rows + self.cols = cols + + self.board = [ [None] * self.cols for r in xrange(self.rows) ] + + self.setup_window() + + def setup_window(self): + self.win = gtk.Window() + + self.title = 'tic-tac-toe with %s' % self.session.jid + + self.win.set_title(self.title) + self.win.set_app_paintable(True) + + self.win.add_events(gdk.BUTTON_PRESS_MASK) + self.win.connect('button-press-event', self.clicked) + self.win.connect('expose-event', self.expose) + + self.win.show_all() + + def clicked(self, widget, event): + if not self.session.is_my_turn(): + return + + (height, width) = widget.get_size() + + # convert click co-ordinates to row and column + + row_height = height // self.rows + col_width = width // self.cols + + row = int(event.y // row_height) + 1 + column = int(event.x // col_width) + 1 + + self.session.move(row, column) + + def expose(self, widget, event): + win = widget.window + + cr = win.cairo_create() + + cr.set_source_rgb(1.0, 1.0, 1.0) + + cr.set_operator(cairo.OPERATOR_SOURCE) + cr.paint() + + (width, height) = widget.get_size() + + row_height = height // self.rows + col_width = width // self.cols + + for i in xrange(self.rows): + for j in xrange(self.cols): + if self.board[i][j] == 'x': + self.draw_x(cr, i, j, row_height, col_width) + elif self.board[i][j] == 'o': + self.draw_o(cr, i, j, row_height, col_width) + + def draw_x(self, cr, row, col, row_height, col_width): + cr.set_source_rgb(0, 0, 0) + + top = row_height * (row + 0.2) + bottom = row_height * (row + 0.8) + + left = col_width * (col + 0.2) + right = col_width * (col + 0.8) + + cr.set_line_width(row_height / 5) + + cr.move_to(left, top) + cr.line_to(right, bottom) + + cr.move_to(right, top) + cr.line_to(left, bottom) + + cr.stroke() + + def draw_o(self, cr, row, col, row_height, col_width): + cr.set_source_rgb(0, 0, 0) + + x = col_width * (col + 0.5) + y = row_height * (row + 0.5) + + cr.arc(x, y, row_height/4, 0, 2.0*3.2) # slightly further than 2*pi + + cr.set_line_width(row_height / 5) + cr.stroke() + + # mark a move on the board + def mark(self, row, column, player): + if self.board[row-1][column-1]: + raise InvalidMove + else: + self.board[row-1][column-1] = player + + self.win.queue_draw()