Add capability to delete unused channels to source plugin

This commit is contained in:
Stefan Hacker
2013-02-26 10:57:35 +01:00
parent deb9aba022
commit e09ba4fb93
5 changed files with 210 additions and 35 deletions

View File

@@ -49,7 +49,7 @@ restrict = true
; Create base/server-channels on-demand ; Create base/server-channels on-demand
createifmissing = true createifmissing = true
; Delete channels as soon as the last player is gone ; Delete channels as soon as the last player is gone
deleteifunused = false deleteifunused = true
; Regular expression for server restriction. ; Regular expression for server restriction.
; Will be checked against steam server id. ; Will be checked against steam server id.

View File

@@ -31,6 +31,8 @@
import sqlite3 import sqlite3
#TODO: Functions returning channels probably should return a dict instead of a tuple
class SourceDB(object): class SourceDB(object):
def __init__(self, path = ":memory:"): def __init__(self, path = ":memory:"):
self.db = sqlite3.connect(path) self.db = sqlite3.connect(path)
@@ -55,6 +57,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() 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 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): def registerChannel(self, sid, cid, game, server = None, team = None):
assert(sid != None and game != None) assert(sid != None and game != None)
assert(not (team != None and server == None)) assert(not (team != None and server == None))
@@ -63,19 +84,27 @@ class SourceDB(object):
self.db.commit() self.db.commit()
return True 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): def unregisterChannel(self, sid, game, server = None, team = None):
assert(sid != None and game != None) assert(sid != None and game != None)
assert(not (team != None and server == None)) assert(not (team != None and server == None))
base = "DELETE FROM source WHERE sid is ? and game is ?" suffix, params = self.__whereClauseForOptionals(server, team)
self.db.execute("DELETE FROM source WHERE sid is ? and game is ?" + suffix, [sid, game] + params)
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])
self.db.commit() self.db.commit()
def dropChannel(self, sid, cid): 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.execute("DELETE FROM source WHERE sid is ? and cid is ?", [sid, cid])
self.db.commit() 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): def registeredChannels(self):
""" Returns channels as a list of (sid, cid, game, server team) tuples grouped by sid """ """ 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() return self.db.execute("SELECT sid, cid, game, server, team FROM source ORDER by sid").fetchall()

View File

@@ -140,6 +140,83 @@ class SourceDBTest(unittest.TestCase):
self.assertEqual(self.db.registeredChannels(), expected) 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__": if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.testName'] #import sys;sys.argv = ['', 'Test.testName']
unittest.main() unittest.main()

View File

@@ -54,7 +54,7 @@ class source(MumoModule):
('restrict', x2bool, True), ('restrict', x2bool, True),
('serverregex', re.compile, re.compile("^\[[\w\d\-\(\):]{1,20}\]$")), ('serverregex', re.compile, re.compile("^\[[\w\d\-\(\):]{1,20}\]$")),
('createifmissing', x2bool, True), ('createifmissing', x2bool, True),
('deleteifunused', x2bool, False) ('deleteifunused', x2bool, True)
) )
default_config = {'source':( default_config = {'source':(
@@ -127,8 +127,8 @@ class source(MumoModule):
def disconnected(self): pass def disconnected(self): pass
def userTransition(self, server, old, new): def userTransition(self, mumble_server, old, new):
sid = server.id() sid = mumble_server.id()
assert(not old or old.valid()) assert(not old or old.valid())
@@ -148,19 +148,31 @@ class source(MumoModule):
if not user_gone: if not user_gone:
#TODO: Establish new group memberships #TODO: Establish new group memberships
self.moveUser(server, moved = self.moveUser(mumble_server, new)
new.state,
new.game,
new.server,
new.identity["team"])
else: else:
# User gone # User gone
assert(old)
self.users.remove(sid, old.state.session)
moved = True
if not new: if not new:
self.dlog(sid, old.state, "User gone") self.dlog(sid, old.state, "User gone")
else: 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") 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): def getGameName(self, game):
return self.gameCfg(game, "name") return self.gameCfg(game, "name")
@@ -230,14 +242,65 @@ class source(MumoModule):
state.channel = cid state.channel = cid
server.setState(state) 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 source_cid = state.channel
target_cid = self.getOrCreateChannelFor(mumble_server, game, server, team) target_cid = self.getOrCreateChannelFor(mumble_server, game, server, team)
if source_cid != target_cid: if source_cid != target_cid:
self.moveUserToCid(mumble_server, state, target_cid) self.moveUserToCid(mumble_server, state, target_cid)
user.state.channel = target_cid
self.users.addOrUpdate(sid, state.session, user)
# TODO: Source channel deletion if unused 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 return True
def validGameType(self, game): def validGameType(self, game):
@@ -301,7 +364,6 @@ class source(MumoModule):
self.log().debug("(%d) (%d|%d) " + what, sid, state.session, state.userid, *argc) self.log().debug("(%d) (%d|%d) " + what, sid, state.session, state.userid, *argc)
def handle(self, server, new_state): def handle(self, server, new_state):
log = self.log()
sid = server.id() sid = server.id()
session = new_state.session session = new_state.session
@@ -317,21 +379,14 @@ class source(MumoModule):
game, game_server = self.parseSourceContext(new_state.context) game, game_server = self.parseSourceContext(new_state.context)
identity = self.parseSourceIdentity(new_state.identity) identity = self.parseSourceIdentity(new_state.identity)
self.dlog(sid, new_state, "Context: '%s' -> '%s'", game, game_server) self.dlog(sid, new_state, "Context: %s -> '%s' / '%s'", repr(new_state.context), game, game_server)
self.dlog(sid, new_state, "Identity: '%s'", identity) self.dlog(sid, new_state, "Identity: %s -> '%s'", repr(new_state.identity), identity)
updated_user = User(new_state, identity, game, game_server) updated_user = User(new_state, identity, game, game_server)
self.dlog(sid, new_state, "Starting transition") self.dlog(sid, new_state, "Starting transition")
self.userTransition(server, old_user, updated_user) self.userTransition(server, old_user, updated_user)
self.dlog(sid, new_state, "Transition complete")
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")
# #
#--- Server callback functions #--- Server callback functions

View File

@@ -106,4 +106,13 @@ class UserRegistry(object):
return False return False
return True 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