Example #1
0
 def connected(self):
     """
     Makes sure the the plugin is correctly configured once the connection
     to the mumble server is (re-)established.
     """
     cfg = self.cfg()
     manager = self.manager()
     log = self.log()
     log.debug("Register for Server callbacks")
     
     self.meta = manager.getMeta()
     
     servers = set(cfg.source.mumbleservers)
     if not servers:
         servers = manager.SERVERS_ALL
     
     self.users = UserRegistry()
     
     self.validateChannelDB()
     
     manager.subscribeServerCallbacks(self, servers)
     manager.subscribeMetaCallbacks(self, servers)
Example #2
0
    def testRegistryCRUDOps(self):
        r = UserRegistry()

        sid, session, user = self.getSomeUsers()

        # Create & Read
        self.assertTrue(r.add(sid[0], session[0], user[0]))
        self.assertFalse(r.add(sid[0], session[0], user[0]))
        self.assertEqual(r.get(sid[0], session[0]), user[0])

        self.assertTrue(r.addOrUpdate(sid[1], session[1], user[1]))
        self.assertEqual(r.get(sid[1], session[1]), user[1])

        # Update
        self.assertTrue(r.addOrUpdate(sid[0], session[0], user[2]))
        self.assertEqual(r.get(sid[0], session[0]), user[2])

        # Delete
        self.assertTrue(r.remove(sid[1], session[1]))
        self.assertFalse(r.remove(sid[1], session[1]))
        self.assertEqual(r.get(sid[1], session[1]), None)

        self.assertTrue(r.remove(sid[0], session[0]))
        self.assertFalse(r.remove(sid[0], session[0]))
        self.assertEqual(r.get(sid[0], session[0]), None)
