Add source module to mumo
The source module is inspired by the bf2 module and is supposed to allow on-the-fly context/identity based player management for source engine based games. Currently supported games are TF2, DOD:S, CStrike:Source and HL2DM. This commit adds the basic structure with unit tests for many of the components. For now only dynamic channel creation and player movement is implemented. Left todo are dynamic ACL/Group management, unused channel deletion, server channel re-use and, most importantly, actual testing beyond the small unit test coverage.
This commit is contained in:
parent
0ac8b542a9
commit
4b61aa2fec
@ -55,6 +55,7 @@ somestr = Blabla
|
||||
somenum = 10
|
||||
testfallbacknum = asdas
|
||||
blubber = Things %(doesnotexistsasdefault)s
|
||||
serverregex = ^\[[\w\d\-\(\):]{1,20}\]$
|
||||
[Server_10]
|
||||
value = False
|
||||
[Server_9]
|
||||
@ -66,7 +67,8 @@ value = True
|
||||
('somestr', str, "fail"),
|
||||
('somenum', int, 0),
|
||||
('somenumtest', int, 1),
|
||||
('blubber', str, "empty")),
|
||||
('blubber', str, "empty"),
|
||||
('serverregex', re.compile, '.*')),
|
||||
(lambda x: re.match("Server_\d+",x)):(('value', x2bool, True),),
|
||||
'somethingelse':(('bla', str, "test"),)}
|
||||
|
||||
@ -121,6 +123,7 @@ value = True
|
||||
assert(cfg.world.somenum == 10)
|
||||
self.assertRaises(AttributeError, getattr, cfg.world, "testfallbacknum")
|
||||
self.assertEqual(cfg.world.blubber, "Things %(doesnotexistsasdefault)s")
|
||||
self.assertEqual(cfg.world.serverregex, re.compile("^\[[\w\d\-\(\):]{1,20}\]$"))
|
||||
assert(cfg.somethingelse.bla == "test")
|
||||
assert(cfg.Server_10.value == False)
|
||||
assert(cfg.Server_2.value == True)
|
||||
|
77
modules-available/source.ini
Normal file
77
modules-available/source.ini
Normal file
@ -0,0 +1,77 @@
|
||||
;
|
||||
; This is a sample configuration file for the mumo source module.
|
||||
; The source module manages ACL/channel movements based on source
|
||||
; gamestate reported by Mumble positional audio plugins
|
||||
;
|
||||
|
||||
; The plugin creates needed channels on demand and re-uses
|
||||
; existing ones if available. After creation the channel
|
||||
; is identified by ID and can be renamed without breaking
|
||||
; the mapping.
|
||||
|
||||
[source]
|
||||
; Database file to hold channel mappings
|
||||
database = source.sqlite
|
||||
; Channel ID of root channel to create channels in
|
||||
basechannelid = 0
|
||||
; Comma seperated list of mumble servers to operate on, leave empty for all
|
||||
mumbleservers =
|
||||
; Regular expression for game name restriction
|
||||
; Restrict to sane game names to prevent injection of malicious game
|
||||
; names into the plugin. Can also be used to restrict game types.
|
||||
gameregex = ^(tf|dod|cstrike|hl2mp)$
|
||||
|
||||
; Prefix to use for groups used in this plugin
|
||||
; Be aware that this combined with gameregex prevents spoofing
|
||||
; of existing groups
|
||||
groupprefix = source_
|
||||
|
||||
; Configuration section valid for all games for which no
|
||||
; specfic game rule is given.
|
||||
|
||||
[generic]
|
||||
; name and server channelname support the following variables:
|
||||
; %(game)s - Shortname of the game
|
||||
; %(server)s - Unique id of the server
|
||||
|
||||
; Game name to use for game channel
|
||||
name = %(game)s
|
||||
|
||||
; Channel to create for server below gamechannel
|
||||
servername = %(server)s
|
||||
|
||||
; Comma seperated list of default team channel names
|
||||
teams = Lobby, Spectator, Team one, Team two, Team three, Team four
|
||||
|
||||
; When creating a channel setup ACLs to restrict it to players
|
||||
restrict = true
|
||||
|
||||
; Create base/server-channels on-demand
|
||||
createifmissing = true
|
||||
; Delete channels as soon as the last player is gone
|
||||
deleteifunused = false
|
||||
|
||||
; Regular expression for server restriction.
|
||||
; Will be checked against steam server id.
|
||||
; Use this to restrict to private servers.
|
||||
serverregex = ^\[[\w\d\-\(\):]{1,20}\]$
|
||||
|
||||
; Game specific sections overriding settings of the
|
||||
; [generic] section.
|
||||
[game:tf]
|
||||
name = Team Fortress 2
|
||||
teams = Lobby, Spectator, Blue, Red
|
||||
|
||||
[game:dod]
|
||||
name = Day of Defeat : Source
|
||||
teams = Lobby, Spectator, U.S. Army, Wehrmacht
|
||||
|
||||
[game:cstrike]
|
||||
name = Counterstrike Source
|
||||
teams = Lobby, Spectator, Terrorist, CT Forces
|
||||
|
||||
[game:hl2mp]
|
||||
name = HL2: Death Match
|
||||
teams = Lobby, Spectator, Combine, Rebels
|
||||
|
||||
|
1
modules/source/__init__.py
Normal file
1
modules/source/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from source import source
|
96
modules/source/db.py
Normal file
96
modules/source/db.py
Normal file
@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8
|
||||
|
||||
# Copyright (C) 2013 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
|
||||
# - Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# - Neither the name of the Mumble Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import sqlite3
|
||||
|
||||
class SourceDB(object):
|
||||
def __init__(self, path = ":memory:"):
|
||||
self.db = sqlite3.connect(path)
|
||||
if self.db:
|
||||
self.db.execute("CREATE TABLE IF NOT EXISTS source(sid INTEGER, cid INTEGER, game TEXT, server TEXT, team INTEGER)")
|
||||
self.db.commit()
|
||||
|
||||
def close(self):
|
||||
if self.db:
|
||||
self.db.commit()
|
||||
self.db.close()
|
||||
self.db = None
|
||||
|
||||
def isOk(self):
|
||||
""" True if the database is correctly initialized """
|
||||
return self.db != None
|
||||
|
||||
def cidFor(self, sid, game, server = None, team = None):
|
||||
assert(sid != None and game != None)
|
||||
assert(not (team != None and server == None))
|
||||
|
||||
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 registerChannel(self, sid, cid, game, server = None, team = None):
|
||||
assert(sid != None and game != None)
|
||||
assert(not (team != None and server == None))
|
||||
|
||||
self.db.execute("INSERT INTO source (sid, cid, game, server, team) VALUES (?,?,?,?,?)", [sid, cid, game, server, team])
|
||||
self.db.commit()
|
||||
return True
|
||||
|
||||
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])
|
||||
|
||||
self.db.commit()
|
||||
|
||||
def dropChannel(self, sid, cid):
|
||||
""" Drops channel with given sid + cid """
|
||||
self.db.execute("DELETE FROM source WHERE sid is ? and cid is ?", [sid, cid])
|
||||
self.db.commit()
|
||||
|
||||
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()
|
||||
|
||||
def reset(self):
|
||||
""" Deletes everything in the database """
|
||||
self.db.execute("DELETE FROM source")
|
||||
self.db.commit()
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
145
modules/source/db_test.py
Normal file
145
modules/source/db_test.py
Normal file
@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8
|
||||
|
||||
# Copyright (C) 2013 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
|
||||
# - Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# - Neither the name of the Mumble Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import unittest
|
||||
from db import SourceDB
|
||||
|
||||
class SourceDBTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.db = SourceDB()
|
||||
|
||||
def tearDown(self):
|
||||
self.db.close()
|
||||
|
||||
|
||||
def testOk(self):
|
||||
self.db.reset()
|
||||
|
||||
self.assertTrue(self.db.isOk())
|
||||
|
||||
def testSingleChannel(self):
|
||||
self.db.reset()
|
||||
|
||||
sid = 5; cid = 10; game = "tf2"; server = "abc[]def"; team = "1"
|
||||
self.assertTrue(self.db.registerChannel(sid, cid, game, server, team))
|
||||
self.assertEqual(self.db.cidFor(sid, game, server, team), cid)
|
||||
self.db.unregisterChannel(sid, game, server, team)
|
||||
self.assertEqual(self.db.cidFor(sid, game, server, team), None)
|
||||
|
||||
def testChannelTree(self):
|
||||
self.db.reset()
|
||||
|
||||
sid = 5; game = "tf2"; server = "abc[]def"; team = 0
|
||||
bcid = 10; scid = 11; tcid = 12
|
||||
|
||||
self.assertTrue(self.db.registerChannel(sid, 1, "canary", server, team))
|
||||
|
||||
# Delete whole tree
|
||||
|
||||
self.assertTrue(self.db.registerChannel(sid, bcid, game))
|
||||
self.assertTrue(self.db.registerChannel(sid, scid, game, server))
|
||||
self.assertTrue(self.db.registerChannel(sid, tcid, game, server, team))
|
||||
|
||||
self.assertEqual(self.db.cidFor(sid, game), bcid)
|
||||
self.assertEqual(self.db.cidFor(sid, game, server), scid)
|
||||
self.assertEqual(self.db.cidFor(sid, game, server, team), tcid)
|
||||
self.assertEqual(self.db.cidFor(sid+1, game, server, team), None)
|
||||
|
||||
self.db.unregisterChannel(sid, game)
|
||||
|
||||
self.assertEqual(self.db.cidFor(sid, game, server, team), None)
|
||||
self.assertEqual(self.db.cidFor(sid, game, server), None)
|
||||
self.assertEqual(self.db.cidFor(sid, game), None)
|
||||
|
||||
# Delete server channel
|
||||
|
||||
self.assertTrue(self.db.registerChannel(sid, bcid, game))
|
||||
self.assertTrue(self.db.registerChannel(sid, scid, game, server))
|
||||
self.assertTrue(self.db.registerChannel(sid, tcid, game, server, team))
|
||||
|
||||
self.db.unregisterChannel(sid, game, server)
|
||||
|
||||
self.assertEqual(self.db.cidFor(sid, game), bcid)
|
||||
self.assertEqual(self.db.cidFor(sid, game, server), None)
|
||||
self.assertEqual(self.db.cidFor(sid, game, server, team), None)
|
||||
|
||||
self.db.unregisterChannel(sid, game)
|
||||
|
||||
# Delete team channel
|
||||
|
||||
self.assertTrue(self.db.registerChannel(sid, bcid, game))
|
||||
self.assertTrue(self.db.registerChannel(sid, scid, game, server))
|
||||
self.assertTrue(self.db.registerChannel(sid, tcid, game, server, team))
|
||||
|
||||
self.db.unregisterChannel(sid, game, server, team)
|
||||
|
||||
self.assertEqual(self.db.cidFor(sid, game), bcid)
|
||||
self.assertEqual(self.db.cidFor(sid, game, server), scid)
|
||||
self.assertEqual(self.db.cidFor(sid, game, server, team), None)
|
||||
|
||||
self.db.unregisterChannel(sid, game)
|
||||
|
||||
# Check canary
|
||||
self.assertEqual(self.db.cidFor(sid, "canary", server, team), 1)
|
||||
self.db.unregisterChannel(sid, "canary", server, team)
|
||||
|
||||
def testDropChannel(self):
|
||||
self.db.reset()
|
||||
|
||||
sid = 1; cid = 5; game = "tf"
|
||||
self.db.registerChannel(sid, cid, game)
|
||||
self.db.dropChannel(sid + 1, cid)
|
||||
self.assertEqual(self.db.cidFor(sid, game), cid)
|
||||
self.db.dropChannel(sid, cid)
|
||||
self.assertEqual(self.db.cidFor(sid, game), None)
|
||||
|
||||
def testRegisteredChannels(self):
|
||||
self.db.reset()
|
||||
|
||||
sid = 5; game = "tf2"; server = "abc[]def"; team = 1
|
||||
bcid = 10; scid = 11; tcid = 12;
|
||||
|
||||
self.db.registerChannel(sid, bcid, game)
|
||||
self.db.registerChannel(sid, scid, game, server)
|
||||
self.db.registerChannel(sid+1, tcid, game, server, team)
|
||||
self.db.registerChannel(sid, tcid, game, server, team)
|
||||
|
||||
|
||||
expected = [(sid, bcid, game, None, None),
|
||||
(sid, scid, game, server, None),
|
||||
(sid, tcid, game, server, team),
|
||||
(sid+1, tcid, game, server, team)]
|
||||
|
||||
self.assertEqual(self.db.registeredChannels(), expected)
|
||||
|
||||
if __name__ == "__main__":
|
||||
#import sys;sys.argv = ['', 'Test.testName']
|
||||
unittest.main()
|
371
modules/source/source.py
Normal file
371
modules/source/source.py
Normal file
@ -0,0 +1,371 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8
|
||||
|
||||
# Copyright (C) 2013 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
|
||||
# - Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# - Neither the name of the Mumble Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#
|
||||
# source.py
|
||||
# This module manages ACL/channel movements based on source
|
||||
# gamestate reported by Mumble positional audio plugins
|
||||
#
|
||||
|
||||
from mumo_module import (MumoModule,
|
||||
commaSeperatedIntegers,
|
||||
commaSeperatedStrings,
|
||||
x2bool)
|
||||
|
||||
from db import SourceDB
|
||||
from users import (User, UserRegistry)
|
||||
|
||||
import re
|
||||
|
||||
class source(MumoModule):
|
||||
default_game_config = (
|
||||
('name', str, "%(game)s"),
|
||||
('servername', str, "%(server)s"),
|
||||
('teams', commaSeperatedStrings, ["Lobby", "Spectator", "Team one", "Team two", "Team three", "Team four"]),
|
||||
('groups', commaSeperatedStrings, ["%(game)s", "%(game)s_%(team)s", "%(game)s_%(team)s_%(channelid)d"]),
|
||||
('restrict', x2bool, True),
|
||||
('serverregex', re.compile, re.compile("^\[[\w\d\-\(\):]{1,20}\]$")),
|
||||
('createifmissing', x2bool, True),
|
||||
('deleteifunused', x2bool, False)
|
||||
)
|
||||
|
||||
default_config = {'source':(
|
||||
('database', str, "source.sqlite"),
|
||||
('basechannelid', int, 0),
|
||||
('servers', commaSeperatedIntegers, []),
|
||||
('gameregex', re.compile, re.compile("^(tf|dod|cstrike)$"))
|
||||
),
|
||||
|
||||
# The generic section defines default values which can be overridden in
|
||||
# optional game specific "game:<gameshorthand>" sections
|
||||
|
||||
'generic': default_game_config,
|
||||
lambda x: re.match('^game:\w+$', x): default_game_config
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, name, manager, configuration=None):
|
||||
MumoModule.__init__(self, name, manager, configuration)
|
||||
self.murmur = manager.getMurmurModule()
|
||||
|
||||
def onStart(self):
|
||||
MumoModule.onStart(self)
|
||||
cfg = self.cfg()
|
||||
self.db = SourceDB(cfg.source.database)
|
||||
|
||||
def onStop(self):
|
||||
MumoModule.onStop(self)
|
||||
self.db.close()
|
||||
|
||||
def connected(self):
|
||||
cfg = self.cfg()
|
||||
manager = self.manager()
|
||||
log = self.log()
|
||||
log.debug("Register for Server callbacks")
|
||||
|
||||
self.meta = manager.getMeta()
|
||||
|
||||
servers = set(cfg.source.servers)
|
||||
if not servers:
|
||||
servers = manager.SERVERS_ALL
|
||||
|
||||
self.users = UserRegistry()
|
||||
|
||||
self.validateChannelDB()
|
||||
|
||||
manager.subscribeServerCallbacks(self, servers)
|
||||
manager.subscribeMetaCallbacks(self, servers)
|
||||
|
||||
|
||||
def validateChannelDB(self):
|
||||
log = self.log()
|
||||
log.debug("Validating channel database")
|
||||
|
||||
current_sid = -1
|
||||
current_mumble_server = None
|
||||
|
||||
for sid, cid, _, _, _ in self.db.registeredChannels():
|
||||
if current_sid != sid:
|
||||
current_mumble_server = self.meta.getServer(sid)
|
||||
current_sid = sid
|
||||
|
||||
try:
|
||||
current_mumble_server.getChannelState(cid)
|
||||
#TODO: Verify ACL?
|
||||
except self.murmur.InvalidChannelException:
|
||||
# Channel no longer exists
|
||||
log.debug("(%d) Channel %d no longer exists. Dropped.", sid, cid)
|
||||
self.db.dropChannel(sid, cid)
|
||||
|
||||
def disconnected(self): pass
|
||||
|
||||
def userTransition(self, server, old, new):
|
||||
sid = server.id()
|
||||
|
||||
assert(not old or old.valid())
|
||||
|
||||
relevant = old or (new and new.valid())
|
||||
if not relevant:
|
||||
return
|
||||
|
||||
user_new = not old and new
|
||||
user_gone = old and (not new or not new.valid())
|
||||
|
||||
if not user_new:
|
||||
# Nuke previous group memberships if any
|
||||
|
||||
#TODO: Remove group memberships
|
||||
|
||||
pass
|
||||
|
||||
if not user_gone:
|
||||
#TODO: Establish new group memberships
|
||||
self.moveUser(server,
|
||||
new.state,
|
||||
new.game,
|
||||
new.server,
|
||||
new.identity["team"])
|
||||
else:
|
||||
# User gone
|
||||
if not new:
|
||||
self.dlog(sid, old.state, "User gone")
|
||||
else:
|
||||
self.dlog(sid, old.state, "User stopped playing")
|
||||
|
||||
|
||||
def getGameName(self, game):
|
||||
return self.gameCfg(game, "name")
|
||||
|
||||
def getServerName(self, game):
|
||||
return self.gameCfg(game, "servername")
|
||||
|
||||
def getTeamName(self, game, index):
|
||||
try:
|
||||
return self.gameCfg(game, "teams")[index]
|
||||
except IndexError:
|
||||
return str(index)
|
||||
|
||||
|
||||
def getOrCreateGameChannelFor(self, mumble_server, game, server, sid, cfg, log, namevars):
|
||||
game_cid = self.db.cidFor(sid, game)
|
||||
if game_cid == None:
|
||||
game_channel_name = self.getGameName(game) % namevars
|
||||
log.debug("(%d) Creating game channel '%s' below %d", sid, game_channel_name, cfg.source.basechannelid)
|
||||
game_cid = mumble_server.addChannel(game_channel_name, cfg.source.basechannelid)
|
||||
self.db.registerChannel(sid, game_cid, game) # Make sure we don't have orphaned server channels around
|
||||
self.db.unregisterChannel(sid, game, server)
|
||||
log.debug("(%d) Game channel created and registered (cid %d)", sid, game_cid)
|
||||
return game_cid
|
||||
|
||||
|
||||
def getOrCreateServerChannelFor(self, mumble_server, game, server, team, sid, log, namevars, game_cid):
|
||||
server_cid = self.db.cidFor(sid, game, server)
|
||||
if server_cid == None:
|
||||
server_channel_name = self.getServerName(game) % namevars
|
||||
log.debug("(%d) Creating server channel '%s' below %d", sid, server_channel_name, game_cid)
|
||||
server_cid = mumble_server.addChannel(server_channel_name, game_cid)
|
||||
self.db.registerChannel(sid, server_cid, game, server)
|
||||
self.db.unregisterChannel(sid, game, server, team) # Make sure we don't have orphaned team channels around
|
||||
log.debug("(%d) Server channel created and registered (cid %d)", sid, server_cid)
|
||||
return server_cid
|
||||
|
||||
|
||||
def getOrCreateTeamChannelFor(self, mumble_server, game, server, team, sid, log, server_cid):
|
||||
team_cid = self.db.cidFor(sid, game, server, team)
|
||||
if team_cid == None:
|
||||
team_channel_name = self.getTeamName(game, team)
|
||||
log.debug("(%d) Creating team channel '%s' below %d", sid, team_channel_name, server_cid)
|
||||
team_cid = mumble_server.addChannel(team_channel_name, server_cid)
|
||||
self.db.registerChannel(sid, team_cid, game, server, team)
|
||||
log.debug("(%d) Team channel created and registered (cid %d)", sid, team_cid)
|
||||
return team_cid
|
||||
|
||||
def getOrCreateChannelFor(self, mumble_server, game, server, team):
|
||||
sid = mumble_server.id()
|
||||
cfg = self.cfg()
|
||||
log = self.log()
|
||||
|
||||
#TODO: Apply ACLs if needed
|
||||
#TODO: Make robust against channel changes not in the db
|
||||
|
||||
namevars = {'game' : game,
|
||||
'server' : server}
|
||||
|
||||
game_cid = self.getOrCreateGameChannelFor(mumble_server, game, server, sid, cfg, log, namevars)
|
||||
server_cid = self.getOrCreateServerChannelFor(mumble_server, game, server, team, sid, log, namevars, game_cid)
|
||||
team_cid = self.getOrCreateTeamChannelFor(mumble_server, game, server, team, sid, log, server_cid)
|
||||
|
||||
return team_cid
|
||||
|
||||
def moveUserToCid(self, server, state, cid):
|
||||
self.dlog(server.id(), state, "Moving from channel %d to %d", state.channel, cid)
|
||||
state.channel = cid
|
||||
server.setState(state)
|
||||
|
||||
def moveUser(self, mumble_server, state, game, server, team):
|
||||
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
|
||||
|
||||
return True
|
||||
|
||||
def validGameType(self, game):
|
||||
return self.cfg().source.gameregex.match(game) != None
|
||||
|
||||
def validServer(self, game, server):
|
||||
return self.gameCfg(game, "serverregex").match(server) != None
|
||||
|
||||
def parseSourceContext(self, context):
|
||||
"""
|
||||
Parse source engine context string. Returns tuple with
|
||||
game name and server identification. Returns None for both
|
||||
if context string is invalid.
|
||||
"""
|
||||
try:
|
||||
prefix, server = context.split('\x00')[0:2]
|
||||
source, game = [s.strip() for s in prefix.split(':', 1)]
|
||||
|
||||
if source != "Source engine":
|
||||
# Not a source engine context
|
||||
return (None, None)
|
||||
|
||||
if not self.validGameType(game) or not self.validServer(game, server):
|
||||
return (None, None)
|
||||
|
||||
return (game, server)
|
||||
|
||||
except (AttributeError, ValueError),e:
|
||||
return (None, None);
|
||||
|
||||
def parseSourceIdentity(self, identity):
|
||||
"""
|
||||
Parse comma separated source engine identity string key value pairs
|
||||
and return them as a dict. Returns None for invalid identity strings.
|
||||
|
||||
Usage: parseSourceIndentity("universe:0;account_type:0;id:00000000;instance:0;team:0")
|
||||
"""
|
||||
try:
|
||||
# For now all values are integers
|
||||
d = {k:int(v) for k, v in [var.split(':', 1) for var in identity.split(';')]}
|
||||
|
||||
# Make sure mandatory values are present
|
||||
if not "team" in d: return None
|
||||
|
||||
return d
|
||||
except (AttributeError, ValueError):
|
||||
return None
|
||||
|
||||
def gameCfg(self, game, variable):
|
||||
"""Return the game specific value for the given variable if it exists. Otherwise the generic value"""
|
||||
sectionname = "game:" + game
|
||||
cfg = self.cfg()
|
||||
|
||||
if sectionname not in cfg:
|
||||
return cfg.generic[variable]
|
||||
|
||||
return cfg[sectionname][variable]
|
||||
|
||||
def dlog(self, sid, state, what, *argc):
|
||||
""" Debug log output helper for user state related things """
|
||||
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
|
||||
|
||||
self.dlog(sid, new_state, "Handle state change")
|
||||
|
||||
old_user = self.users.get(sid, session)
|
||||
|
||||
if old_user and not old_user.hasContextOrIdentityChanged(new_state):
|
||||
# No change in relevant fields. Simply update state for reference
|
||||
old_user.updateState(new_state)
|
||||
self.dlog(sid, new_state, "State change irrelevant for plugin")
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
#
|
||||
#--- Server callback functions
|
||||
#
|
||||
|
||||
def userDisconnected(self, server, state, context=None):
|
||||
sid = server.id()
|
||||
session = state.session
|
||||
|
||||
self.userTransition(server, self.users.get(sid, session), None)
|
||||
|
||||
def userStateChanged(self, server, state, context=None):
|
||||
self.handle(server, state)
|
||||
|
||||
def userConnected(self, server, state, context=None):
|
||||
self.handle(server, state)
|
||||
|
||||
def channelRemoved(self, server, state, context=None):
|
||||
cid = state.id
|
||||
sid = server.id()
|
||||
|
||||
self.log().debug("(%d) Channel %d removed.", sid, cid)
|
||||
self.db.dropChannel(sid, cid)
|
||||
|
||||
def userTextMessage(self, server, user, message, current=None): pass
|
||||
def channelCreated(self, server, state, context=None): pass
|
||||
def channelStateChanged(self, server, state, context=None): pass
|
||||
|
||||
#
|
||||
#--- Meta callback functions
|
||||
#
|
||||
|
||||
def started(self, server, context=None):
|
||||
self.log().debug("Started")
|
||||
|
||||
def stopped(self, server, context=None):
|
||||
self.log().debug("Stopped")
|
345
modules/source/source_test.py
Normal file
345
modules/source/source_test.py
Normal file
@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8
|
||||
|
||||
# Copyright (C) 2013 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
|
||||
# - Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# - Neither the name of the Mumble Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import unittest
|
||||
import Queue
|
||||
import config
|
||||
import re
|
||||
import logging
|
||||
import source
|
||||
|
||||
class InvalidChannelExceptionMock(Exception):
|
||||
pass
|
||||
|
||||
class StateMock():
|
||||
def __init__(self, cid = 0, session = 0, userid = -1):
|
||||
self.channel = cid
|
||||
self.session = session
|
||||
self.userid = userid
|
||||
|
||||
class ServerMock():
|
||||
def __init__(self, sid):
|
||||
self.sid = sid
|
||||
self.reset()
|
||||
|
||||
def id(self):
|
||||
return self.sid
|
||||
|
||||
def lastChannelID(self):
|
||||
return self.uid
|
||||
|
||||
def addChannel(self, name, parent):
|
||||
self.name.append(name)
|
||||
self.parent.append(parent)
|
||||
|
||||
self.uid += 1
|
||||
return self.uid
|
||||
|
||||
|
||||
def getChannelState(self, cid):
|
||||
if not cid in self.channel:
|
||||
raise InvalidChannelExceptionMock()
|
||||
return {'fake':True}
|
||||
|
||||
def setState(self, state):
|
||||
self.user_state.append(state)
|
||||
|
||||
def reset(self):
|
||||
self.uid = 1000
|
||||
self.name = []
|
||||
self.parent = []
|
||||
self.user_state = []
|
||||
|
||||
class MurmurMock():
|
||||
InvalidChannelException = InvalidChannelExceptionMock
|
||||
def __init__(self):
|
||||
self.s = ServerMock(1)
|
||||
|
||||
def getServer(self, sid):
|
||||
assert(sid == self.s.id())
|
||||
return self.s
|
||||
|
||||
def reset(self):
|
||||
self.s.reset()
|
||||
|
||||
class ManagerMock():
|
||||
SERVERS_ALL = [-1]
|
||||
|
||||
def __init__(self):
|
||||
self.q = Queue.Queue()
|
||||
self.m = MurmurMock()
|
||||
|
||||
def getQueue(self):
|
||||
return self.q
|
||||
|
||||
def getMurmurModule(self):
|
||||
return self.m
|
||||
|
||||
def subscribeServerCallbacks(self, callback, servers):
|
||||
self.serverCB = {'callback' : callback, 'servers' : servers}
|
||||
|
||||
def subscribeMetaCallbacks(self, callback, servers):
|
||||
self.metaCB = {'callback' : callback, 'servers' : servers}
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.mm = ManagerMock();
|
||||
self.mserv = self.mm.m.getServer(1)
|
||||
|
||||
|
||||
testconfig = config.Config(None, source.source.default_config)
|
||||
testconfig.source.database = ":memory:"
|
||||
|
||||
# As it is hard to create the read only config structure from
|
||||
# hand use a spare one to steal from
|
||||
spare = config.Config(None, source.source.default_config)
|
||||
testconfig.__dict__['game:tf'] = spare.generic
|
||||
testconfig.__dict__['game:tf'].name = "Team Fortress 2"
|
||||
testconfig.__dict__['game:tf'].teams = ["Lobby", "Spectator", "Blue", "Red"]
|
||||
testconfig.__dict__['game:tf'].serverregex = re.compile("^\[A-1:123\]$")
|
||||
testconfig.__dict__['game:tf'].servername = "Test %(game)s %(server)s"
|
||||
|
||||
self.s = source.source("source", self.mm, testconfig)
|
||||
self.mm.s = self.s
|
||||
|
||||
# Since we don't want to run threaded if we don't have to
|
||||
# emulate startup to the derived class function
|
||||
self.s.onStart()
|
||||
self.s.connected()
|
||||
|
||||
# Critical test assumption
|
||||
self.assertEqual(self.mm.metaCB['callback'], self.s)
|
||||
self.assertEqual(self.mm.serverCB['callback'], self.s)
|
||||
|
||||
def resetDB(self):
|
||||
self.s.db.db.execute("DELETE FROM source");
|
||||
|
||||
def resetState(self):
|
||||
self.resetDB()
|
||||
self.mm.m.reset()
|
||||
|
||||
def tearDown(self):
|
||||
self.s.disconnected()
|
||||
self.s.onStop()
|
||||
|
||||
def testDefaultConfig(self):
|
||||
self.resetState()
|
||||
|
||||
mm = ManagerMock()
|
||||
INVALIDFORCEDEFAULT = ""
|
||||
s = source.source("source", mm, INVALIDFORCEDEFAULT)
|
||||
self.assertNotEqual(s.cfg(), None)
|
||||
|
||||
def testConfiguration(self):
|
||||
self.resetState()
|
||||
|
||||
# Ensure the default configuration makes sense
|
||||
self.assertEqual(self.mm.serverCB['servers'], self.mm.SERVERS_ALL)
|
||||
self.assertEqual(self.mm.metaCB['servers'], self.mm.SERVERS_ALL)
|
||||
|
||||
self.assertEqual(self.s.cfg().source.basechannelid, 0)
|
||||
self.assertEqual(self.s.cfg().generic.name, "%(game)s")
|
||||
|
||||
self.assertEqual(self.s.gameCfg("wugu", "name"), "%(game)s")
|
||||
self.assertEqual(self.s.gameCfg("tf", "name"), "Team Fortress 2")
|
||||
|
||||
def testIdentityParser(self):
|
||||
self.resetState()
|
||||
|
||||
expected = {"universe" : 1,
|
||||
"account_type" : 2,
|
||||
"id" : 3,
|
||||
"instance" : 4,
|
||||
"team" : 5}
|
||||
|
||||
got = self.s.parseSourceIdentity("universe:1;account_type:2;id:00000003;instance:4;team:5")
|
||||
self.assertDictEqual(expected, got)
|
||||
|
||||
got = self.s.parseSourceIdentity("universe:1;account_type:2;id:00000003;instance:4;")
|
||||
self.assertEqual(got, None, "Required team variable missing")
|
||||
|
||||
self.assertEqual(self.s.parseSourceIdentity(None), None)
|
||||
self.assertEqual(self.s.parseSourceIdentity(""), None)
|
||||
self.assertEqual(self.s.parseSourceIdentity("whatever:4;dskjfskjdfkjsfkjsfkj"), None)
|
||||
|
||||
def testContextParser(self):
|
||||
self.resetState()
|
||||
|
||||
none = (None, None)
|
||||
self.assertEqual(self.s.parseSourceContext(None), none)
|
||||
self.assertEqual(self.s.parseSourceContext(""), none)
|
||||
self.assertEqual(self.s.parseSourceContext("whatever:4;skjdakjkjwqdkjqkj"), none)
|
||||
|
||||
expected = ("dod", "[A-1:2807761920(3281)]")
|
||||
actual = self.s.parseSourceContext("Source engine: dod\x00[A-1:2807761920(3281)]\x00")
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
expected = ("dod", "[0:1]")
|
||||
actual = self.s.parseSourceContext("Source engine: dod\x00[0:1]\x00")
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
expected = ("cstrike", "[0:1]")
|
||||
actual = self.s.parseSourceContext("Source engine: cstrike\x00[0:1]\x00")
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
actual = self.s.parseSourceContext("Source engine: fake\x00[A-1:2807761920(3281)]\x00")
|
||||
self.assertEqual(none, actual)
|
||||
|
||||
actual = self.s.parseSourceContext("Source engine: cstrike\x0098vcv98re98ver98ver98v\x00")
|
||||
self.assertEqual(none, actual)
|
||||
|
||||
# Check alternate serverregex
|
||||
expected = ("tf", "[A-1:123]")
|
||||
actual = self.s.parseSourceContext("Source engine: tf\x00[A-1:123]\x00")
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
actual = self.s.parseSourceContext("Source engine: tf\x00[A-1:2807761920(3281)]\x00")
|
||||
self.assertEqual(none, actual)
|
||||
|
||||
def testGetOrCreateChannelFor(self):
|
||||
mumble_server = self.mserv
|
||||
|
||||
prev = mumble_server.lastChannelID()
|
||||
game = "tf"; server = "[A-1:123]"; team = 3
|
||||
cid = self.s.getOrCreateChannelFor(mumble_server, game, server, team)
|
||||
|
||||
self.assertEqual(3, cid - prev)
|
||||
|
||||
self.assertEqual(mumble_server.parent[0], 0)
|
||||
self.assertEqual(mumble_server.parent[1], prev + 1)
|
||||
self.assertEqual(mumble_server.parent[2], prev + 2)
|
||||
|
||||
self.assertEqual(mumble_server.name[0], "Team Fortress 2")
|
||||
self.assertEqual(mumble_server.name[1], "Test tf [A-1:123]")
|
||||
self.assertEqual(mumble_server.name[2], "Red")
|
||||
|
||||
sid = mumble_server.id()
|
||||
|
||||
self.assertEqual(self.s.db.cidFor(sid, game), prev + 1);
|
||||
self.assertEqual(self.s.db.cidFor(sid, game, server), prev + 2);
|
||||
self.assertEqual(self.s.db.cidFor(sid, game, server, team), prev + 3);
|
||||
|
||||
gotcid = self.s.getOrCreateChannelFor(mumble_server, game, server, team)
|
||||
self.assertEqual(cid, gotcid)
|
||||
|
||||
#print self.s.db.db.execute("SELECT * FROM source").fetchall()
|
||||
|
||||
def testGetGameName(self):
|
||||
self.resetState()
|
||||
|
||||
self.assertEqual(self.s.getGameName("tf"), "Team Fortress 2")
|
||||
self.assertEqual(self.s.getGameName("invalid"), "%(game)s");
|
||||
|
||||
def testGetServerName(self):
|
||||
self.resetState()
|
||||
|
||||
self.assertEqual(self.s.getServerName("tf"), "Test %(game)s %(server)s")
|
||||
self.assertEqual(self.s.getServerName("invalid"), "%(server)s");
|
||||
|
||||
def testGetTeamName(self):
|
||||
self.resetState()
|
||||
|
||||
self.assertEqual(self.s.getTeamName("tf", 2), "Blue")
|
||||
self.assertEqual(self.s.getTeamName("tf", 100), "100") #oob
|
||||
|
||||
self.assertEqual(self.s.getTeamName("invalid", 2), "Team one")
|
||||
self.assertEqual(self.s.getTeamName("invalid", 100), "100") #oob
|
||||
|
||||
def testValidGameType(self):
|
||||
self.resetState()
|
||||
|
||||
self.assertTrue(self.s.validGameType("dod"))
|
||||
self.assertTrue(self.s.validGameType("cstrike"))
|
||||
self.assertTrue(self.s.validGameType("tf"))
|
||||
|
||||
self.assertFalse(self.s.validGameType("dodx"))
|
||||
self.assertFalse(self.s.validGameType("xdod"))
|
||||
self.assertFalse(self.s.validGameType(""))
|
||||
|
||||
def testValidServer(self):
|
||||
self.resetState()
|
||||
|
||||
self.assertTrue(self.s.validServer("dod", "[A-1:2807761920(3281)]"))
|
||||
|
||||
self.assertFalse(self.s.validServer("dod", "A-1:2807761920(3281)]"))
|
||||
self.assertFalse(self.s.validServer("dod", "[A-1:2807761920(3281)"))
|
||||
self.assertFalse(self.s.validServer("dod", "[A-1:2807761920(3281)&]"))
|
||||
|
||||
self.assertTrue(self.s.validServer("tf", "[A-1:123]"))
|
||||
|
||||
self.assertFalse(self.s.validServer("tf", "x[A-1:123]"))
|
||||
self.assertFalse(self.s.validServer("tf", "[A-1:123]x"))
|
||||
|
||||
def testMoveUser(self):
|
||||
self.resetState()
|
||||
|
||||
mumble_server = self.mserv
|
||||
user_state = StateMock()
|
||||
prev = self.mserv.lastChannelID()
|
||||
|
||||
TEAM_BLUE = 2
|
||||
TEAM_RED = 3
|
||||
|
||||
BASE_SID = 0
|
||||
GAME_SID = prev + 1
|
||||
SERVER_SID = prev + 2
|
||||
TEAM_RED_SID = prev + 3
|
||||
TEAM_BLUE_SID = prev + 4
|
||||
|
||||
self.s.moveUser(self.mserv, user_state, "tf", "[A-1:123]", TEAM_BLUE)
|
||||
|
||||
self.assertEqual(mumble_server.parent[0], BASE_SID)
|
||||
self.assertEqual(mumble_server.parent[1], GAME_SID)
|
||||
self.assertEqual(mumble_server.parent[2], SERVER_SID)
|
||||
|
||||
self.assertEqual(mumble_server.name[0], "Team Fortress 2")
|
||||
self.assertEqual(mumble_server.name[1], "Test tf [A-1:123]")
|
||||
self.assertEqual(mumble_server.name[2], "Blue")
|
||||
self.assertEqual(len(mumble_server.name), 3)
|
||||
|
||||
self.assertEqual(user_state.channel, TEAM_RED_SID)
|
||||
self.assertEqual(mumble_server.user_state[0], user_state)
|
||||
|
||||
self.s.moveUser(self.mserv, user_state, "tf", "[A-1:123]", TEAM_RED)
|
||||
|
||||
self.assertEqual(mumble_server.parent[3], SERVER_SID)
|
||||
self.assertEqual(mumble_server.name[3], "Red")
|
||||
self.assertEqual(len(mumble_server.parent), 4)
|
||||
|
||||
self.assertEqual(user_state.channel, TEAM_BLUE_SID)
|
||||
self.assertEqual(mumble_server.user_state[0], user_state)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
#logging.basicConfig(level = logging.DEBUG)
|
||||
#import sys;sys.argv = ['', 'Test.testName']
|
||||
unittest.main()
|
109
modules/source/users.py
Normal file
109
modules/source/users.py
Normal file
@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8
|
||||
|
||||
# Copyright (C) 2013 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
|
||||
# - Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# - Neither the name of the Mumble Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
class User(object):
|
||||
"""
|
||||
User to hold state as well as parsed data fields in a
|
||||
sane fashion.
|
||||
"""
|
||||
def __init__(self, state, identity=None, game=None, server=None):
|
||||
self.state = state
|
||||
self.identity = identity or {}
|
||||
self.server = server
|
||||
self.game = game
|
||||
|
||||
def valid(self):
|
||||
""" True if valid data is available for all fields """
|
||||
return self.state and self.identity and self.server and self.game
|
||||
|
||||
def hasContextOrIdentityChanged(self, otherstate):
|
||||
""" Checks whether the given state diverges from this users's """
|
||||
return self.state.context != otherstate.context or \
|
||||
self.state.identity != otherstate.identity
|
||||
|
||||
def updateState(self, state):
|
||||
""" Updates the state of this user """
|
||||
self.state = state
|
||||
|
||||
def updateData(self, identity, game, server):
|
||||
""" Updates the data fields for this user """
|
||||
self.identity = identity
|
||||
self.game = game
|
||||
self.server = server
|
||||
|
||||
class UserRegistry(object):
|
||||
"""
|
||||
Registry to store User objects for given servers
|
||||
and sessions.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.users = {} # {session:user, ...}
|
||||
|
||||
def get(self, sid, session):
|
||||
""" Return user or None from registry """
|
||||
try:
|
||||
return self.users[sid][session]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def add(self, sid, session, user):
|
||||
""" Add new user to registry """
|
||||
assert(isinstance(user, User))
|
||||
|
||||
if not sid in self.users:
|
||||
self.users[sid] = {session:user}
|
||||
elif not session in self.users[sid]:
|
||||
self.users[sid][session] = user
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def addOrUpdate(self, sid, session, user):
|
||||
""" Add user or overwrite existing one """
|
||||
assert(isinstance(user, User))
|
||||
|
||||
if not sid in self.users:
|
||||
self.users[sid] = {session:user}
|
||||
else:
|
||||
self.users[sid][session] = user
|
||||
|
||||
return True
|
||||
|
||||
def remove(self, sid, session):
|
||||
""" Remove user from registry """
|
||||
try:
|
||||
del self.users[sid][session]
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
81
modules/source/users_test.py
Normal file
81
modules/source/users_test.py
Normal file
@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8
|
||||
|
||||
# Copyright (C) 2013 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
|
||||
# - Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# - Neither the name of the Mumble Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import unittest
|
||||
|
||||
from users import User, UserRegistry
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
def getSomeUsers(self, n =5):
|
||||
sid = []; session = []; user = []
|
||||
for i in range(n):
|
||||
s=str(i)
|
||||
sid.append(i) ; session.append(i)
|
||||
user.append(User("state"+s, "identity"+s, "game"+s, "server"+s))
|
||||
|
||||
return sid, session, user
|
||||
|
||||
def testRegistryCRUDOps(self):
|
||||
r = UserRegistry()
|
||||
|
||||
sid, session, user = self.getSomeUsers()
|
||||
|
||||
# Create & Read
|
||||
self.assertTrue(r.add(sid[0], session[0], user[0]))
|
||||
self.assertFalse(r.add(sid[0], session[0], user[0]))
|
||||
self.assertEqual(r.get(sid[0], session[0]), user[0])
|
||||
|
||||
self.assertTrue(r.addOrUpdate(sid[1], session[1], user[1]))
|
||||
self.assertEqual(r.get(sid[1], session[1]), user[1])
|
||||
|
||||
# Update
|
||||
self.assertTrue(r.addOrUpdate(sid[0], session[0], user[2]))
|
||||
self.assertEqual(r.get(sid[0], session[0]), user[2])
|
||||
|
||||
# Delete
|
||||
self.assertTrue(r.remove(sid[1], session[1]))
|
||||
self.assertFalse(r.remove(sid[1], session[1]))
|
||||
self.assertEqual(r.get(sid[1], session[1]), None)
|
||||
|
||||
self.assertTrue(r.remove(sid[0], session[0]))
|
||||
self.assertFalse(r.remove(sid[0], session[0]))
|
||||
self.assertEqual(r.get(sid[0], session[0]), None)
|
||||
|
||||
def testUser(self):
|
||||
u = User("State", {'team':2} , "tf", "Someserver")
|
||||
self.assertTrue(u.valid())
|
||||
self.assertFalse(User("State").valid())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
#import sys;sys.argv = ['', 'Test.testName']
|
||||
unittest.main()
|
16
testsuite.py
16
testsuite.py
@ -29,6 +29,16 @@
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from worker_test import *
|
||||
from config_test import *
|
||||
from mumo_manager_test import *
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
|
||||
from worker_test import *
|
||||
from config_test import *
|
||||
from mumo_manager_test import *
|
||||
|
||||
from modules.source.source_test import *
|
||||
from modules.source.users_test import *
|
||||
from modules.source.db_test import *
|
||||
|
||||
#import sys;sys.argv = ['', 'Test.testName']
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user