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
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.

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
#

View File

@@ -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