Example #3
0
class source(MumoModule):
    """
    This class combines the basic mumble moderator callbacks with
    server level callbacks for handling source game positional audio
    context and identity information.
    """
    default_game_config = (
                             ('name', str, "%(game)s"),
                             ('servername', str, "%(server)s"),
                             ('teams', commaSeperatedStrings, ["Lobby", "Spectator", "Team one", "Team two", "Team three", "Team four"]),
                             ('restrict', x2bool, True),
                             ('serverregex', re.compile, re.compile("^\[[\w\d\-\(\):]{1,20}\]$")),
                             ('deleteifunused', x2bool, True)
                          )
    
    default_config = {'source':(
                             ('database', str, "source.sqlite"),
                             ('basechannelid', int, 0),
                             ('mumbleservers', commaSeperatedIntegers, []),
                             ('gameregex', re.compile, re.compile("^(tf|dod|cstrike|hl2mp)$")),
                             ('groupprefix', str, "source_")
                             ),
                      
                      # The generic section defines default values which can be overridden in
                      # optional game specific "game:<gameshorthand>" sections

                      'generic': default_game_config,
                      lambda x: re.match('^game:\w+$', x): default_game_config
                      
                    }
    
    def __init__(self, name, manager, configuration=None):
        MumoModule.__init__(self, name, manager, configuration)
        self.murmur = manager.getMurmurModule()

    def onStart(self):
        MumoModule.onStart(self)
        cfg = self.cfg()
        self.db = SourceDB(cfg.source.database)
    
    def onStop(self):
        MumoModule.onStop(self)
        self.db.close()
        
    def connected(self):
        """
        Makes sure the the plugin is correctly configured once the connection
        to the mumble server is (re-)established.
        """
        cfg = self.cfg()
        manager = self.manager()
        log = self.log()
        log.debug("Register for Server callbacks")
        
        self.meta = manager.getMeta()
        
        servers = set(cfg.source.mumbleservers)
        if not servers:
            servers = manager.SERVERS_ALL
        
        self.users = UserRegistry()
        
        self.validateChannelDB()
        
        manager.subscribeServerCallbacks(self, servers)
        manager.subscribeMetaCallbacks(self, servers)
        
    
    def validateChannelDB(self):
        """
        Makes sure the plugins internal datatbase
        matches the actual state of the servers.
        """
        log = self.log()
        log.debug("Validating channel database")
                
        current_sid = -1
        current_mumble_server = None
        
        for sid, cid, game, server, team in self.db.registeredChannels():
            if current_sid != sid:
                current_mumble_server = self.meta.getServer(sid)
                current_sid = sid
                
            try:
                state = current_mumble_server.getChannelState(cid)
                self.db.mapName(state.name, sid, game, server, team)
                #TODO: Verify ACL?
                
            except self.murmur.InvalidChannelException:
                # Channel no longer exists
                log.debug("(%d) Channel %d no longer exists. Dropped.", sid, cid)
                self.db.dropChannel(sid, cid)
            except AttributeError:
                # Server no longer exists
                assert(current_mumble_server == None)
                log.debug("(%d) Server for channel %d no longer exists. Dropped.", sid, cid)
                self.db.dropChannel(sid, cid)
    
    def disconnected(self): pass
    
    def removeFromGroups(self, mumble_server, session, game, server, team):
        """
        Removes the client from all relevant groups
        """
        sid = mumble_server.id()
        prefix = self.cfg().source.groupprefix
        game_cid = self.db.cidFor(sid, game)
        
        group = prefix + game
        mumble_server.removeUserFromGroup(game_cid, session, group) # Game
        
        group += "_" + server
        mumble_server.removeUserFromGroup(game_cid, session, group) # Server
        
        group += "_" + str(team)
        mumble_server.removeUserFromGroup(game_cid, session, group) # Team 
    
    def addToGroups(self, mumble_server, session, game, server, team):
        """
        Adds the client to all relevant groups
        """
        sid = mumble_server.id()
        prefix = self.cfg().source.groupprefix
        game_cid = self.db.cidFor(sid, game)
        assert(game_cid != None)
        
        group = prefix + game
        mumble_server.addUserToGroup(game_cid, session, group) # Game
        
        group += "_" + server
        mumble_server.addUserToGroup(game_cid, session, group) # Server
        
        group += "_" + str(team)
        mumble_server.addUserToGroup(game_cid, session, group) # Team 
    

    def transitionPresentUser(self, mumble_server, old, new, sid, user_new):
        """
        Transitions a user that has been and is currently playing
        """
        assert(new)
        
        target_cid = self.getOrCreateTargetChannelFor(mumble_server, new)
        
        if user_new:
            self.dlog(sid, new.state, "User started playing: g/s/t %s/%s/%d", new.game, new.server, new.identity["team"])
            self.addToGroups(mumble_server, new.state.session, new.game, new.server, new.identity["team"])
        else:
            assert old
            self.dlog(sid, old.state, "User switched: g/s/t %s/%s/%d", new.game, new.server, new.identity["team"])
            self.removeFromGroups(mumble_server, old.state.session, old.game, old.server, old.identity["team"])
            self.addToGroups(mumble_server, new.state.session, new.game, new.server, new.identity["team"])
        
        return self.moveUser(mumble_server, new, target_cid)


    def transitionGoneUser(self, mumble_server, old, new, sid):
        """
        Transitions a user that played but is no longer doing so now.
        """
        assert(old)
        
        self.users.remove(sid, old.state.session)
        
        if new:
            self.removeFromGroups(mumble_server, old.state.session, old.game, old.server, old.identity["team"])
            
            bcid = self.cfg().source.basechannelid
            self.dlog(sid, old.state, "User stopped playing. Moving to %d.", bcid)
            self.moveUserToCid(mumble_server, new.state, bcid)
        else:
            self.dlog(sid, old.state, "User gone")
        return True


    def userLeftChannel(self, mumble_server, old, sid):
        """
        User left channel. Make sure we check for vacancy it if the game it
        belongs to is configured that way.
        """
        chan = self.db.channelFor(sid, old.game, old.server, old.identity['team'])
        if chan:
            _, cid, game, _, _ = chan
            if self.getGameConfig(game, "deleteifunused"):
                self.deleteIfUnused(mumble_server, cid)

    def userTransition(self, mumble_server, old, new):
        """
        Handles the transition of the user between given old and new states.
        
        If no old state is available (connect, starting to play, ...) old can be
        None. If an old state is given it is assumed that it is valid.
        
        If no new state is available (disconnect) new can be None. A new state
        can be either valid (playing) or invalid (not or no longer playing).
        
        Depending on the previous and the new state this function performs all
        needed actions.
        """
        sid = mumble_server.id()

        assert(not old or old.valid())
        
        relevant = old or (new and new.valid()) 
        if not relevant:
            return
        
        user_new = not old and new and new.valid()
        user_gone = old and (not new or not new.valid())
        
        if not user_gone:
            moved = self.transitionPresentUser(mumble_server, old, new, sid, user_new)
            
        else:
            moved = self.transitionGoneUser(mumble_server, old, new, sid)
        
        
        if moved and old:
            self.userLeftChannel(mumble_server, old, sid)
                    
    def getGameName(self, game):
        """
        Returns the unexpanded game specific game name template.
        """
        return self.getGameConfig(game, "name")
    
    def getServerName(self, game):
        """
        Returns the unexpanded game specific server name template.
        """
        return self.getGameConfig(game, "servername")
        
    def getTeamName(self, game, index):
        """
        Returns the game specific team name for the given team index.
        If the index is invalid the stringified index is returned.
        """
        try:
            return self.getGameConfig(game, "teams")[index]
        except IndexError:
            return str(index)
    
    def setACLsForGameChannel(self, mumble_server, game_cid, game):
        """
        Sets the appropriate ACLs for a game channel for the given cid.
        """
        # Shorthands
        ACL = self.murmur.ACL
        EAT = self.murmur.PermissionEnter | self.murmur.PermissionTraverse # Enter And Traverse
        W = self.murmur.PermissionWhisper # Whisper
        S = self.murmur.PermissionSpeak # Speak
        
        groupname = '~' + self.cfg().source.groupprefix + game
        
        mumble_server.setACL(game_cid,
                          [ACL(applyHere = True, # Deny everything
                               applySubs = True,
                               userid = -1,
                               group = 'all',
                               deny = EAT | W | S),
                           ACL(applyHere = True, # Allow enter and traverse to players
                               applySubs = False,
                               userid = -1,
                               group = groupname,
                               allow = EAT)],
                           [], True)
    

    def setACLsForServerChannel(self, mumble_server, server_cid, game, server):
        """
        Sets the appropriate ACLs for a server channel for the given cid.
        """
        # Shorthands
        ACL = self.murmur.ACL
        EAT = self.murmur.PermissionEnter | self.murmur.PermissionTraverse # Enter And Traverse
        W = self.murmur.PermissionWhisper # Whisper
        S = self.murmur.PermissionSpeak # Speak
        
        groupname = '~' + self.cfg().source.groupprefix + game + "_" + server
        
        mumble_server.setACL(server_cid,
                          [ACL(applyHere = True, # Allow enter and traverse to players
                               applySubs = False,
                               userid = -1,
                               group = groupname,
                               allow = EAT)],
                           [], True)
        

    def setACLsForTeamChannel(self, mumble_server, team_cid, game, server, team):
        """
        Sets the appropriate ACLs for a team channel for the given cid.
        """
        # Shorthands
        ACL = self.murmur.ACL
        EAT = self.murmur.PermissionEnter | self.murmur.PermissionTraverse # Enter And Traverse
        W = self.murmur.PermissionWhisper # Whisper
        S = self.murmur.PermissionSpeak # Speak
        
        groupname = '~' + self.cfg().source.groupprefix + game + "_" + server + "_" + str(team)
        
        mumble_server.setACL(team_cid,
                          [ACL(applyHere = True, # Allow enter and traverse to players
                               applySubs = False,
                               userid = -1,
                               group = groupname,
                               allow = EAT | W | S)],
                           [], True)

    def getOrCreateGameChannelFor(self, mumble_server, game, server, sid, cfg, log, namevars):
        """
        Helper function for getting or creating only the game channel. Returns
        the cid of the exisitng or created game channel.
        """
        sid = mumble_server.id()
        game_cid = self.db.cidFor(sid, game)
        if game_cid == None:
            game_channel_name = self.db.nameFor(sid, game,
                                                default = (self.getGameName(game) % namevars))
            
            log.debug("(%d) Creating game channel '%s' below %d", sid, game_channel_name, cfg.source.basechannelid)
            game_cid = mumble_server.addChannel(game_channel_name, cfg.source.basechannelid)
            self.db.registerChannel(sid, game_cid, game) # Make sure we don't have orphaned server channels around
            self.db.unregisterChannel(sid, game, server)
            
            if self.getGameConfig(game, "restrict"):
                log.debug("(%d) Setting ACL's for new game channel (cid %d)", sid, game_cid)
                self.setACLsForGameChannel(mumble_server, game_cid, game)
            
            log.debug("(%d) Game channel created and registered (cid %d)", sid, game_cid)
        return game_cid


    def getOrCreateServerChannelFor(self, mumble_server, game, server, team, sid, log, namevars, game_cid):
        """
        Helper function for getting or creating only the server channel. The game
        channel must already exist. Returns the cid of the existing or created
        server channel.
        """
        server_cid = self.db.cidFor(sid, game, server)
        if server_cid == None:
            server_channel_name = self.db.nameFor(sid, game, server,
                                                  default = self.getServerName(game) % namevars)
            
            log.debug("(%d) Creating server channel '%s' below %d", sid, server_channel_name, game_cid)
            server_cid = mumble_server.addChannel(server_channel_name, game_cid)
            self.db.registerChannel(sid, server_cid, game, server)
            self.db.unregisterChannel(sid, game, server, team) # Make sure we don't have orphaned team channels around
            
            if self.getGameConfig(game, "restrict"):
                log.debug("(%d) Setting ACL's for new server channel (cid %d)", sid, server_cid)
                self.setACLsForServerChannel(mumble_server, server_cid, game, server)
            
            log.debug("(%d) Server channel created and registered (cid %d)", sid, server_cid)
        return server_cid


    def getOrCreateTeamChannelFor(self, mumble_server, game, server, team, sid, log, server_cid):
        """
        Helper function for getting or creating only the team channel. Game and
        server channel must already exist. Returns the cid of the existing or
        created team channel.
        """
        
        team_cid = self.db.cidFor(sid, game, server, team)
        if team_cid == None:
            team_channel_name = self.db.nameFor(sid, game, server, team,
                                                default = self.getTeamName(game, team))
            
            log.debug("(%d) Creating team channel '%s' below %d", sid, team_channel_name, server_cid)
            team_cid = mumble_server.addChannel(team_channel_name, server_cid)
            self.db.registerChannel(sid, team_cid, game, server, team)
            
            if self.getGameConfig(game, "restrict"):
                log.debug("(%d) Setting ACL's for new team channel (cid %d)", sid, team_cid)
                self.setACLsForTeamChannel(mumble_server, team_cid, game, server, team)
            
            log.debug("(%d) Team channel created and registered (cid %d)", sid, team_cid)
        return team_cid

    def getOrCreateChannelFor(self, mumble_server, game, server, team):
        """
        Checks whether a requested team channel already exists. If not
        all missing parts of the channel structure are created. Returns
        the cid of the existing or created team channel.
        """
        sid = mumble_server.id()
        cfg = self.cfg()
        log = self.log()
        
        namevars = {'game' : game,
                    'server' : server}
        
        game_cid = self.getOrCreateGameChannelFor(mumble_server, game, server, sid, cfg, log, namevars)
        server_cid = self.getOrCreateServerChannelFor(mumble_server, game, server, team, sid, log, namevars, game_cid)
        team_cid = self.getOrCreateTeamChannelFor(mumble_server, game, server, team, sid, log, server_cid)
        
        return team_cid
    
    def moveUserToCid(self, server, state, cid):
        """
        Low level helper for moving a user to a channel known by its ID
        """
        self.dlog(server.id(), state, "Moving from channel %d to %d", state.channel, cid)
        state.channel = cid
        server.setState(state)
        
    def getOrCreateTargetChannelFor(self, mumble_server, user):
        """
        Returns the cid of the target channel for this user. If needed
        missing channels will be created.
        """
        return self.getOrCreateChannelFor(mumble_server,
                                          user.game,
                                          user.server,
                                          user.identity["team"])
        
    def moveUser(self, mumble_server, user, target_cid = None):
        """
        Move user according to current game state.
        
        This function performs all tasks of the move including creating
        channels if needed or deleting unused ones when appropriate.
        If a target_cid is given it is assumed that the channel
        structure is already present.
        """
        state = user.state
        game = user.game
        server = user.server
        team = user.identity["team"]
        sid = mumble_server.id()
        
        source_cid = state.channel
        
        if target_cid == None:
            target_cid = self.getOrCreateChannelFor(mumble_server, game, server, team)
        
        if source_cid != target_cid:
            self.moveUserToCid(mumble_server, state, target_cid)
            user.state.channel = target_cid
            self.users.addOrUpdate(sid, state.session, user)
            
            return True
        
        return False
    
    def deleteIfUnused(self, mumble_server, cid):
        """
        Takes the cid of a server or team channel and checks if all
        related channels (siblings and server) are unused. If true
        the channel is unused and will be deleted.
        
        Note: Assumes tree structure
        """
        
        sid = mumble_server.id()
        log = self.log()
        
        result = self.db.channelForCid(sid, cid)
        if not result:
            return False
        
        _, _, cur_game, cur_server, cur_team = result
        assert(cur_game)
        
        if not cur_server:
            # Don't handle game channels
            log.debug("(%d) Delete if unused on game channel %d, ignoring", sid, cid)
            return False
        
        server_channel_cid = None
        relevant = self.db.channelsFor(sid, cur_game, cur_server)
        
        for _, cur_cid, _, _, cur_team in relevant:
            if cur_team == self.db.NO_TEAM:
                server_channel_cid = cur_cid
                
            if self.users.usingChannel(sid, cur_cid):
                log.debug("(%d) Delete if unused: Channel %d in use", sid, cur_cid)
                return False # Used
        
        assert(server_channel_cid != None)
        
        # Unused. Delete server and children
        log.debug("(%s) Channel %d unused. Will be deleted.", sid, server_channel_cid)
        mumble_server.removeChannel(server_channel_cid)
        return True

    def isValidGameType(self, game):
        return self.cfg().source.gameregex.match(game) != None
    
    def isValidServer(self, game, server):
        return self.getGameConfig(game, "serverregex").match(server) != None
    
    def parseSourceContext(self, context):
        """
        Parse source engine context string. Returns tuple with
        game name and server identification. Returns None for both 
        if context string is invalid.
        """
        try:
            prefix, server = context.split('\x00')[0:2]
            source, game = [s.strip() for s in prefix.split(':', 1)]
            
            if source != "Source engine":
                # Not a source engine context
                return (None, None)
            
            if not self.isValidGameType(game) or not self.isValidServer(game, server):
                return (None, None)
            
            return (game, server)
            
        except (AttributeError, ValueError),e:
            return (None, None);
Example #4
0
 def testRegistryCRUDOps(self):
     r = UserRegistry()
     
     sid, session, user = self.getSomeUsers()
     
     # Create & Read
     self.assertTrue(r.add(sid[0], session[0], user[0]))
     self.assertFalse(r.add(sid[0], session[0], user[0]))
     self.assertEqual(r.get(sid[0], session[0]), user[0])
     
     self.assertTrue(r.addOrUpdate(sid[1], session[1], user[1]))
     self.assertEqual(r.get(sid[1], session[1]), user[1])
     
     # Update
     self.assertTrue(r.addOrUpdate(sid[0], session[0], user[2]))
     self.assertEqual(r.get(sid[0], session[0]), user[2])
     
     # Delete
     self.assertTrue(r.remove(sid[1], session[1]))
     self.assertFalse(r.remove(sid[1], session[1]))
     self.assertEqual(r.get(sid[1], session[1]), None)
     
     self.assertTrue(r.remove(sid[0], session[0]))
     self.assertFalse(r.remove(sid[0], session[0]))
     self.assertEqual(r.get(sid[0], session[0]), None)