syncplay/syncplay/server.py
2013-07-03 17:53:21 +02:00

420 lines
18 KiB
Python

#coding:utf8
import hashlib
from twisted.internet import task, reactor
from twisted.internet.protocol import Factory
import syncplay
from syncplay.protocols import SyncServerProtocol
import time
from syncplay import constants
import threading
from syncplay.messages import getMessage
import codecs
import os
from string import Template
from ircBot import Bot as IRCBot
import argparse
class SyncFactory(Factory):
def __init__(self, password = '', motdFilePath = None, httpReplyFilePath= None, ircConfig = None, ircVerbose = False):
print getMessage("en", "welcome-server-notification").format(syncplay.version)
if(password):
password = hashlib.md5(password).hexdigest()
self.password = password
self._motdFilePath = motdFilePath
self._httpReplyFilePath = httpReplyFilePath
self._rooms = {}
self._roomStates = {}
self._roomUpdate = threading.RLock()
self.ircVerbose = ircVerbose
ircConnectionData = self.readIrcConfig(ircConfig)
if(ircConnectionData):
self.setupIRCBot(ircConnectionData)
def readIrcConfig(self, ircConfig):
if(ircConfig and os.path.isfile(ircConfig)):
cfg = codecs.open(ircConfig, "r", "utf-8-sig").read()
cfg = cfg.splitlines()
ircConnectionData = {
"server": "",
"port": "",
"nick": "",
"channel": ""
}
for line in cfg:
if("irc.server: " in line):
ircConnectionData['server'] = line.split(": ")[1]
elif("irc.serverPort: " in line):
ircConnectionData['port'] = int(line.split(": ")[1])
elif("irc.botName: " in line):
ircConnectionData['nick'] = line.split(": ")[1]
elif("irc.channel: " in line):
ircConnectionData['channel'] = line.split(": ")[1]
return ircConnectionData
def setupIRCBot(self, ircConnectionData):
botFunctions = {
"pause": self.ircPauseRoom,
"getRooms": self.getRooms,
"setRoomPosition": self.ircSetRoomPosition,
"getRoomPosition": self.getRoomPosition,
"getRoomUsers": self.getRoomUsernames,
"isRoomPaused": self.isRoomPaused,
}
try:
self.ircBot = IRCBot(
ircConnectionData['server'],
ircConnectionData['port'],
ircConnectionData['nick'],
ircConnectionData['channel'],
botFunctions,
)
self.ircBot.start()
except:
print "IRC Bot could not be started, please check your configuration"
def buildProtocol(self, addr):
return SyncServerProtocol(self)
def _createRoomIfDoesntExist(self, roomName):
if (not self._rooms.has_key(roomName)):
with self._roomUpdate:
self._rooms[roomName] = {}
self._roomStates[roomName] = {
"position": 0.0,
"paused": True,
"setBy": None,
"lastUpdate": time.time()
}
def addWatcher(self, watcherProtocol, username, roomName, roomPassword):
allnames = []
for room in self._rooms.itervalues():
for watcher in room.itervalues():
allnames.append(watcher.name.lower())
while username.lower() in allnames:
username += '_'
self._createRoomIfDoesntExist(roomName)
watcher = Watcher(self, watcherProtocol, username, roomName)
with self._roomUpdate:
self._rooms[roomName][watcherProtocol] = watcher
reactor.callLater(0.1, watcher.scheduleSendState)
l = lambda w: w.sendUserSetting(username, roomName, None, {"joined": True})
self.broadcast(watcherProtocol, l)
if(self.ircVerbose):
self.ircBot.sp_joined(username, roomName)
def getWatcher(self, watcherProtocol):
for room in self._rooms.itervalues():
if(room.has_key(watcherProtocol)):
return room[watcherProtocol]
def getAllWatchers(self, watcherProtocol): #TODO: Optimize me
watchers = {}
for room in self._rooms.itervalues():
for watcher in room.itervalues():
watchers[watcher.watcherProtocol] = watcher
return watchers
def _removeWatcherFromTheRoom(self, watcherProtocol):
for room in self._rooms.itervalues():
with self._roomUpdate:
watcher = room.pop(watcherProtocol, None)
if(watcher):
return watcher
def _deleteRoomIfEmpty(self, room):
if (self._rooms[room] == {}):
with self._roomUpdate:
self._rooms.pop(room)
self._roomStates.pop(room)
def getRoomPausedAndPosition(self, room):
position = self._roomStates[room]["position"]
paused = self._roomStates[room]["paused"]
if (not paused):
timePassedSinceSet = time.time() - self._roomStates[room]["lastUpdate"]
position += timePassedSinceSet
return paused, position
def getMotd(self, userIp, username, room):
if(self._motdFilePath and os.path.isfile(self._motdFilePath)):
tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read()
args = dict(version=syncplay.version, userIp=userIp, username=username, room=room)
try:
motd = Template(tmpl).substitute(args)
return motd if len(motd) < constants.SERVER_MAX_TEMPLATE_LENGTH else getMessage("en", "server-messed-up-motd-too-long").format(constants.SERVER_MAX_TEMPLATE_LENGTH, len(motd))
except ValueError:
return getMessage("en", "server-messed-up-motd-unescaped-placeholders")
else:
return ""
def gethttpRequestReply(self):
if(self._httpReplyFilePath and os.path.isfile(self._httpReplyFilePath)):
tmpl = codecs.open(self._httpReplyFilePath, "r", "utf-8-sig").read()
return tmpl.encode('utf-8')
else:
return getMessage("en", "server-default-http-reply")
def sendState(self, watcherProtocol, doSeek = False, senderLatency = 0, forcedUpdate = False):
watcher = self.getWatcher(watcherProtocol)
if(not watcher):
return
room = watcher.room
paused, position = self.getRoomPausedAndPosition(room)
setBy = self._roomStates[room]["setBy"]
watcher.paused = paused
watcher.position = position
watcherProtocol.sendState(position, paused, doSeek, setBy, senderLatency, watcher.latency, forcedUpdate)
if(time.time() - watcher.lastUpdate > constants.PROTOCOL_TIMEOUT):
watcherProtocol.drop()
self.removeWatcher(watcherProtocol)
def __updateWatcherPing(self, latencyCalculation, watcher):
if (latencyCalculation):
ping = (time.time() - latencyCalculation) / 2
if (watcher.latency):
watcher.latency = watcher.latency * (constants.PING_MOVING_AVERAGE_WEIGHT) + ping * (1-constants.PING_MOVING_AVERAGE_WEIGHT) #Exponential moving average
else:
watcher.latency = ping
def __shouldServerForceUpdateOnRoom(self, pauseChanged, doSeek):
return doSeek or pauseChanged
def __updatePausedState(self, paused, watcher):
watcher.paused = paused
if(self._roomStates[watcher.room]["paused"] <> paused):
self._roomStates[watcher.room]["setBy"] = watcher.name
self._roomStates[watcher.room]["paused"] = paused
self._roomStates[watcher.room]["lastUpdate"] = time.time()
return True
def __updatePositionState(self, position, doSeek, watcher):
watcher.position = position
if (doSeek):
self._roomStates[watcher.room]["position"] = position
self._roomStates[watcher.room]["setBy"] = watcher.name
self._roomStates[watcher.room]["lastUpdate"] = time.time()
else:
setter = min(self._rooms[watcher.room].values())
self._roomStates[watcher.room]["position"] = setter.position
self._roomStates[watcher.room]["setBy"] = setter.name
self._roomStates[watcher.room]["lastUpdate"] = setter.lastUpdate
def __notifyIrcBot(self, position, paused, doSeek, watcher, oldPosition, pauseChanged):
if (self.ircVerbose):
if (paused and pauseChanged):
self.ircBot.sp_paused(watcher.name, watcher.room)
elif (not paused and pauseChanged):
self.ircBot.sp_unpaused(watcher.name, watcher.room)
if (doSeek and position):
self.ircBot.sp_seek(watcher.name, oldPosition, position, watcher.room)
def updateWatcherState(self, watcherProtocol, position, paused, doSeek, latencyCalculation):
watcher = self.getWatcher(watcherProtocol)
self.__updateWatcherPing(latencyCalculation, watcher)
watcher.lastUpdate = time.time()
if(watcher.file):
oldPosition = self._roomStates[watcher.room]["position"]
pauseChanged = False
if(paused is not None):
pauseChanged = self.__updatePausedState(paused, watcher)
if(position is not None):
self.__updatePositionState(position, doSeek or pauseChanged, watcher)
forceUpdate = self.__shouldServerForceUpdateOnRoom(pauseChanged, doSeek)
if(forceUpdate):
self.__notifyIrcBot(position, paused, doSeek, watcher, oldPosition, pauseChanged)
l = lambda w: self.sendState(w, doSeek, watcher.latency, forceUpdate)
self.broadcastRoom(watcher.watcherProtocol, l)
def removeWatcher(self, watcherProtocol):
watcher = self.getWatcher(watcherProtocol)
if(not watcher):
return
l = lambda w: w.sendUserSetting(watcher.name, watcher.room, None, {"left": True})
self.broadcast(watcherProtocol, l)
self._removeWatcherFromTheRoom(watcherProtocol)
watcher.deactivate()
self._deleteRoomIfEmpty(watcher.room)
if(self.ircVerbose):
self.ircBot.sp_left(watcher.name, watcher.room)
def watcherGetUsername(self, watcherProtocol):
return self.getWatcher(watcherProtocol).name
def watcherGetRoom(self, watcherProtocol):
return self.getWatcher(watcherProtocol).room
def watcherSetRoom(self, watcherProtocol, room):
watcher = self._removeWatcherFromTheRoom(watcherProtocol)
if(not watcher):
return
watcher.resetStateTimer()
oldRoom = watcher.room
self._createRoomIfDoesntExist(room)
with self._roomUpdate:
self._rooms[room][watcherProtocol] = watcher
self._deleteRoomIfEmpty(oldRoom)
watcher.room = room
self.sendState(watcherProtocol, True)
l = lambda w: w.sendUserSetting(watcher.name, watcher.room, None, None)
self.broadcast(watcherProtocol, l)
def watcherSetFile(self, watcherProtocol, file_):
watcher = self.getWatcher(watcherProtocol)
watcher.file = file_
l = lambda w: w.sendUserSetting(watcher.name, watcher.room, watcher.file, None)
self.broadcast(watcherProtocol, l)
if(self.ircVerbose):
self.ircBot.sp_fileplaying(watcher.name, watcher.file['name'], watcher.room)
def broadcastRoom(self, sender, what):
room = self._rooms[self.watcherGetRoom(sender)]
if(room):
with self._roomUpdate:
for receiver in room:
what(receiver)
def broadcast(self, sender, what):
with self._roomUpdate:
for room in self._rooms.itervalues():
for receiver in room:
what(receiver)
def _findUserByUsername(self, username):
with self._roomUpdate:
for room in self._rooms.itervalues():
for user in room.itervalues():
if user.name == username:
return user
def ircPauseRoom(self, setBy, paused):
user = self._findUserByUsername(setBy)
if(user):
with self._roomUpdate:
if(self._roomStates[user.room]['paused'] != paused):
self._roomStates[user.room]['paused'] = paused
self._roomStates[user.room]['setBy'] = "IRC: " + setBy
if(paused):
self.ircBot.sp_paused("IRC: " + user.name, user.room)
elif(not paused):
self.ircBot.sp_unpaused("IRC: " + user.name, user.room)
l = lambda w: self.sendState(w, False, user.latency, True)
self.broadcastRoom(user.watcherProtocol, l)
def getRooms(self):
return self._rooms.keys()
def getRoomPosition(self, room):
with self._roomUpdate:
if room in self._roomStates:
return self._roomStates[room]["position"]
def ircSetRoomPosition(self, setBy, time):
user = self._findUserByUsername(setBy)
if(user):
with self._roomUpdate:
oldPosition = self._roomStates[user.room]['paused']
if(oldPosition - time > 1):
self._roomStates[user.room]['paused'] = time
self._roomStates[user.room]['setBy'] = "IRC: " + setBy
self.ircBot.sp_seek(user.name, oldPosition, time, user.room)
l = lambda w: self.sendState(w, True, user.latency, True)
self.broadcastRoom(user.watcherProtocol, l)
def getRoomUsernames(self, room):
l = []
with self._roomUpdate:
if room in self._rooms:
for user in self._rooms[room].itervalues():
if(user.file):
l.append({'nick': user.name, 'file': user.file['name'], "duration": user.file['duration']})
else:
l.append({'nick': user.name, 'file': None, "duration": None})
return l
def isRoomPaused(self, room):
with self._roomUpdate:
if room in self._roomStates:
return self._roomStates[room]["paused"]
class SyncIsolatedFactory(SyncFactory):
def broadcast(self, sender, what):
self.broadcastRoom(sender, what)
def getAllWatchers(self, watcherProtocol):
room = self.getWatcher(watcherProtocol).room
if(self._rooms.has_key(room)):
return self._rooms[room]
else:
return {}
def watcherSetRoom(self, watcherProtocol, room):
watcher = self.getWatcher(watcherProtocol)
oldRoom = watcher.room
l = lambda w: w.sendUserSetting(watcher.name, oldRoom, None, {"left": True})
self.broadcast(watcherProtocol, l)
SyncFactory.watcherSetRoom(self, watcherProtocol, room)
class Watcher(object):
def __init__(self, factory, watcherProtocol, name, room):
self.factory = factory
self.watcherProtocol = watcherProtocol
self.name = name
self.room = room
self.file = None
self._sendStateTimer = None
self.position = None
self.latency = 0
self.lastUpdate = time.time()
def __lt__(self, b):
if(self.position is None):
return False
elif(b.position is None):
return True
else:
return self.position < b.position
def getRoomPosition(self):
_, position = self.factory.getRoomPausedAndPosition(self.room)
return position
def scheduleSendState(self):
self._sendStateTimer = task.LoopingCall(self.sendState)
self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL, True)
def sendState(self):
self.factory.sendState(self.watcherProtocol)
def resetStateTimer(self):
if(self._sendStateTimer):
self._sendStateTimer.stop()
self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL)
def deactivate(self):
if(self._sendStateTimer):
self._sendStateTimer.stop()
class ConfigurationGetter(object):
def getConfiguration(self):
self._prepareArgParser()
self._args = self._argparser.parse_args()
if(self._args.port == None):
self._args.port = constants.DEFAULT_PORT
return self._args
def _prepareArgParser(self):
self._argparser = argparse.ArgumentParser(description=getMessage("en", "server-argument-description"),
epilog=getMessage("en", "server-argument-epilog"))
self._argparser.add_argument('--port', metavar='port', type=str, nargs='?', help=getMessage("en", "server-port-argument"))
self._argparser.add_argument('--password', metavar='password', type=str, nargs='?', help=getMessage("en", "server-password-argument"))
self._argparser.add_argument('--isolate-rooms', action='store_true', help=getMessage("en", "server-isolate-room-argument"))
self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("en", "server-motd-argument"))
self._argparser.add_argument('--http-reply-file', metavar='file', type=str, nargs='?', help=getMessage("en", "server-http-reply-argument"))
self._argparser.add_argument('--irc-verbose', action='store_true', help=getMessage("en", "server-irc-verbose"))
self._argparser.add_argument('--irc-config-file', metavar='file', type=str, nargs='?', help=getMessage("en", "server-irc-config"))