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
|
; 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.
|
||||||
|
@@ -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()
|
||||||
|
@@ -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()
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user