Add capability to delete unused channels to source plugin
This commit is contained in:
@@ -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.
|
||||
|
@@ -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()
|
||||
|
@@ -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()
|
@@ -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
|
||||
#
|
||||
|
@@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user