From e09ba4fb93431653bfc6528c5f5f0cd612069391 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Tue, 26 Feb 2013 10:57:35 +0100 Subject: [PATCH] Add capability to delete unused channels to source plugin --- modules-available/source.ini | 2 +- modules/source/db.py | 52 ++++++++++++++--- modules/source/db_test.py | 77 +++++++++++++++++++++++++ modules/source/source.py | 105 ++++++++++++++++++++++++++--------- modules/source/users.py | 9 +++ 5 files changed, 210 insertions(+), 35 deletions(-) diff --git a/modules-available/source.ini b/modules-available/source.ini index f8a80e0..f0c2b95 100644 --- a/modules-available/source.ini +++ b/modules-available/source.ini @@ -49,7 +49,7 @@ restrict = true ; Create base/server-channels on-demand createifmissing = true ; Delete channels as soon as the last player is gone -deleteifunused = false +deleteifunused = true ; Regular expression for server restriction. ; Will be checked against steam server id. diff --git a/modules/source/db.py b/modules/source/db.py index 8483389..fd5eff5 100644 --- a/modules/source/db.py +++ b/modules/source/db.py @@ -31,6 +31,8 @@ import sqlite3 +#TODO: Functions returning channels probably should return a dict instead of a tuple + class SourceDB(object): def __init__(self, path = ":memory:"): self.db = sqlite3.connect(path) @@ -54,6 +56,25 @@ class SourceDB(object): v = self.db.execute("SELECT cid FROM source WHERE sid is ? and game is ? and server is ? and team is ?", [sid, game, server, team]).fetchone() return v[0] if v else None + + def channelForCid(self, sid, cid): + assert(sid != None and cid != None) + return self.db.execute("SELECT sid, cid, game, server, team FROM source WHERE sid is ? and cid is ?", [sid, cid]).fetchone() + + def channelFor(self, sid, game, server = None, team = None): + """ Returns matching channel as (sid, cid, game, server team) tuple """ + assert(sid != None and game != None) + assert(not (team != None and server == None)) + + v = self.db.execute("SELECT sid, cid, game, server, team FROM source WHERE sid is ? and game is ? and server is ? and team is ?", [sid, game, server, team]).fetchone() + return v + + def channelsFor(self, sid, game, server = None, team = None): + assert(sid != None and game != None) + assert(not (team != None and server == None)) + + suffix, params = self.__whereClauseForOptionals(server, team) + return self.db.execute("SELECT sid, cid, game, server, team FROM source WHERE sid is ? and game is ?" + suffix, [sid, game] + params).fetchall() def registerChannel(self, sid, cid, game, server = None, team = None): assert(sid != None and game != None) @@ -63,19 +84,27 @@ class SourceDB(object): self.db.commit() return True + def __whereClauseForOptionals(self, server, team): + """ + Generates where class conditions that interpret missing server + or team as "don't care". + + Returns (suffix, additional parameters) tuple + """ + + if server != None and team != None: + return (" and server is ? and team is ?", [server, team]) + elif server != None: + return (" and server is ?", [server]) + else: + return ("", []) + def unregisterChannel(self, sid, game, server = None, team = None): assert(sid != None and game != None) assert(not (team != None and server == None)) - base = "DELETE FROM source WHERE sid is ? and game is ?" - - if server != None and team != None: - self.db.execute(base + " and server is ? and team is ?", [sid, game, server, team]) - elif server != None: - self.db.execute(base + " and server is ?", [sid, game, server]) - else: - self.db.execute(base, [sid, game]) - + suffix, params = self.__whereClauseForOptionals(server, team) + self.db.execute("DELETE FROM source WHERE sid is ? and game is ?" + suffix, [sid, game] + params) self.db.commit() def dropChannel(self, sid, cid): @@ -83,6 +112,11 @@ class SourceDB(object): self.db.execute("DELETE FROM source WHERE sid is ? and cid is ?", [sid, cid]) self.db.commit() + def isRegisteredChannel(self, sid, cid): + """ Returns true if a channel with given sid and cid is registered """ + res = self.db.execute("SELECT cid FROM source WHERE sid is ? and cid is ?", [sid, cid]).fetchone() + return res != None + def registeredChannels(self): """ Returns channels as a list of (sid, cid, game, server team) tuples grouped by sid """ return self.db.execute("SELECT sid, cid, game, server, team FROM source ORDER by sid").fetchall() diff --git a/modules/source/db_test.py b/modules/source/db_test.py index 367339d..860dfeb 100644 --- a/modules/source/db_test.py +++ b/modules/source/db_test.py @@ -140,6 +140,83 @@ class SourceDBTest(unittest.TestCase): self.assertEqual(self.db.registeredChannels(), expected) + def testIsRegisteredChannel(self): + self.db.reset() + sid = 1; cid = 0; game = "tf" + self.db.registerChannel(sid, cid, game) + + self.assertTrue(self.db.isRegisteredChannel(sid, cid)) + self.assertFalse(self.db.isRegisteredChannel(sid+1, cid)) + self.assertFalse(self.db.isRegisteredChannel(sid, cid+1)) + + self.db.unregisterChannel(sid, game) + + self.assertFalse(self.db.isRegisteredChannel(sid, cid)) + + def testChannelFor(self): + self.db.reset() + sid = 1; cid = 0; game = "tf"; server = "serv"; team = 0 + self.db.registerChannel(sid, cid, game) + self.db.registerChannel(sid, cid+1, game, server) + self.db.registerChannel(sid, cid+2, game, server, team) + + res = self.db.channelFor(sid, game, server, team) + self.assertEqual(res, (sid, cid + 2, game, server, team)) + + res = self.db.channelFor(sid, game, server) + self.assertEqual(res, (sid, cid + 1, game, server, None)) + + res = self.db.channelFor(sid, game) + self.assertEqual(res, (sid, cid, game, None, None)) + + res = self.db.channelFor(sid, game, server, team+5) + self.assertEqual(res, None) + + def testChannelForCid(self): + self.db.reset() + sid = 1; cid = 0; game = "tf"; server = "serv"; team = 0 + self.db.registerChannel(sid, cid, game) + self.db.registerChannel(sid, cid+1, game, server) + self.db.registerChannel(sid, cid+2, game, server, team) + + res = self.db.channelForCid(sid, cid) + self.assertEqual(res, (sid, cid, game, None, None)) + + + res = self.db.channelForCid(sid, cid + 1) + self.assertEqual(res, (sid, cid + 1, game, server, None)) + + + res = self.db.channelForCid(sid, cid + 2) + self.assertEqual(res, (sid, cid + 2, game, server, team)) + + + res = self.db.channelForCid(sid, cid + 3) + self.assertEqual(res, None) + + def testChannelsFor(self): + self.db.reset() + sid = 1; cid = 0; game = "tf"; server = "serv"; team = 0 + self.db.registerChannel(sid, cid, game) + self.db.registerChannel(sid, cid+1, game, server) + self.db.registerChannel(sid, cid+2, game, server, team) + + chans = ((sid, cid+2, game, server, team), + (sid, cid+1, game, server, None), + (sid, cid, game, None, None)) + + res = self.db.channelsFor(sid, game, server, team) + self.assertItemsEqual(res, chans[0:1]) + + res = self.db.channelsFor(sid, game, server) + self.assertItemsEqual(res, chans[0:2]) + + res = self.db.channelsFor(sid, game) + self.assertItemsEqual(res, chans) + + res = self.db.channelsFor(sid+1, game) + self.assertItemsEqual(res, []) + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() \ No newline at end of file diff --git a/modules/source/source.py b/modules/source/source.py index 744c590..d6b4006 100644 --- a/modules/source/source.py +++ b/modules/source/source.py @@ -54,7 +54,7 @@ class source(MumoModule): ('restrict', x2bool, True), ('serverregex', re.compile, re.compile("^\[[\w\d\-\(\):]{1,20}\]$")), ('createifmissing', x2bool, True), - ('deleteifunused', x2bool, False) + ('deleteifunused', x2bool, True) ) default_config = {'source':( @@ -127,11 +127,11 @@ class source(MumoModule): def disconnected(self): pass - def userTransition(self, server, old, new): - sid = server.id() + def userTransition(self, mumble_server, old, new): + sid = mumble_server.id() assert(not old or old.valid()) - + relevant = old or (new and new.valid()) if not relevant: return @@ -148,17 +148,29 @@ class source(MumoModule): if not user_gone: #TODO: Establish new group memberships - self.moveUser(server, - new.state, - new.game, - new.server, - new.identity["team"]) + moved = self.moveUser(mumble_server, new) else: # User gone + assert(old) + self.users.remove(sid, old.state.session) + moved = True + if not new: self.dlog(sid, old.state, "User gone") else: + # Move user out of our channel structure + self.moveUserToCid(mumble_server, new.state, self.cfg().source.basechannelid) self.dlog(sid, old.state, "User stopped playing") + + + if moved and old: + # If moved from a valid game state perform channel use check + chan = self.db.channelForCid(sid, old.state.channel) + if chan: + _, _, game, _, _ = chan + if self.gameCfg(game, "deleteifunused"): + self.deleteIfUnused(mumble_server, old.state.channel) + def getGameName(self, game): @@ -230,16 +242,67 @@ class source(MumoModule): state.channel = cid server.setState(state) - def moveUser(self, mumble_server, state, game, server, team): + def moveUser(self, mumble_server, user): + state = user.state + game = user.game + server = user.server + team = user.identity["team"] + sid = mumble_server.id() + source_cid = state.channel target_cid = self.getOrCreateChannelFor(mumble_server, game, server, team) + if source_cid != target_cid: self.moveUserToCid(mumble_server, state, target_cid) - - # TODO: Source channel deletion if unused + 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 == None: + 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 validGameType(self, game): return self.cfg().source.gameregex.match(game) != None @@ -301,7 +364,6 @@ class source(MumoModule): self.log().debug("(%d) (%d|%d) " + what, sid, state.session, state.userid, *argc) def handle(self, server, new_state): - log = self.log() sid = server.id() session = new_state.session @@ -317,22 +379,15 @@ class source(MumoModule): game, game_server = self.parseSourceContext(new_state.context) identity = self.parseSourceIdentity(new_state.identity) - self.dlog(sid, new_state, "Context: '%s' -> '%s'", game, game_server) - self.dlog(sid, new_state, "Identity: '%s'", identity) + self.dlog(sid, new_state, "Context: %s -> '%s' / '%s'", repr(new_state.context), game, game_server) + self.dlog(sid, new_state, "Identity: %s -> '%s'", repr(new_state.identity), identity) updated_user = User(new_state, identity, game, game_server) self.dlog(sid, new_state, "Starting transition") self.userTransition(server, old_user, updated_user) - - if updated_user.valid(): - self.users.addOrUpdate(sid, session, updated_user) - self.dlog(sid, new_state, "Transition completed") - else: - # User isn't relevant for this plugin - self.users.remove(sid, session) - self.dlog(sid, new_state, "User not of concern for plugin") - + self.dlog(sid, new_state, "Transition complete") + # #--- Server callback functions # diff --git a/modules/source/users.py b/modules/source/users.py index c260b6a..5809170 100644 --- a/modules/source/users.py +++ b/modules/source/users.py @@ -106,4 +106,13 @@ class UserRegistry(object): return False return True + def usingChannel(self, sid, cid): + """ + Return true if any user in the registry is occupying the given channel + """ + for user in self.users[sid].itervalues(): + if user.state and user.state.channel == cid: + return True + + return False