From 59ec92b2d7082b81a24eb9f818bd940540cbe4e3 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 26 Feb 2010 00:15:53 +0000 Subject: [PATCH] Imported a cut down mumble app --- mumble/MumbleCtlDbus.py | 330 +++++++++++++ mumble/MumbleCtlIce.py | 436 +++++++++++++++++ mumble/__init__.py | 14 + mumble/admin.py | 101 ++++ mumble/management/__init__.py | 22 + mumble/management/commands/__init__.py | 15 + mumble/management/commands/checkenv.py | 194 ++++++++ mumble/management/commands/mmrunserver.py | 21 + mumble/management/commands/mmshell.py | 21 + mumble/management/commands/mmsyncdb.py | 21 + mumble/management/server_detect.py | 135 ++++++ mumble/mctl.py | 128 +++++ mumble/mmobjects.py | 284 +++++++++++ mumble/models.py | 565 ++++++++++++++++++++++ mumble/murmurenvutils.py | 244 ++++++++++ mumble/utils.py | 30 ++ settings.py | 7 + 17 files changed, 2568 insertions(+) create mode 100644 mumble/MumbleCtlDbus.py create mode 100644 mumble/MumbleCtlIce.py create mode 100644 mumble/__init__.py create mode 100644 mumble/admin.py create mode 100644 mumble/management/__init__.py create mode 100644 mumble/management/commands/__init__.py create mode 100644 mumble/management/commands/checkenv.py create mode 100644 mumble/management/commands/mmrunserver.py create mode 100644 mumble/management/commands/mmshell.py create mode 100644 mumble/management/commands/mmsyncdb.py create mode 100644 mumble/management/server_detect.py create mode 100644 mumble/mctl.py create mode 100644 mumble/mmobjects.py create mode 100644 mumble/models.py create mode 100644 mumble/murmurenvutils.py create mode 100644 mumble/utils.py diff --git a/mumble/MumbleCtlDbus.py b/mumble/MumbleCtlDbus.py new file mode 100644 index 0000000..5684d36 --- /dev/null +++ b/mumble/MumbleCtlDbus.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009, withgod + * 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from PIL import Image +from struct import pack, unpack +from zlib import compress, decompress + +from mctl import MumbleCtlBase +from utils import ObjectInfo + +import dbus +from dbus.exceptions import DBusException + + +def MumbleCtlDbus( connstring ): + """ Choose the correct DBus handler (1.1.8 or legacy) to use. """ + + meta = dbus.Interface( dbus.SystemBus().get_object( connstring, '/' ), 'net.sourceforge.mumble.Meta' ); + + try: + meta.getVersion(); + except DBusException: + return MumbleCtlDbus_Legacy( connstring, meta ); + else: + return MumbleCtlDbus_118( connstring, meta ); + + +class MumbleCtlDbus_118(MumbleCtlBase): + method = "DBus"; + + def __init__( self, connstring, meta ): + self.dbus_base = connstring; + self.meta = meta; + + def _getDbusMeta( self ): + return self.meta; + + def _getDbusServerObject( self, srvid): + if srvid not in self.getBootedServers(): + raise SystemError, 'No murmur process with the given server ID (%d) is running and attached to system dbus under %s.' % ( srvid, self.meta ); + + return dbus.Interface( dbus.SystemBus().get_object( self.dbus_base, '/%d' % srvid ), 'net.sourceforge.mumble.Murmur' ); + + def getVersion( self ): + return MumbleCtlDbus_118.convertDbusTypeToNative( self.meta.getVersion() ); + + def getAllConf(self, srvid): + conf = self.meta.getAllConf(dbus.Int32(srvid)) + + info = {}; + for key in conf: + if key == "playername": + info['username'] = conf[key]; + else: + info[str(key)] = conf[key]; + return info; + + def getConf(self, srvid, key, value): + if key == "username": + key = "playername"; + + return self.meta.getConf(dbus.Int32( srvid ), key) + + def setConf(self, srvid, key, value): + if key == "username": + key = "playername"; + + self.meta.setConf(dbus.Int32( srvid ), key, value) + + def getDefaultConf(self): + conf = self.meta.getDefaultConf() + + info = {}; + for key in conf: + if key == "playername": + info['username'] = conf[key]; + else: + info[str(key)] = conf[key]; + return info; + + def start( self, srvid ): + self.meta.start( srvid ); + + def stop( self, srvid ): + self.meta.stop( srvid ); + + def isBooted( self, srvid ): + return bool( self.meta.isBooted( srvid ) ); + + def deleteServer( self, srvid ): + srvid = dbus.Int32( srvid ) + if self.meta.isBooted( srvid ): + self.meta.stop( srvid ) + + self.meta.deleteServer( srvid ) + + def newServer(self): + return self.meta.newServer() + + def registerPlayer(self, srvid, name, email, password): + mumbleid = int( self._getDbusServerObject(srvid).registerPlayer(name) ); + self.setRegistration( srvid, mumbleid, name, email, password ); + return mumbleid; + + def unregisterPlayer(self, srvid, mumbleid): + self._getDbusServerObject(srvid).unregisterPlayer(dbus.Int32( mumbleid )) + + def getChannels(self, srvid): + chans = self._getDbusServerObject(srvid).getChannels() + + ret = {}; + + for channel in chans: + ret[ channel[0] ] = ObjectInfo( + id = int(channel[0]), + name = str(channel[1]), + parent = int(channel[2]), + links = [ int(lnk) for lnk in channel[3] ], + ); + + return ret; + + def getPlayers(self, srvid): + players = self._getDbusServerObject(srvid).getPlayers(); + + ret = {}; + + for playerObj in players: + ret[ int(playerObj[0]) ] = ObjectInfo( + session = int( playerObj[0] ), + mute = bool( playerObj[1] ), + deaf = bool( playerObj[2] ), + suppress = bool( playerObj[3] ), + selfMute = bool( playerObj[4] ), + selfDeaf = bool( playerObj[5] ), + channel = int( playerObj[6] ), + userid = int( playerObj[7] ), + name = str( playerObj[8] ), + onlinesecs = int( playerObj[9] ), + bytespersec = int( playerObj[10] ) + ); + + return ret; + + def getRegisteredPlayers(self, srvid, filter = ''): + users = self._getDbusServerObject(srvid).getRegisteredPlayers( filter ); + ret = {}; + + for user in users: + ret[int(user[0])] = ObjectInfo( + userid = int( user[0] ), + name = unicode( user[1] ), + email = unicode( user[2] ), + pw = unicode( user[3] ) + ); + + return ret + + def getACL(self, srvid, channelid): + raw_acls, raw_groups, raw_inherit = self._getDbusServerObject(srvid).getACL(channelid) + + acls = [ ObjectInfo( + applyHere = bool(rule[0]), + applySubs = bool(rule[1]), + inherited = bool(rule[2]), + userid = int(rule[3]), + group = str(rule[4]), + allow = int(rule[5]), + deny = int(rule[6]), + ) + for rule in raw_acls + ]; + + groups = [ ObjectInfo( + name = str(group[0]), + inherited = bool(group[1]), + inherit = bool(group[2]), + inheritable = bool(group[3]), + add = [ int(usrid) for usrid in group[4] ], + remove = [ int(usrid) for usrid in group[5] ], + members = [ int(usrid) for usrid in group[6] ], + ) + for group in raw_groups + ]; + + return acls, groups, bool(raw_inherit); + + def setACL(self, srvid, channelid, acls, groups, inherit): + # Pack acl ObjectInfo into a tuple and send that over dbus + dbus_acls = [ + ( rule.applyHere, rule.applySubs, rule.inherited, rule.userid, rule.group, rule.allow, rule.deny ) + for rule in acls + ]; + + dbus_groups = [ + ( group.name, group.inherited, group.inherit, group.inheritable, group.add, group.remove, group.members ) + for group in groups + ]; + + return self._getDbusServerObject(srvid).setACL( channelid, dbus_acls, dbus_groups, inherit ); + + def getBootedServers(self): + return MumbleCtlDbus_118.convertDbusTypeToNative(self.meta.getBootedServers()) + + def getAllServers(self): + return MumbleCtlDbus_118.convertDbusTypeToNative(self.meta.getAllServers()) + + def setSuperUserPassword(self, srvid, value): + self.meta.setSuperUserPassword(dbus.Int32(srvid), value) + + def getRegistration(self, srvid, mumbleid): + user = self._getDbusServerObject(srvid).getRegistration(dbus.Int32(mumbleid)) + return ObjectInfo( + userid = mumbleid, + name = unicode(user[1]), + email = unicode(user[2]), + pw = '', + ); + + def setRegistration(self, srvid, mumbleid, name, email, password): + return MumbleCtlDbus_118.convertDbusTypeToNative( + self._getDbusServerObject(srvid).setRegistration(dbus.Int32(mumbleid), name, email, password) + ) + + def getTexture(self, srvid, mumbleid): + texture = self._getDbusServerObject(srvid).getTexture(dbus.Int32(mumbleid)); + + if len(texture) == 0: + raise ValueError( "No Texture has been set." ); + # this returns a list of bytes. + # first 4 bytes: Length of uncompressed string, rest: compressed data + orig_len = ( texture[0] << 24 ) | ( texture[1] << 16 ) | ( texture[2] << 8 ) | ( texture[3] ); + # convert rest to string and run decompress + bytestr = ""; + for byte in texture[4:]: + bytestr += pack( "B", int(byte) ); + decompressed = decompress( bytestr ); + # iterate over 4 byte chunks of the string + imgdata = ""; + for idx in range( 0, orig_len, 4 ): + # read 4 bytes = BGRA and convert to RGBA + bgra = unpack( "4B", decompressed[idx:idx+4] ); + imgdata += pack( "4B", bgra[2], bgra[1], bgra[0], bgra[3] ); + + # return an 600x60 RGBA image object created from the data + return Image.fromstring( "RGBA", ( 600, 60 ), imgdata); + + def setTexture(self, srvid, mumbleid, infile): + # open image, convert to RGBA, and resize to 600x60 + img = Image.open( infile ).convert( "RGBA" ).transform( ( 600, 60 ), Image.EXTENT, ( 0, 0, 600, 60 ) ); + # iterate over the list and pack everything into a string + bgrastring = ""; + for ent in list( img.getdata() ): + # ent is in RGBA format, but Murmur wants BGRA (ARGB inverse), so stuff needs + # to be reordered when passed to pack() + bgrastring += pack( "4B", ent[2], ent[1], ent[0], ent[3] ); + # compress using zlib + compressed = compress( bgrastring ); + # pack the original length in 4 byte big endian, and concat the compressed + # data to it to emulate qCompress(). + texture = pack( ">L", len(bgrastring) ) + compressed; + # finally call murmur and set the texture + self._getDbusServerObject(srvid).setTexture(dbus.Int32( mumbleid ), texture) + + def verifyPassword( self, srvid, username, password ): + player = self.getRegisteredPlayers( srvid, username ); + if not player: + return -2; + + ok = MumbleCtlDbus_118.convertDbusTypeToNative( + self._getDbusServerObject(srvid).verifyPassword( dbus.Int32( player[0].userid ), password ) + ); + + if ok: + return player[0].userid; + else: + return -1; + + @staticmethod + def convertDbusTypeToNative(data): + #i know dbus.* type is extends python native type. + #but dbus.* type is not native type. it's not good transparent for using Ice/Dbus. + ret = None + + if isinstance(data, tuple) or type(data) is data.__class__ is dbus.Array or data.__class__ is dbus.Struct: + ret = [] + for x in data: + ret.append(MumbleCtlDbus_118.convertDbusTypeToNative(x)) + elif data.__class__ is dbus.Dictionary: + ret = {} + for x in data.items(): + ret[MumbleCtlDbus_118.convertDbusTypeToNative(x[0])] = MumbleCtlDbus_118.convertDbusTypeToNative(x[1]) + else: + if data.__class__ is dbus.Boolean: + ret = bool(data) + elif data.__class__ is dbus.String: + ret = unicode(data) + elif data.__class__ is dbus.Int32 or data.__class__ is dbus.UInt32: + ret = int(data) + elif data.__class__ is dbus.Byte: + ret = int(data) + return ret + + +class MumbleCtlDbus_Legacy( MumbleCtlDbus_118 ): + def getVersion( self ): + return ( 1, 1, 4, u"1.1.4" ); + + def setRegistration(self, srvid, mumbleid, name, email, password): + return MumbleCtlDbus_118.convertDbusTypeToNative( + self._getDbusServerObject(srvid).updateRegistration( ( dbus.Int32(mumbleid), name, email, password ) ) + ) + + + + diff --git a/mumble/MumbleCtlIce.py b/mumble/MumbleCtlIce.py new file mode 100644 index 0000000..a8df6ab --- /dev/null +++ b/mumble/MumbleCtlIce.py @@ -0,0 +1,436 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009, withgod + * 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from os.path import exists +from PIL import Image +from struct import pack, unpack +from zlib import compress, decompress + +from mctl import MumbleCtlBase + +from utils import ObjectInfo + +import Ice + + +def protectDjangoErrPage( func ): + """ Catch and reraise Ice exceptions to prevent the Django page from failing. + + Since I need to "import Murmur", Django would try to read a murmur.py file + which doesn't exist, and thereby produce an IndexError exception. This method + erases the exception's traceback, preventing Django from trying to read any + non-existant files and borking. + """ + + def protection_wrapper( *args, **kwargs ): + """ Call the original function and catch Ice exceptions. """ + try: + return func( *args, **kwargs ); + except Ice.Exception, e: + raise e; + protection_wrapper.innerfunc = func + + return protection_wrapper; + + + +@protectDjangoErrPage +def MumbleCtlIce( connstring, slicefile ): + """ Choose the correct Ice handler to use (1.1.8 or 1.2.x), and make sure the + Murmur version matches the slice Version. + """ + + try: + import Murmur + + except ImportError: + if not slicefile: + raise EnvironmentError( "You didn't configure a slice file. Please set the SLICE variable in settings.py." ) + + if not exists( slicefile ): + raise EnvironmentError( "The slice file does not exist: '%s' - please check the settings." % slicefile ) + + if " " in slicefile: + raise EnvironmentError( "You have a space char in your Slice path. This will confuse Ice, please check." ) + + if not slicefile.endswith( ".ice" ): + raise EnvironmentError( "The slice file name MUST end with '.ice'." ) + + Ice.loadSlice( slicefile ) + + import Murmur + + ice = Ice.initialize() + prx = ice.stringToProxy( connstring.encode("utf-8") ) + meta = Murmur.MetaPrx.checkedCast(prx) + + murmurversion = meta.getVersion()[:3] + + if murmurversion == (1, 1, 8): + return MumbleCtlIce_118( connstring, meta ); + + elif murmurversion[:2] == (1, 2): + return MumbleCtlIce_120( connstring, meta ); + + +class MumbleCtlIce_118(MumbleCtlBase): + method = "ICE"; + + def __init__( self, connstring, meta ): + self.proxy = connstring; + self.meta = meta; + + @protectDjangoErrPage + def _getIceServerObject(self, srvid): + return self.meta.getServer(srvid); + + @protectDjangoErrPage + def getBootedServers(self): + ret = [] + for x in self.meta.getBootedServers(): + ret.append(x.id()) + return ret + + @protectDjangoErrPage + def getVersion( self ): + return self.meta.getVersion(); + + @protectDjangoErrPage + def getAllServers(self): + ret = [] + for x in self.meta.getAllServers(): + ret.append(x.id()) + return ret + + @protectDjangoErrPage + def getRegisteredPlayers(self, srvid, filter = ''): + users = self._getIceServerObject(srvid).getRegisteredPlayers( filter.encode( "UTF-8" ) ) + ret = {}; + + for user in users: + ret[user.playerid] = ObjectInfo( + userid = int( user.playerid ), + name = unicode( user.name, "utf8" ), + email = unicode( user.email, "utf8" ), + pw = unicode( user.pw, "utf8" ) + ); + + return ret + + @protectDjangoErrPage + def getChannels(self, srvid): + return self._getIceServerObject(srvid).getChannels(); + + @protectDjangoErrPage + def getPlayers(self, srvid): + users = self._getIceServerObject(srvid).getPlayers() + + ret = {}; + + for useridx in users: + user = users[useridx]; + ret[ user.session ] = ObjectInfo( + session = user.session, + userid = user.playerid, + mute = user.mute, + deaf = user.deaf, + suppress = user.suppressed, + selfMute = user.selfMute, + selfDeaf = user.selfDeaf, + channel = user.channel, + name = user.name, + onlinesecs = user.onlinesecs, + bytespersec = user.bytespersec + ); + + return ret; + + @protectDjangoErrPage + def getDefaultConf(self): + return self.setUnicodeFlag(self.meta.getDefaultConf()) + + @protectDjangoErrPage + def getAllConf(self, srvid): + conf = self.setUnicodeFlag(self._getIceServerObject(srvid).getAllConf()) + + info = {}; + for key in conf: + if key == "playername": + info['username'] = conf[key]; + else: + info[str(key)] = conf[key]; + return info; + + @protectDjangoErrPage + def newServer(self): + return self.meta.newServer().id() + + @protectDjangoErrPage + def isBooted( self, srvid ): + return bool( self._getIceServerObject(srvid).isRunning() ); + + @protectDjangoErrPage + def start( self, srvid ): + self._getIceServerObject(srvid).start(); + + @protectDjangoErrPage + def stop( self, srvid ): + self._getIceServerObject(srvid).stop(); + + @protectDjangoErrPage + def deleteServer( self, srvid ): + if self._getIceServerObject(srvid).isRunning(): + self._getIceServerObject(srvid).stop() + self._getIceServerObject(srvid).delete() + + @protectDjangoErrPage + def setSuperUserPassword(self, srvid, value): + self._getIceServerObject(srvid).setSuperuserPassword( value.encode( "UTF-8" ) ) + + @protectDjangoErrPage + def getConf(self, srvid, key): + if key == "username": + key = "playername"; + + return self._getIceServerObject(srvid).getConf( key ) + + @protectDjangoErrPage + def setConf(self, srvid, key, value): + if key == "username": + key = "playername"; + if value is None: + value = '' + self._getIceServerObject(srvid).setConf( key, value.encode( "UTF-8" ) ) + + @protectDjangoErrPage + def registerPlayer(self, srvid, name, email, password): + mumbleid = self._getIceServerObject(srvid).registerPlayer( name.encode( "UTF-8" ) ) + self.setRegistration( srvid, mumbleid, name, email, password ); + return mumbleid; + + @protectDjangoErrPage + def unregisterPlayer(self, srvid, mumbleid): + self._getIceServerObject(srvid).unregisterPlayer(mumbleid) + + @protectDjangoErrPage + def getRegistration(self, srvid, mumbleid): + user = self._getIceServerObject(srvid).getRegistration(mumbleid) + return ObjectInfo( + userid = mumbleid, + name = user.name, + email = user.email, + pw = '', + ); + + @protectDjangoErrPage + def setRegistration(self, srvid, mumbleid, name, email, password): + import Murmur + user = Murmur.Player() + user.playerid = mumbleid; + user.name = name.encode( "UTF-8" ) + user.email = email.encode( "UTF-8" ) + user.pw = password.encode( "UTF-8" ) + # update*r*egistration r is lowercase... + return self._getIceServerObject(srvid).updateregistration(user) + + @protectDjangoErrPage + def getACL(self, srvid, channelid): + # need to convert acls to say "userid" instead of "playerid". meh. + raw_acls, raw_groups, raw_inherit = self._getIceServerObject(srvid).getACL(channelid) + + acls = [ ObjectInfo( + applyHere = rule.applyHere, + applySubs = rule.applySubs, + inherited = rule.inherited, + userid = rule.playerid, + group = rule.group, + allow = rule.allow, + deny = rule.deny, + ) + for rule in raw_acls + ]; + + return acls, raw_groups, raw_inherit; + + @protectDjangoErrPage + def setACL(self, srvid, channelid, acls, groups, inherit): + import Murmur + + ice_acls = []; + + for rule in acls: + ice_rule = Murmur.ACL(); + ice_rule.applyHere = rule.applyHere; + ice_rule.applySubs = rule.applySubs; + ice_rule.inherited = rule.inherited; + ice_rule.playerid = rule.userid; + ice_rule.group = rule.group; + ice_rule.allow = rule.allow; + ice_rule.deny = rule.deny; + ice_acls.append(ice_rule); + + return self._getIceServerObject(srvid).setACL( channelid, ice_acls, groups, inherit ); + + @protectDjangoErrPage + def getTexture(self, srvid, mumbleid): + texture = self._getIceServerObject(srvid).getTexture(mumbleid) + if len(texture) == 0: + raise ValueError( "No Texture has been set." ); + # this returns a list of bytes. + decompressed = decompress( texture ); + # iterate over 4 byte chunks of the string + imgdata = ""; + for idx in range( 0, len(decompressed), 4 ): + # read 4 bytes = BGRA and convert to RGBA + # manual wrote getTexture returns "Textures are stored as zlib compress()ed 600x60 32-bit RGBA data." + # http://mumble.sourceforge.net/slice/Murmur/Server.html#getTexture + # but return values BGRA X( + bgra = unpack( "4B", decompressed[idx:idx+4] ); + imgdata += pack( "4B", bgra[2], bgra[1], bgra[0], bgra[3] ); + + # return an 600x60 RGBA image object created from the data + return Image.fromstring( "RGBA", ( 600, 60 ), imgdata ); + + @protectDjangoErrPage + def setTexture(self, srvid, mumbleid, infile): + # open image, convert to RGBA, and resize to 600x60 + img = Image.open( infile ).convert( "RGBA" ).transform( ( 600, 60 ), Image.EXTENT, ( 0, 0, 600, 60 ) ); + # iterate over the list and pack everything into a string + bgrastring = ""; + for ent in list( img.getdata() ): + # ent is in RGBA format, but Murmur wants BGRA (ARGB inverse), so stuff needs + # to be reordered when passed to pack() + bgrastring += pack( "4B", ent[2], ent[1], ent[0], ent[3] ); + # compress using zlib + compressed = compress( bgrastring ); + # pack the original length in 4 byte big endian, and concat the compressed + # data to it to emulate qCompress(). + texture = pack( ">L", len(bgrastring) ) + compressed; + # finally call murmur and set the texture + self._getIceServerObject(srvid).setTexture(mumbleid, texture) + + @protectDjangoErrPage + def verifyPassword(self, srvid, username, password): + return self._getIceServerObject(srvid).verifyPassword(username, password); + + @staticmethod + def setUnicodeFlag(data): + ret = '' + if isinstance(data, tuple) or isinstance(data, list) or isinstance(data, dict): + ret = {} + for key in data.keys(): + ret[MumbleCtlIce_118.setUnicodeFlag(key)] = MumbleCtlIce_118.setUnicodeFlag(data[key]) + else: + ret = unicode(data, 'utf-8') + + return ret + + + + +class MumbleCtlIce_120(MumbleCtlIce_118): + @protectDjangoErrPage + def getRegisteredPlayers(self, srvid, filter = ''): + users = self._getIceServerObject( srvid ).getRegisteredUsers( filter.encode( "UTF-8" ) ) + ret = {}; + + for id in users: + ret[id] = ObjectInfo( + userid = id, + name = unicode( users[id], "utf8" ), + email = '', + pw = '' + ); + + return ret + + @protectDjangoErrPage + def getPlayers(self, srvid): + return self._getIceServerObject(srvid).getUsers(); + + @protectDjangoErrPage + def registerPlayer(self, srvid, name, email, password): + # To get the real values of these ENUM entries, try + # Murmur.UserInfo.UserX.value + import Murmur + user = { + Murmur.UserInfo.UserName: name.encode( "UTF-8" ), + Murmur.UserInfo.UserEmail: email.encode( "UTF-8" ), + Murmur.UserInfo.UserPassword: password.encode( "UTF-8" ), + }; + return self._getIceServerObject(srvid).registerUser( user ); + + @protectDjangoErrPage + def unregisterPlayer(self, srvid, mumbleid): + self._getIceServerObject(srvid).unregisterUser(mumbleid) + + @protectDjangoErrPage + def getRegistration(self, srvid, mumbleid): + reg = self._getIceServerObject( srvid ).getRegistration( mumbleid ) + user = ObjectInfo( userid=mumbleid, name="", email="", comment="", hash="", pw="" ); + import Murmur + if Murmur.UserInfo.UserName in reg: user.name = reg[Murmur.UserInfo.UserName]; + if Murmur.UserInfo.UserEmail in reg: user.email = reg[Murmur.UserInfo.UserEmail]; + if Murmur.UserInfo.UserComment in reg: user.comment = reg[Murmur.UserInfo.UserComment]; + if Murmur.UserInfo.UserHash in reg: user.hash = reg[Murmur.UserInfo.UserHash]; + return user; + + @protectDjangoErrPage + def setRegistration(self, srvid, mumbleid, name, email, password): + import Murmur + user = { + Murmur.UserInfo.UserName: name.encode( "UTF-8" ), + Murmur.UserInfo.UserEmail: email.encode( "UTF-8" ), + Murmur.UserInfo.UserPassword: password.encode( "UTF-8" ), + }; + return self._getIceServerObject( srvid ).updateRegistration( mumbleid, user ) + + @protectDjangoErrPage + def getAllConf(self, srvid): + conf = self.setUnicodeFlag(self._getIceServerObject(srvid).getAllConf()) + + info = {}; + for key in conf: + if key == "playername" and conf[key]: + # Buggy database transition from 1.1.8 -> 1.2.0 + # Store username as "username" field and set playername field to empty + info['username'] = conf[key]; + self.setConf( srvid, "playername", "" ); + self.setConf( srvid, "username", conf[key] ); + else: + info[str(key)] = conf[key]; + + return info; + + @protectDjangoErrPage + def getConf(self, srvid, key): + return self._getIceServerObject(srvid).getConf( key ) + + @protectDjangoErrPage + def setConf(self, srvid, key, value): + if value is None: + value = '' + self._getIceServerObject(srvid).setConf( key, value.encode( "UTF-8" ) ) + + @protectDjangoErrPage + def getACL(self, srvid, channelid): + return self._getIceServerObject(srvid).getACL(channelid) + + @protectDjangoErrPage + def setACL(self, srvid, channelid, acls, groups, inherit): + return self._getIceServerObject(srvid).setACL( channelid, acls, groups, inherit ); + diff --git a/mumble/__init__.py b/mumble/__init__.py new file mode 100644 index 0000000..cb703eb --- /dev/null +++ b/mumble/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" diff --git a/mumble/admin.py b/mumble/admin.py new file mode 100644 index 0000000..1e437ef --- /dev/null +++ b/mumble/admin.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ + +#from mumble.forms import MumbleAdminForm, MumbleUserAdminForm +from mumble.models import Mumble, MumbleUser + +class MumbleAdmin(admin.ModelAdmin): + """ Specification for the "Server administration" admin section. """ + + list_display = [ 'name', 'addr', 'port', 'get_booted', 'get_is_public', + 'get_users_regged', 'get_users_online', 'get_channel_count' ]; + list_filter = [ 'addr' ]; + search_fields = [ 'name', 'addr', 'port' ]; + ordering = [ 'name' ]; + #form = MumbleAdminForm; + + + def get_booted( self, obj ): + return obj.booted + + get_booted.short_description = _('Boot Server') + get_booted.boolean = True + + def get_users_regged( self, obj ): + """ Populates the "Registered users" column. """ + if obj.booted: + return obj.users_regged; + else: + return '-'; + + get_users_regged.short_description = _( 'Registered users' ); + + + def get_users_online( self, obj ): + """ Populates the "Online users" column. """ + if obj.booted: + return obj.users_online; + else: + return '-'; + + get_users_online.short_description = _( 'Online users' ); + + + def get_channel_count( self, obj ): + """ Populates the "Channel Count" column. """ + if obj.booted: + return obj.channel_cnt; + else: + return '-'; + + get_channel_count.short_description = _( 'Channel count' ); + + + def get_is_public( self, obj ): + """ Populates the "Public" column. """ + if obj.booted: + if obj.is_public: + return _( 'Yes' ); + else: + return _( 'No' ); + else: + return '-'; + + get_is_public.short_description = _( 'Public' ); + + +class MumbleUserAdmin(admin.ModelAdmin): + """ Specification for the "Registered users" admin section. """ + + list_display = [ 'owner', 'server', 'name', 'get_acl_admin' ]; + list_filter = [ 'server' ]; + search_fields = [ 'owner__username', 'name' ]; + ordering = [ 'owner__username' ]; + + #form = MumbleUserAdminForm + + def get_acl_admin( self, obj ): + return obj.aclAdmin + + get_acl_admin.short_description = _('Admin on root channel') + get_acl_admin.boolean = True + + +admin.site.register( Mumble, MumbleAdmin ); +admin.site.register( MumbleUser, MumbleUserAdmin ); diff --git a/mumble/management/__init__.py b/mumble/management/__init__.py new file mode 100644 index 0000000..b2c1b0f --- /dev/null +++ b/mumble/management/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from server_detect import find_existing_instances +from django.db.models import signals +from mumble import models + +signals.post_syncdb.connect( find_existing_instances, sender=models ); + diff --git a/mumble/management/commands/__init__.py b/mumble/management/commands/__init__.py new file mode 100644 index 0000000..6ceaf67 --- /dev/null +++ b/mumble/management/commands/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" diff --git a/mumble/management/commands/checkenv.py b/mumble/management/commands/checkenv.py new file mode 100644 index 0000000..a7867b2 --- /dev/null +++ b/mumble/management/commands/checkenv.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import os, Ice + +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.conf import settings + +from mumble.models import Mumble + + +class TestFailed( Exception ): + pass; + +class Command( BaseCommand ): + def handle(self, **options): + self.check_slice(); + self.check_rootdir(); + self.check_dbase(); + self.check_sites(); + self.check_mumbles(); + self.check_admins(); + self.check_secret_key(); + + def check_slice( self ): + print "Checking slice file...", + if settings.SLICE is None: + raise TestFailed( "You don't have set the SLICE variable in settings.py." ) + + if " " in settings.SLICE: + raise TestFailed( "You have a space char in your Slice path. This will confuse Ice, please check." ) + + if not settings.SLICE.endswith( ".ice" ): + raise TestFailed( "The slice file name MUST end with '.ice'." ) + + try: + fd = open( settings.SLICE, "rb" ) + slice = fd.read() + fd.close() + except IOError, err: + raise TestFailed( "Failed opening the slice file: %s" % err ) + + import Ice + Ice.loadSlice( settings.SLICE ) + + print "[ OK ]" + + def check_rootdir( self ): + print "Checking root directory access...", + if not os.path.exists( settings.MUMBLE_DJANGO_ROOT ): + raise TestFailed( "The mumble-django root directory does not exist." ); + + elif settings.DATABASE_ENGINE != "sqlite3": + print "not using sqlite [ OK ]" + + else: + statinfo = os.stat( settings.MUMBLE_DJANGO_ROOT ); + + if statinfo.st_uid == 0: + raise TestFailed( + "The mumble-django root directory belongs to user root. This is " + "most certainly not what you want because it will prevent your " + "web server from being able to write to the database. Please check." ); + + elif not os.access( settings.MUMBLE_DJANGO_ROOT, os.W_OK ): + raise TestFailed( "The mumble-django root directory is not writable." ); + + else: + print "[ OK ]"; + + def check_dbase( self ): + print "Checking database access...", + if settings.DATABASE_ENGINE == "sqlite3": + if not os.path.exists( settings.DATABASE_NAME ): + raise TestFailed( "database does not exist. Have you run syncdb yet?" ); + + else: + statinfo = os.stat( settings.DATABASE_NAME ); + + if statinfo.st_uid == 0: + raise TestFailed( + "the database file belongs to root. This is most certainly not what " + "you want because it will prevent your web server from being able " + "to write to it. Please check." ); + + elif not os.access( settings.DATABASE_NAME, os.W_OK ): + raise TestFailed( "database file is not writable." ); + + else: + print "[ OK ]"; + + else: + print "not using sqlite, so I can't check."; + + + def check_sites( self ): + print "Checking URL configuration...", + + try: + site = Site.objects.get_current(); + + except Site.DoesNotExist: + try: + sid = settings.SITE_ID + except AttributeError: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured( + "You're using the Django \"sites framework\" without having set the SITE_ID " + "setting. Create a site in your database and rerun this command to fix this error.") + else: + print( "none set.\n" + "Please enter the domain where Mumble-Django is reachable." ); + dom = raw_input( "> " ).strip(); + site = Site( id=sid, name=dom, domain=dom ); + site.save(); + + if site.domain == 'example.com': + print( "still the default.\n" + "The domain is configured as example.com, which is the default but does not make sense. " + "Please enter the domain where Mumble-Django is reachable." ); + + site.domain = raw_input( "> " ).strip(); + site.save(); + + print site.domain, "[ OK ]"; + + + def check_admins( self ): + print "Checking if an Admin user exists...", + + for user in User.objects.all(): + if user.is_superuser: + print "[ OK ]"; + return; + + raise TestFailed( "" + "No admin user exists, so you won't be able to log in to the admin system. You " + "should run `./manage.py createsuperuser` to create one." ); + + + def check_mumbles( self ): + print "Checking Murmur instances...", + + mm = Mumble.objects.all(); + + if mm.count() == 0: + raise TestFailed( + "no Mumble servers are configured, you might want to run " + "`./manage.py syncdb` to run an auto detection." ); + + else: + for mumble in mm: + try: + mumble.getCtl(); + except Ice.Exception, err: + raise TestFailed( + "Connecting to Murmur `%s` (%s) failed: %s" % ( mumble.name, mumble.dbus, err ) + ); + print "[ OK ]"; + + def check_secret_key( self ): + print "Checking SECRET_KEY...", + + blacklist = ( 'u-mp185msk#z4%s(do2^5405)y5d!9adbn92)apu_p^qvqh10v', ); + + if settings.SECRET_KEY in blacklist: + raise TestFailed( + "Your SECRET_KEY setting matches one of the keys that were put in the settings.py " + "file shipped with Mumble-Django, which means your SECRET_KEY is all but secret. " + "You should change the setting, or run gen_secret_key.sh to do it for you." + ); + else: + print "[ OK ]"; + + + + + + diff --git a/mumble/management/commands/mmrunserver.py b/mumble/management/commands/mmrunserver.py new file mode 100644 index 0000000..b47bfe7 --- /dev/null +++ b/mumble/management/commands/mmrunserver.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from django.core.management.commands.runserver import Command as OrigCommand +from mumble.murmurenvutils import MumbleCommandWrapper + +class Command( MumbleCommandWrapper, OrigCommand ): + pass diff --git a/mumble/management/commands/mmshell.py b/mumble/management/commands/mmshell.py new file mode 100644 index 0000000..fcf35c9 --- /dev/null +++ b/mumble/management/commands/mmshell.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from django.core.management.commands.shell import Command as OrigCommand +from mumble.murmurenvutils import MumbleCommandWrapper_noargs + +class Command( MumbleCommandWrapper_noargs, OrigCommand ): + pass diff --git a/mumble/management/commands/mmsyncdb.py b/mumble/management/commands/mmsyncdb.py new file mode 100644 index 0000000..2611198 --- /dev/null +++ b/mumble/management/commands/mmsyncdb.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from django.core.management.commands.syncdb import Command as OrigCommand +from mumble.murmurenvutils import MumbleCommandWrapper_noargs + +class Command( MumbleCommandWrapper_noargs, OrigCommand ): + pass diff --git a/mumble/management/server_detect.py b/mumble/management/server_detect.py new file mode 100644 index 0000000..5c033d5 --- /dev/null +++ b/mumble/management/server_detect.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import os + +from django.conf import settings + +from mumble import models +from mumble.mctl import MumbleCtlBase + + +def find_in_dicts( keys, conf, default, valueIfNotFound=None ): + if not isinstance( keys, tuple ): + keys = ( keys, ); + + for keyword in keys: + if keyword in conf: + return conf[keyword]; + + for keyword in keys: + keyword = keyword.lower(); + if keyword in default: + return default[keyword]; + + return valueIfNotFound; + + +def find_existing_instances( **kwargs ): + if "verbosity" in kwargs: + v = kwargs['verbosity']; + else: + v = 1; + + if v > 1: + print "Starting Mumble servers and players detection now."; + + triedEnviron = False; + online = False; + while not online: + if not triedEnviron and 'MURMUR_CONNSTR' in os.environ: + dbusName = os.environ['MURMUR_CONNSTR']; + triedEnviron = True; + if v > 1: + print "Trying environment setting", dbusName; + else: + print "--- Murmur connection info ---" + print " 1) DBus -- net.sourceforge.mumble.murmur" + print " 2) ICE -- Meta:tcp -h 127.0.0.1 -p 6502" + print "Enter 1 or 2 for the defaults above, nothing to skip Server detection," + print "and if the defaults do not fit your needs, enter the correct string." + print "Whether to use DBus or ICE will be detected automatically from the" + print "string's format." + print + + dbusName = raw_input( "Service string: " ).strip(); + + if not dbusName: + if v: + print 'Be sure to run "python manage.py syncdb" with Murmur running before' + print "trying to use this app! Otherwise, existing Murmur servers won't be" + print 'configurable!'; + return False; + elif dbusName == "1": + dbusName = "net.sourceforge.mumble.murmur"; + elif dbusName == "2": + dbusName = "Meta:tcp -h 127.0.0.1 -p 6502"; + + try: + ctl = MumbleCtlBase.newInstance( dbusName, settings.SLICE ); + except Exception, instance: + if v: + print "Unable to connect using name %s. The error was:" % dbusName; + print instance; + print + else: + online = True; + if v > 1: + print "Successfully connected to Murmur via connection string %s, using %s." % ( dbusName, ctl.method ); + + servIDs = ctl.getAllServers(); + + for id in servIDs: + if v > 1: + print "Checking Murmur instance with id %d." % id; + # first check that the server has not yet been inserted into the DB + try: + instance = models.Mumble.objects.get( dbus=dbusName, srvid=id ); + except models.Mumble.DoesNotExist: + values = { + "srvid": id, + "dbus": dbusName, + } + + if v > 1: + print "Found new Murmur instance %d on bus '%s'... " % ( id, dbusName ), + + # now create a model for the record set. + instance = models.Mumble( **values ); + else: + if v > 1: + print "Syncing Murmur instance... ", + + instance.configureFromMurmur(); + + if v > 1: + print instance.name; + + instance.save( dontConfigureMurmur=True ); + + # Now search for players on this server that have not yet been registered + if instance.booted: + if v > 1: + print "Looking for registered Players on Server id %d." % id; + instance.readUsersFromMurmur( verbose=v ); + elif v > 1: + print "This server is not running, can't sync players."; + + if v > 1: + print "Successfully finished Servers and Players detection."; + return True; + + diff --git a/mumble/mctl.py b/mumble/mctl.py new file mode 100644 index 0000000..502aff1 --- /dev/null +++ b/mumble/mctl.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009, withgod + * 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import re + +class MumbleCtlBase (object): + """ This class defines the base interface that the Mumble model expects. """ + + cache = {}; + + def getAllConf(self, srvid): + raise NotImplementedError( "mctl::getAllConf" ); + + def getVersion( self ): + raise NotImplementedError( "mctl::getVersion" ); + + def getConf(self, srvid, key): + raise NotImplementedError( "mctl::getConf" ); + + def setConf(self, srvid, key, value): + raise NotImplementedError( "mctl::setConf" ); + + def getDefaultConf(self): + raise NotImplementedError( "mctl::getDefaultConf" ); + + def newServer(self): + raise NotImplementedError( "mctl::newServer" ); + + def setSuperUserPassword(self, srvid, value): + raise NotImplementedError( "mctl::setSuperUserPassword" ); + + def start(self, srvid): + raise NotImplementedError( "mctl::start" ); + + def stop(self, srvid): + raise NotImplementedError( "mctl::stop" ); + + def isBooted(self, srvid): + raise NotImplementedError( "mctl::isBooted" ); + + def deleteServer(self, srvid): + raise NotImplementedError( "mctl::deleteServer" ); + + def getPlayers(self, srvid): + raise NotImplementedError( "mctl::getPlayers" ); + + def getRegisteredPlayers(self, srvid, filter): + raise NotImplementedError( "mctl::getRegisteredPlayers" ); + + def getChannels(self, srvid): + raise NotImplementedError( "mctl::getChannels" ); + + def registerPlayer(self, srvid, name, email, password): + raise NotImplementedError( "mctl::registerPlayer" ); + + def getRegistration(self, srvid, mumbleid): + raise NotImplementedError( "mctl::getRegistration" ); + + def setRegistration(self, srvid, mumbleid, name, email, password): + raise NotImplementedError( "mctl::setRegistration" ); + + def unregisterPlayer(self, srvid, mumbleid): + raise NotImplementedError( "mctl::unregisterPlayer" ); + + def getBootedServers(self): + raise NotImplementedError( "mctl::getBootedServers" ); + + def getAllServers(self): + raise NotImplementedError( "mctl::getAllServers" ); + + def getACL(self, srvid, channelid): + raise NotImplementedError( "mctl::getACL" ); + + def setACL(self, srvid, channelid, acl, groups, inherit): + raise NotImplementedError( "mctl::setACL" ); + + def getTexture(self, srvid, mumbleid): + raise NotImplementedError( "mctl::getTexture" ); + + def setTexture(self, srvid, mumbleid, infile): + raise NotImplementedError( "mctl::setTexture" ); + + def verifyPassword( self, srvid, username, password ): + raise NotImplementedError( "mctl::verifyPassword" ); + + @staticmethod + def newInstance( connstring, slicefile ): + """ Create a new CTL object for the given connstring. """ + + # check cache + if connstring in MumbleCtlBase.cache: + return MumbleCtlBase.cache[connstring]; + + # connstring defines whether to connect via ICE or DBus. + # Dbus service names: some.words.divided.by.periods + # ICE specs are WAY more complex, so if DBus doesn't match, use ICE. + rd = re.compile( r'^(\w+\.)*\w+$' ); + + if rd.match( connstring ): + from MumbleCtlDbus import MumbleCtlDbus + ctl = MumbleCtlDbus( connstring ) + else: + from MumbleCtlIce import MumbleCtlIce + ctl = MumbleCtlIce( connstring, slicefile ) + + MumbleCtlBase.cache[connstring] = ctl; + return ctl; + + @staticmethod + def clearCache(): + MumbleCtlBase.cache = {}; + + + diff --git a/mumble/mmobjects.py b/mumble/mmobjects.py new file mode 100644 index 0000000..3c01e79 --- /dev/null +++ b/mumble/mmobjects.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import datetime +from time import time +from os.path import join + +from django.utils.http import urlquote +from django.conf import settings + +def cmp_names( left, rite ): + """ Compare two objects by their name property. """ + return cmp( left.name, rite.name ); + + +class mmChannel( object ): + """ Represents a channel in Murmur. """ + + def __init__( self, server, channel_obj, parent_chan = None ): + self.server = server; + self.players = list(); + self.subchans = list(); + self.linked = list(); + + self.channel_obj = channel_obj; + self.chanid = channel_obj.id; + + self.parent = parent_chan; + if self.parent is not None: + self.parent.subchans.append( self ); + + self._acl = None; + + # Lookup unknown attributes in self.channel_obj to automatically include Murmur's fields + def __getattr__( self, key ): + if hasattr( self.channel_obj, key ): + return getattr( self.channel_obj, key ); + else: + raise AttributeError( "'%s' object has no attribute '%s'" % ( self.__class__.__name__, key ) ); + + def parent_channels( self ): + """ Return the names of this channel's parents in the channel tree. """ + if self.parent is None or self.parent.is_server or self.parent.chanid == 0: + return []; + return self.parent.parent_channels() + [self.parent.name]; + + + def getACL( self ): + """ Retrieve the ACL for this channel. """ + if not self._acl: + self._acl = mmACL( self, self.server.ctl.getACL( self.server.srvid, self.chanid ) ); + + return self._acl; + + acl = property( getACL, doc=getACL.__doc__ ); + + + is_server = False; + is_channel = True; + is_player = False; + + + playerCount = property( + lambda self: len( self.players ) + sum( [ chan.playerCount for chan in self.subchans ] ), + doc="The number of players in this channel." + ); + + id = property( + lambda self: "channel_%d"%self.chanid, + doc="A string ready to be used in an id property of an HTML tag." + ); + + top_or_not_empty = property( + lambda self: self.parent is None or self.parent.chanid == 0 or self.playerCount > 0, + doc="True if this channel needs to be shown because it is root, a child of root, or has players." + ); + + show = property( lambda self: settings.SHOW_EMPTY_SUBCHANS or self.top_or_not_empty ); + + def __str__( self ): + return '' % ( self.name, self.chanid ); + + def sort( self ): + """ Sort my subchannels and players, and then iterate over them and sort them recursively. """ + self.subchans.sort( cmp_names ); + self.players.sort( cmp_names ); + for subc in self.subchans: + subc.sort(); + + def visit( self, callback, lvl = 0 ): + """ Call callback on myself, then visit my subchans, then my players. """ + callback( self, lvl ); + for subc in self.subchans: + subc.visit( callback, lvl + 1 ); + for plr in self.players: + plr.visit( callback, lvl + 1 ); + + + def getURL( self, for_user = None ): + """ Create an URL to connect to this channel. The URL is of the form + mumble://username@host:port/parentchans/self.name + """ + userstr = ""; + + if for_user is not None: + userstr = "%s@" % for_user.name; + + versionstr = "version=%d.%d.%d" % tuple(self.server.version[0:3]); + + # create list of all my parents and myself + chanlist = self.parent_channels() + [self.name]; + # urlencode channel names + chanlist = [ urlquote( chan ) for chan in chanlist ]; + # create a path by joining the channel names + chanpath = join( *chanlist ); + + if self.server.port != settings.MUMBLE_DEFAULT_PORT: + return "mumble://%s%s:%d/%s?%s" % ( userstr, self.server.addr, self.server.port, chanpath, versionstr ); + + return "mumble://%s%s/%s?%s" % ( userstr, self.server.addr, chanpath, versionstr ); + + connecturl = property( getURL, doc="A convenience wrapper for getURL." ); + + def setDefault( self ): + """ Make this the server's default channel. """ + self.server.defchan = self.chanid; + self.server.save(); + + is_default = property( + lambda self: self.server.defchan == self.chanid, + doc="True if this channel is the server's default channel." + ); + + def asDict( self ): + chandata = self.channel_obj.__dict__.copy(); + chandata['players'] = [ pl.asDict() for pl in self.players ]; + chandata['subchans'] = [ sc.asDict() for sc in self.subchans ]; + return chandata; + + + + +class mmPlayer( object ): + """ Represents a Player in Murmur. """ + + def __init__( self, server, player_obj, player_chan ): + self.player_obj = player_obj; + + self.onlinesince = datetime.datetime.fromtimestamp( float( time() - player_obj.onlinesecs ) ); + self.channel = player_chan; + self.channel.players.append( self ); + + if self.isAuthed: + from mumble.models import MumbleUser + try: + self.mumbleuser = MumbleUser.objects.get( mumbleid=self.userid, server=server ); + except MumbleUser.DoesNotExist: + self.mumbleuser = None; + else: + self.mumbleuser = None; + + # Lookup unknown attributes in self.player_obj to automatically include Murmur's fields + def __getattr__( self, key ): + if hasattr( self.player_obj, key ): + return getattr( self.player_obj, key ); + else: + raise AttributeError( "'%s' object has no attribute '%s'" % ( self.__class__.__name__, key ) ); + + def __str__( self ): + return '' % ( self.name, self.session, self.userid ); + + hasComment = property( + lambda self: hasattr( self.player_obj, "comment" ) and bool(self.player_obj.comment), + doc="True if this player has a comment set." + ); + + isAuthed = property( + lambda self: self.userid != -1, + doc="True if this player is authenticated (+A)." + ); + + isAdmin = property( + lambda self: self.mumbleuser and self.mumbleuser.getAdmin(), + doc="True if this player is in the Admin group in the ACL." + ); + + is_server = False; + is_channel = False; + is_player = True; + + # kept for compatibility to mmChannel (useful for traversal funcs) + playerCount = property( lambda self: -1, doc="Exists only for compatibility to mmChannel." ); + + id = property( + lambda self: "player_%d"%self.session, + doc="A string ready to be used in an id property of an HTML tag." + ); + + def visit( self, callback, lvl = 0 ): + """ Call callback on myself. """ + callback( self, lvl ); + + def asDict( self ): + pldata = self.player_obj.__dict__.copy(); + + if self.mumbleuser: + if self.mumbleuser.hasTexture(): + pldata['texture'] = self.mumbleuser.textureUrl; + + return pldata; + + + +class mmACL( object ): + """ Represents an ACL for a certain channel. """ + + def __init__( self, channel, acl_obj ): + self.channel = channel; + self.acls, self.groups, self.inherit = acl_obj; + + self.groups_dict = {}; + + for group in self.groups: + self.groups_dict[ group.name ] = group; + + def group_has_member( self, name, userid ): + """ Checks if the given userid is a member of the given group in this channel. """ + if name not in self.groups_dict: + raise ReferenceError( "No such group '%s'" % name ); + + return userid in self.groups_dict[name].add or userid in self.groups_dict[name].members; + + def group_add_member( self, name, userid ): + """ Make sure this userid is a member of the group in this channel (and subs). """ + if name not in self.groups_dict: + raise ReferenceError( "No such group '%s'" % name ); + + group = self.groups_dict[name]; + + # if neither inherited nor to be added, add + if userid not in group.members and userid not in group.add: + group.add.append( userid ); + + # if to be removed, unremove + if userid in group.remove: + group.remove.remove( userid ); + + def group_remove_member( self, name, userid ): + """ Make sure this userid is NOT a member of the group in this channel (and subs). """ + if name not in self.groups_dict: + raise ReferenceError( "No such group '%s'" % name ); + + group = self.groups_dict[name]; + + # if added here, unadd + if userid in group.add: + group.add.remove( userid ); + # if member and not in remove, add to remove + elif userid in group.members and userid not in group.remove: + group.remove.append( userid ); + + def save( self ): + """ Send this ACL to Murmur. """ + return self.channel.server.ctl.setACL( + self.channel.server.srvid, + self.channel.chanid, + self.acls, self.groups, self.inherit + ); + + + diff --git a/mumble/models.py b/mumble/models.py new file mode 100644 index 0000000..9804b13 --- /dev/null +++ b/mumble/models.py @@ -0,0 +1,565 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import socket + +from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User +from django.db import models +from django.db.models import signals +from django.conf import settings + +from mumble.mmobjects import mmChannel, mmPlayer +from mumble.mctl import MumbleCtlBase + + +def mk_config_property( field, doc="" ): + """ Create a property for the given config field. """ + + def get_field( self ): + if self.id is not None: + return self.getConf( field ) + else: + return None + + def set_field( self, value ): + self.setConf( field, value ) + + return property( get_field, set_field, doc=doc ) + + +class Mumble( models.Model ): + """ Represents a Murmur server instance. + + All configurable settings are represented by a field in this model. To change the + settings, just update the appropriate field and call the save() method. + + To set up a new server instance, instanciate this Model. The first field you should + define is the "dbus" field, which tells the connector subsystem how to connect to + the Murmurd master process. Set this to the appropriate DBus service name or the + Ice proxy string. + + When an instance of this model is deleted, the according server instance will be + deleted as well. + """ + + name = models.CharField( _('Server Name'), max_length = 200 ); + dbus = models.CharField( _('DBus or ICE base'), max_length = 200, default = settings.DEFAULT_CONN, help_text=_( + "Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 -p 6502' for Ice.") ); + srvid = models.IntegerField( _('Server ID'), editable = False ); + addr = models.CharField( _('Server Address'), max_length = 200, help_text=_( + "Hostname or IP address to bind to. You should use a hostname here, because it will appear on the " + "global server list.") ); + port = models.IntegerField( _('Server Port'), default=settings.MUMBLE_DEFAULT_PORT, help_text=_( + "Port number to bind to. Use -1 to auto assign one.") ); + + + supw = property( lambda self: '', + lambda self, value: self.ctl.setSuperUserPassword( self.srvid, value ), + doc='Superuser Password' + ) + + url = mk_config_property( "registerurl", "Website URL" ) + motd = mk_config_property( "welcometext", "Welcome Message" ) + passwd = mk_config_property( "password", "Server Password" ) + users = mk_config_property( "users", "Max. Users" ) + bwidth = mk_config_property( "bandwidth", "Bandwidth [Bps]" ) + sslcrt = mk_config_property( "certificate", "SSL Certificate" ) + sslkey = mk_config_property( "key", "SSL Key" ) + player = mk_config_property( "username", "Player name regex" ) + channel = mk_config_property( "channelname", "Channel name regex" ) + defchan = mk_config_property( "defaultchannel", "Default channel" ) + + obfsc = property( + lambda self: ( self.getConf( "obfuscate" ) == "true" ) if self.id is not None else None, + lambda self, value: self.setConf( "obfuscate", str(value).lower() ), + doc="IP Obfuscation" + ) + + def getBooted( self ): + if self.id is not None: + return self.ctl.isBooted( self.srvid ); + else: + return False + + def setBooted( self, value ): + if value != self.getBooted(): + if value: + self.ctl.start( self.srvid ); + else: + self.ctl.stop( self.srvid ); + + booted = property( getBooted, setBooted, doc="Boot Server" ) + + class Meta: + unique_together = ( ( 'dbus', 'srvid' ), ( 'addr', 'port' ), ); + verbose_name = _('Server instance'); + verbose_name_plural = _('Server instances'); + + def __unicode__( self ): + if not self.id: + return u'Murmur "%s" (NOT YET CREATED)' % self.name; + return u'Murmur "%s" (%d)' % ( self.name, self.srvid ); + + def save( self, dontConfigureMurmur=False ): + """ Save the options configured in this model instance not only to Django's database, + but to Murmur as well. + """ + if dontConfigureMurmur: + # skip murmur configuration, e.g. because we're inserting models for existing servers. + return models.Model.save( self ); + + # check if this server already exists, if not call newServer and set my srvid first + + if self.id is None: + self.srvid = self.ctl.newServer(); + + if self.port == -1: + self.port = max( [ rec['port'] for rec in Mumble.objects.values('port') ] ) + 1; + + if self.port < 1 or self.port >= 2**16: + raise ValueError( _("Port number %(portno)d is not within the allowed range %(minrange)d - %(maxrange)d") % { + 'portno': self.port, + 'minrange': 1, + 'maxrange': 2**16, + }); + + self.ctl.setConf( self.srvid, 'host', socket.gethostbyname( self.addr ) ); + self.ctl.setConf( self.srvid, 'port', str(self.port) ); + self.ctl.setConf( self.srvid, 'registername', self.name ); + self.ctl.setConf( self.srvid, 'registerurl', self.url ); + + # registerHostname needs to take the port no into account + if self.port and self.port != settings.MUMBLE_DEFAULT_PORT: + self.ctl.setConf( self.srvid, 'registerhostname', "%s:%d" % ( self.addr, self.port ) ); + else: + self.ctl.setConf( self.srvid, 'registerhostname', self.addr ); + + if self.supw: + self.ctl.setSuperUserPassword( self.srvid, self.supw ); + self.supw = ''; + + if self.booted != self.ctl.isBooted( self.srvid ): + if self.booted: + self.ctl.start( self.srvid ); + else: + self.ctl.stop( self.srvid ); + + # Now allow django to save the record set + return models.Model.save( self ); + + + def __init__( self, *args, **kwargs ): + models.Model.__init__( self, *args, **kwargs ); + self._ctl = None; + self._channels = None; + self._rootchan = None; + + + users_regged = property( lambda self: self.mumbleuser_set.count(), doc="Number of registered users." ); + users_online = property( lambda self: len(self.ctl.getPlayers(self.srvid)), doc="Number of online users." ); + channel_cnt = property( lambda self: len(self.ctl.getChannels(self.srvid)), doc="Number of channels." ); + is_public = property( lambda self: self.passwd == '', + doc="False if a password is needed to join this server." ); + + is_server = True; + is_channel = False; + is_player = False; + + + # Ctl instantiation + def getCtl( self ): + """ Instantiate and return a MumbleCtl object for this server. + + Only one instance will be created, and reused on subsequent calls. + """ + if not self._ctl: + self._ctl = MumbleCtlBase.newInstance( self.dbus, settings.SLICE ); + return self._ctl; + + ctl = property( getCtl, doc="Get a Control object for this server. The ctl is cached for later reuse." ); + + + def getConf( self, field ): + return self.ctl.getConf( self.srvid, field ) + + def setConf( self, field, value ): + return self.ctl.setConf( self.srvid, field, value ) + + def configureFromMurmur( self ): + default = self.ctl.getDefaultConf(); + conf = self.ctl.getAllConf( self.srvid ); + + def find_in_dicts( keys, valueIfNotFound=None ): + if not isinstance( keys, tuple ): + keys = ( keys, ); + + for keyword in keys: + if keyword in conf: + return conf[keyword]; + + for keyword in keys: + keyword = keyword.lower(); + if keyword in default: + return default[keyword]; + + return valueIfNotFound; + + servername = find_in_dicts( "registername", "noname" ); + if not servername: + # RegistrationName was found in the dicts, but is an empty string + servername = "noname"; + + addr = find_in_dicts( ( "registerhostname", "host" ), "0.0.0.0" ); + if addr.find( ':' ) != -1: + # The addr is a hostname which actually contains a port number, but we already got that from + # the port field, so we can simply drop it. + addr = addr.split(':')[0]; + + self.name = servername; + self.addr = addr; + self.port = find_in_dicts( "port" ); + + self.save( dontConfigureMurmur=True ); + + + def readUsersFromMurmur( self, verbose=0 ): + if not self.booted: + raise SystemError( "This murmur instance is not currently running, can't sync." ); + + players = self.ctl.getRegisteredPlayers(self.srvid); + + for idx in players: + playerdata = players[idx]; + + if playerdata.userid == 0: # Skip SuperUsers + continue; + if verbose > 1: + print "Checking Player with id %d and name '%s'." % ( playerdata.userid, playerdata.name ); + + try: + playerinstance = MumbleUser.objects.get( server=self, mumbleid=playerdata.userid ); + + except MumbleUser.DoesNotExist: + if verbose: + print 'Found new Player "%s".' % playerdata.name; + + playerinstance = MumbleUser( + mumbleid = playerdata.userid, + name = playerdata.name, + password = '', + server = self, + owner = None + ); + + else: + if verbose > 1: + print "This player is already listed in the database."; + + playerinstance.name = playerdata.name; + + playerinstance.save( dontConfigureMurmur=True ); + + + def isUserAdmin( self, user ): + """ Determine if the given user is an admin on this server. """ + if user.is_authenticated(): + try: + return self.mumbleuser_set.get( owner=user ).getAdmin(); + except MumbleUser.DoesNotExist: + return False; + return False; + + + # Deletion handler + def deleteServer( self ): + """ Delete this server instance from Murmur. """ + self.ctl.deleteServer(self.srvid) + + @staticmethod + def pre_delete_listener( **kwargs ): + kwargs['instance'].deleteServer(); + + + # Channel list + def getChannels( self ): + """ Query the channels from Murmur and create a tree structure. + + Again, this will only be done for the first call to this function. Subsequent + calls will simply return the list created last time. + """ + if self._channels is None: + self._channels = {}; + chanlist = self.ctl.getChannels(self.srvid).values(); + links = {}; + + # sometimes, ICE seems to return the Channel list in a weird order. + # itercount prevents infinite loops. + itercount = 0; + maxiter = len(chanlist) * 3; + while len(chanlist) and itercount < maxiter: + itercount += 1; + for theChan in chanlist: + # Channels - Fields: 0 = ID, 1 = Name, 2 = Parent-ID, 3 = Links + if( theChan.parent == -1 ): + # No parent + self._channels[theChan.id] = mmChannel( self, theChan ); + elif theChan.parent in self.channels: + # parent already known + self._channels[theChan.id] = mmChannel( self, theChan, self.channels[theChan.parent] ); + else: + continue; + + chanlist.remove( theChan ); + + self._channels[theChan.id].serverId = self.id; + + # process links - if the linked channels are known, link; else save their ids to link later + for linked in theChan.links: + if linked in self._channels: + self._channels[theChan.id].linked.append( self._channels[linked] ); + else: + if linked not in links: + links[linked] = list(); + links[linked].append( self._channels[theChan.id] ); + + # check if earlier round trips saved channel ids to be linked to the current channel + if theChan.id in links: + for targetChan in links[theChan.id]: + targetChan.linked.append( self._channels[theChan.id] ); + + self._channels[0].name = self.name; + + self.players = {}; + for thePlayer in self.ctl.getPlayers(self.srvid).values(): + # Players - Fields: 0 = UserID, 6 = ChannelID + self.players[ thePlayer.session ] = mmPlayer( self, thePlayer, self._channels[ thePlayer.channel ] ); + + self._channels[0].sort(); + + return self._channels; + + channels = property( getChannels, doc="A convenience wrapper for getChannels()." ); + rootchan = property( lambda self: self.channels[0], doc="A convenience wrapper for getChannels()[0]." ); + + def getURL( self, forUser = None ): + """ Create an URL of the form mumble://username@host:port/ for this server. """ + userstr = ""; + if forUser is not None: + userstr = "%s@" % forUser.name; + + versionstr = "version=%d.%d.%d" % tuple(self.version[0:3]); + + if self.port != settings.MUMBLE_DEFAULT_PORT: + return "mumble://%s%s:%d/?%s" % ( userstr, self.addr, self.port, versionstr ); + + return "mumble://%s%s/?%s" % ( userstr, self.addr, versionstr ); + + connecturl = property( getURL, doc="A convenience wrapper for getURL()." ); + + version = property( lambda self: self.ctl.getVersion(), doc="The version of Murmur." ); + + def asDict( self ): + return { 'name': self.name, + 'id': self.id, + 'root': self.rootchan.asDict() + }; + + + +class MumbleUser( models.Model ): + """ Represents a User account in Murmur. + + To change an account, simply set the according field in this model and call the save() + method to update the account in Murmur and in Django's database. Note that, once saved + for the first time, the server field must not be changed. Attempting to do this will + result in an AttributeError. To move an account to a new server, recreate it on the + new server and delete the old model. + + When you delete an instance of this model, the according user account will be deleted + in Murmur as well, after revoking the user's admin privileges. + """ + + mumbleid = models.IntegerField( _('Mumble player_id'), editable = False, default = -1 ); + name = models.CharField( _('User name and Login'), max_length = 200 ); + password = models.CharField( _('Login password'), max_length = 200, blank=True ); + server = models.ForeignKey( Mumble, verbose_name=_('Server instance'), related_name="mumbleuser_set" ); + owner = models.ForeignKey( User, verbose_name=_('Account owner'), related_name="mumbleuser_set", null=True, blank=True ); + + class Meta: + unique_together = ( ( 'server', 'owner' ), ( 'server', 'mumbleid' ) ); + verbose_name = _( 'User account' ); + verbose_name_plural = _( 'User accounts' ); + + is_server = False; + is_channel = False; + is_player = True; + + def __unicode__( self ): + return _("Mumble user %(mu)s on %(srv)s owned by Django user %(du)s") % { + 'mu': self.name, + 'srv': self.server, + 'du': self.owner + }; + + def save( self, dontConfigureMurmur=False ): + """ Save the settings in this model to Murmur. """ + if dontConfigureMurmur: + # skip murmur configuration, e.g. because we're inserting models for existing players. + return models.Model.save( self ); + + # Before the record set is saved, update Murmur via controller. + ctl = self.server.ctl; + + if self.owner: + email = self.owner.email; + else: + email = settings.DEFAULT_FROM_EMAIL; + + if self.id is None: + # This is a new user record, so Murmur doesn't know about it yet + if len( ctl.getRegisteredPlayers( self.server.srvid, self.name ) ) > 0: + raise ValueError( _( "Another player already registered that name." ) ); + if not self.password: + raise ValueError( _( "Cannot register player without a password!" ) ); + + self.mumbleid = ctl.registerPlayer( self.server.srvid, self.name, email, self.password ); + + # Update user's registration + elif self.password: + ctl.setRegistration( + self.server.srvid, + self.mumbleid, + self.name, + email, + self.password + ); + + # Don't save the users' passwords, we don't need them anyway + self.password = ''; + + # Now allow django to save the record set + return models.Model.save( self ); + + def __init__( self, *args, **kwargs ): + models.Model.__init__( self, *args, **kwargs ); + self._registration = None; + + # Admin handlers + def getAdmin( self ): + """ Get ACL of root Channel, get the admin group and see if this user is in it. """ + return self.server.rootchan.acl.group_has_member( "admin", self.mumbleid ); + + def setAdmin( self, value ): + """ Set or revoke this user's membership in the admin group on the root channel. """ + if value: + self.server.rootchan.acl.group_add_member( "admin", self.mumbleid ); + else: + self.server.rootchan.acl.group_remove_member( "admin", self.mumbleid ); + self.server.rootchan.acl.save(); + return value; + + aclAdmin = property( getAdmin, setAdmin, doc="Wrapper around getAdmin/setAdmin (not a database field like isAdmin)" ); + + # Registration fetching + def getRegistration( self ): + """ Retrieve a user's registration from Murmur as a dict. """ + if not self._registration: + self._registration = self.server.ctl.getRegistration( self.server.srvid, self.mumbleid ); + return self._registration; + + registration = property( getRegistration, doc=getRegistration.__doc__ ); + + def getComment( self ): + """ Retrieve a user's comment, if any. """ + if "comment" in self.registration: + return self.registration["comment"]; + else: + return None; + + comment = property( getComment, doc=getComment.__doc__ ); + + def getHash( self ): + """ Retrieve a user's hash, if any. """ + if "hash" in self.registration: + return self.registration["hash"]; + else: + return None; + + hash = property( getHash, doc=getHash.__doc__ ); + + # Texture handlers + + def getTexture( self ): + """ Get the user texture as a PIL Image. """ + return self.server.ctl.getTexture(self.server.srvid, self.mumbleid); + + def setTexture( self, infile ): + """ Read an image from the infile and install it as the user's texture. """ + self.server.ctl.setTexture(self.server.srvid, self.mumbleid, infile) + + texture = property( getTexture, setTexture, + doc="Get the texture as a PIL Image or read from a file (pass the path)." + ); + + def hasTexture( self ): + """ Check if this user has a texture set. """ + try: + self.getTexture(); + except ValueError: + return False; + else: + return True; + + def getTextureUrl( self ): + """ Get a URL under which the texture can be retrieved. """ + from views import showTexture + from django.core.urlresolvers import reverse + return reverse( showTexture, kwargs={ 'server': self.server.id, 'userid': self.id } ); + + textureUrl = property( getTextureUrl, doc=getTextureUrl.__doc__ ); + + # Deletion handler + + @staticmethod + def pre_delete_listener( **kwargs ): + kwargs['instance'].unregister(); + + def unregister( self ): + """ Delete this user account from Murmur. """ + if self.getAdmin(): + self.setAdmin( False ); + self.server.ctl.unregisterPlayer(self.server.srvid, self.mumbleid) + + + # "server" field protection + + def __setattr__( self, name, value ): + if name == 'server': + if self.id is not None and self.server != value: + raise AttributeError( _( "This field must not be updated once the record has been saved." ) ); + + models.Model.__setattr__( self, name, value ); + + + + +signals.pre_delete.connect( Mumble.pre_delete_listener, sender=Mumble ); +signals.pre_delete.connect( MumbleUser.pre_delete_listener, sender=MumbleUser ); + + + + diff --git a/mumble/murmurenvutils.py b/mumble/murmurenvutils.py new file mode 100644 index 0000000..8231e79 --- /dev/null +++ b/mumble/murmurenvutils.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import os, subprocess, signal +from select import select +from os.path import join, exists +from shutil import copyfile + +from django.conf import settings + +from utils import ObjectInfo + +def get_available_versions(): + """ Return murmur versions installed inside the LAB_DIR. """ + dirs = os.listdir( settings.TEST_MURMUR_LAB_DIR ); + dirs.sort(); + return dirs; + + +def run_callback( version, callback, *args, **kwargs ): + """ Initialize the database and run murmur, then call the callback. + After the callback has returned, kill murmur. + + The callback will be passed the Popen object that wraps murmur, + and any arguments that were passed to run_callback. + + If the callback raises an exception, murmur will still be properly + shutdown and the exception will be reraised. + + The callback can either return an arbitrary value, or a tuple. + If it returns a tuple, it must be of the form: + + ( intended_return_value, call_update_dbase ) + + That means: If the second value evaluates to True, update_dbase + will be called; the first value will be returned by run_callback. + + If the callback returns anything other than a tuple, that value + will be returned directly. + + So, If run_callback should return a tuple, you will need to return + the tuple form mentioned above in the callback, and put your tuple + into the first parameter. + """ + + murmur_root = join( settings.TEST_MURMUR_LAB_DIR, version ); + if not exists( murmur_root ): + raise EnvironmentError( "This version could not be found: '%s' does not exist!" % murmur_root ); + + init_dbase( version ); + + process = run_murmur( version ); + + try: + result = callback( process, *args, **kwargs ); + if type(result) == tuple: + if result[1]: + update_dbase( version ); + return result[0]; + else: + return result; + finally: + kill_murmur( process ); + + +def init_dbase( version ): + """ Initialize Murmur's database by copying the one from FILES_DIR. """ + dbasefile = join( settings.TEST_MURMUR_FILES_DIR, "murmur-%s.db3" % version ); + if not exists( dbasefile ): + raise EnvironmentError( "This version could not be found: '%s' does not exist!" % dbasefile ); + murmurfile = join( settings.TEST_MURMUR_LAB_DIR, version, "murmur.sqlite" ); + copyfile( dbasefile, murmurfile ); + + +def update_dbase( version ): + """ Copy Murmur's database to FILES_DIR (the inverse of init_dbase). """ + murmurfile = join( settings.TEST_MURMUR_LAB_DIR, version, "murmur.sqlite" ); + if not exists( murmurfile ): + raise EnvironmentError( "Murmur's database could not be found: '%s' does not exist!" % murmurfile ); + dbasefile = join( settings.TEST_MURMUR_FILES_DIR, "murmur-%s.db3" % version ); + copyfile( murmurfile, dbasefile ); + + +def run_murmur( version ): + """ Run the given Murmur version as a subprocess. + + Either returns a Popen object or raises an EnvironmentError. + """ + + murmur_root = join( settings.TEST_MURMUR_LAB_DIR, version ); + if not exists( murmur_root ): + raise EnvironmentError( "This version could not be found: '%s' does not exist!" % murmur_root ); + + binary_candidates = ( 'murmur.64', 'murmur.x86', 'murmurd' ); + + for binname in binary_candidates: + if exists( join( murmur_root, binname ) ): + process = subprocess.Popen( + ( join( murmur_root, binname ), '-fg' ), + stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + cwd=murmur_root + ); + + # Check capabilities by waiting for certain lines to show up. + capa = ObjectInfo( has_dbus=False, has_ice=False, has_instance=False, has_users=False ); + + def canRead( self, timeout=1 ): + rdy_read, rdy_write, rdy_other = select( [self.stdout], [], [], timeout ); + return self.stdout in rdy_read; + + setattr(subprocess.Popen, 'canRead', canRead) + + while process.canRead(0.5): + line = process.stdout.readline(); + #print "read line:", line + if line == 'DBus registration succeeded\n': + capa.has_dbus = True; + elif line == 'MurmurIce: Endpoint "tcp -h 127.0.0.1 -p 6502" running\n': + capa.has_ice = True; + elif line == '1 => Server listening on 0.0.0.0:64738\n': + capa.has_instance = True; + elif "> Authenticated\n" in line: + capa.has_users = True; + + process.capabilities = capa; + + return process; + + raise EnvironmentError( "Murmur binary not found. (Tried %s)" % unicode(binary_candidates) ); + + +def wait_for_user( process, timeout=1 ): + """ Wait for a user to connect. This call will consume any output from murmur + until a line indicating a user's attempt to connect has been found. + + The timeout parameter specifies how long (in seconds) to wait for input. + It defaults to 1 second. If you set this to 0 it will return at the end + of input (and thereby tell you if a player has already connected). If + you set this to None, the call will block until a player has connected. + + Returns True if a user has connected before the timeout has been hit, + False otherwise. + """ + while process.canRead( timeout ): + line = process.stdout.readline(); + if "> Authenticated\n" in line: + process.capabilities.has_users = True; + return True; + return False; + + +def kill_murmur( process ): + """ Send a sigterm to the given process. """ + return os.kill( process.pid, signal.SIGTERM ); + + +class MumbleCommandWrapper_noargs( object ): + """ Mixin used to run a standard Django command inside MurmurEnvUtils. + + To modify a standard Django command for MEU, you will need to create + a new command and derive its Command class from the wrapper, and the + Command class of the original command: + + from django.core.management.commands.shell import Command as ShellCommand + from mumble.murmurenvutils import MumbleCommandWrapper + + class Command( MumbleCommandWrapper, ShellCommand ): + pass + + That will run the original command, after the user has had the chance to + select the version of Murmur to run. + """ + + def _choose_version( self ): + print "Choose version:"; + + vv = get_available_versions(); + for idx in range(len(vv)): + print " #%d %s" % ( idx, vv[idx] ); + + chosen = int( raw_input("#> ") ); + + return vv[chosen]; + + def handle_noargs( self, **options ): + self.origOpts = options; + + run_callback( self._choose_version(), self.runOrig ); + + def runOrig( self, proc ): + super( MumbleCommandWrapper_noargs, self ).handle_noargs( **self.origOpts ); + + +class MumbleCommandWrapper( object ): + """ Mixin used to run a standard Django command inside MurmurEnvUtils. + + To modify a standard Django command for MEU, you will need to create + a new command and derive its Command class from the wrapper, and the + Command class of the original command: + + from django.core.management.commands.shell import Command as ShellCommand + from mumble.murmurenvutils import MumbleCommandWrapper + + class Command( MumbleCommandWrapper, ShellCommand ): + pass + + That will run the original command, after the user has had the chance to + select the version of Murmur to run. + """ + + def _choose_version( self ): + print "Choose version:"; + + vv = get_available_versions(); + for idx in range(len(vv)): + print " #%d %s" % ( idx, vv[idx] ); + + chosen = int( raw_input("#> ") ); + + return vv[chosen]; + + def handle( self, *args, **options ): + self.origArgs = args; + self.origOpts = options; + + run_callback( self._choose_version(), self.runOrig ); + + def runOrig( self, proc ): + super( MumbleCommandWrapper, self ).handle( *self.origArgs, **self.origOpts ); + + diff --git a/mumble/utils.py b/mumble/utils.py new file mode 100644 index 0000000..eb72068 --- /dev/null +++ b/mumble/utils.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright © 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +class ObjectInfo( object ): + """ Wraps arbitrary information to be easily accessed. """ + + def __init__( self, **kwargs ): + self.__dict__ = kwargs; + + def __str__( self ): + return unicode( self ); + + def __repr__( self ): + return unicode( self ); + + def __unicode__( self ): + return unicode( self.__dict__ ); diff --git a/settings.py b/settings.py index 0532af1..5eaa842 100644 --- a/settings.py +++ b/settings.py @@ -88,6 +88,7 @@ INSTALLED_APPS = ( 'registration', 'eve_proxy', 'eve_api', + 'mumble', 'sso', ) @@ -109,3 +110,9 @@ JABBER_GROUP = 'dreddit' # Use sudo? JABBER_SUDO = True + +### Mumble Service Settings + +DEFAULT_CONN = 'Meta:tcp -h 127.0.0.1 -p 6502' +MUMBLE_DEFAULT_PORT = 64738 +SLICE = '/usr/share/slice/Murmur.ice'