diff --git a/constants/fokabotCommands.py b/constants/fokabotCommands.py index b2369951..c6a9e0d0 100644 --- a/constants/fokabotCommands.py +++ b/constants/fokabotCommands.py @@ -304,22 +304,7 @@ def systemShutdown(fro, chan, message): return restartShutdown(False) def systemReload(fro, chan, message): - # Reload settings from bancho_settings - glob.banchoConf.loadSettings() - - # Reload channels too - glob.channels.loadChannels() - - # And chat filters - glob.chatFilters.loadFilters() - - # Send new channels and new bottom icon to everyone - glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) - glob.streams.broadcast("main", serverPackets.channelInfoEnd()) - for key, value in glob.channels.channels.items(): - if value.publicRead == True and value.hidden == False: - glob.streams.broadcast("main", serverPackets.channelInfo(key)) - + glob.banchoConf.reload() return "Bancho settings reloaded!" def systemMaintenance(fro, chan, message): diff --git a/events/changeActionEvent.py b/events/changeActionEvent.py index c4421d16..07135c95 100644 --- a/events/changeActionEvent.py +++ b/events/changeActionEvent.py @@ -5,20 +5,19 @@ from constants import serverPackets from objects import glob - def handle(userToken, packetData): # Get usertoken data userID = userToken.userID username = userToken.username # Make sure we are not banned - if userUtils.isBanned(userID): - userToken.enqueue(serverPackets.loginBanned()) - return + #if userUtils.isBanned(userID): + # userToken.enqueue(serverPackets.loginBanned()) + # return # Send restricted message if needed - if userToken.restricted: - userToken.checkRestricted(True) + #if userToken.restricted: + # userToken.checkRestricted(True) # Change action packet packetData = clientPackets.userActionChange(packetData) @@ -34,8 +33,10 @@ def handle(userToken, packetData): ''' # Update cached stats if our pp changed if we've just submitted a score or we've changed gameMode - if (userToken.actionID == actions.PLAYING or userToken.actionID == actions.MULTIPLAYING) or (userToken.pp != userUtils.getPP(userID, userToken.gameMode)) or (userToken.gameMode != packetData["gameMode"]): - # Always update game mode, or we'll cache stats from the wrong game mode if we've changed it + #if (userToken.actionID == actions.PLAYING or userToken.actionID == actions.MULTIPLAYING) or (userToken.pp != userUtils.getPP(userID, userToken.gameMode)) or (userToken.gameMode != packetData["gameMode"]): + + # Update cached stats if we've changed gamemode + if userToken.gameMode != packetData["gameMode"]: userToken.gameMode = packetData["gameMode"] userToken.updateCachedStats() diff --git a/events/logoutEvent.py b/events/logoutEvent.py index 16ccfd84..b4ac4e64 100644 --- a/events/logoutEvent.py +++ b/events/logoutEvent.py @@ -1,4 +1,5 @@ import time +import json from common.log import logUtils as log from constants import serverPackets @@ -6,7 +7,7 @@ from objects import glob -def handle(userToken, _=None): +def handle(userToken, _=None, deleteToken=True): # get usertoken data userID = userToken.userID username = userToken.username @@ -38,7 +39,19 @@ def handle(userToken, _=None): glob.ircServer.forceDisconnection(userToken.username) # Delete token - glob.tokens.deleteToken(requestToken) + if deleteToken: + glob.tokens.deleteToken(requestToken) + else: + userToken.kicked = True + + # Change username if needed + newUsername = glob.redis.get("ripple:change_username_pending:{}".format(userID)) + if newUsername is not None: + log.debug("Sending username change request for user {}".format(userID)) + glob.redis.publish("peppy:change_username", json.dumps({ + "userID": userID, + "newUsername": newUsername.decode("utf-8") + })) # Console output log.info("{} has been disconnected. (logout)".format(username)) diff --git a/handlers/mainHandler.py b/handlers/mainHandler.py index 557fa7aa..453ccd79 100644 --- a/handlers/mainHandler.py +++ b/handlers/mainHandler.py @@ -122,7 +122,7 @@ def wrapper(): packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent), packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent), packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent), - + packetIDs.client_channelJoin: handleEvent(channelJoinEvent), packetIDs.client_channelPart: handleEvent(channelPartEvent), packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent), @@ -206,6 +206,9 @@ def wrapper(): userToken.updatePingTime() # Release token lock userToken.lock.release() + # Delete token if kicked + if userToken.kicked: + glob.tokens.deleteToken(userToken) if glob.outputRequestTime: # End time diff --git a/objects/banchoConfig.py b/objects/banchoConfig.py index ca13de90..667d37bc 100644 --- a/objects/banchoConfig.py +++ b/objects/banchoConfig.py @@ -1,5 +1,6 @@ # TODO: Rewrite this shit from common import generalUtils +from constants import serverPackets from objects import glob @@ -41,3 +42,20 @@ def setMaintenance(self, maintenance): """ self.config["banchoMaintenance"] = maintenance glob.db.execute("UPDATE bancho_settings SET value_int = %s WHERE name = 'bancho_maintenance'", [int(maintenance)]) + + def reload(self): + # Reload settings from bancho_settings + glob.banchoConf.loadSettings() + + # Reload channels too + glob.channels.loadChannels() + + # And chat filters + glob.chatFilters.loadFilters() + + # Send new channels and new bottom icon to everyone + glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) + glob.streams.broadcast("main", serverPackets.channelInfoEnd()) + for key, value in glob.channels.channels.items(): + if value.publicRead == True and value.hidden == False: + glob.streams.broadcast("main", serverPackets.channelInfo(key)) \ No newline at end of file diff --git a/objects/osuToken.py b/objects/osuToken.py index 162b8eb1..e2a9a0eb 100644 --- a/objects/osuToken.py +++ b/objects/osuToken.py @@ -31,6 +31,7 @@ def __init__(self, userID, token_ = None, ip ="", irc = False, timeOffset = 0, t self.privileges = userUtils.getPrivileges(self.userID) self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer") or userUtils.isInPrivilegeGroup(self.userID, "community manager") self.irc = irc + self.kicked = False self.restricted = userUtils.isRestricted(self.userID) self.loginTime = int(time.time()) self.pingTime = self.loginTime @@ -323,22 +324,28 @@ def kick(self, message="You have been kicked from the server. Please login again self.enqueue(serverPackets.loginFailed()) # Logout event - logoutEvent.handle(self, None) + logoutEvent.handle(self, deleteToken=False) - def silence(self, seconds, reason, author = 999): + def silence(self, seconds = None, reason = "", author = 999): """ Silences this user (db, packet and token) - :param seconds: silence length in seconds - :param reason: silence reason + :param seconds: silence length in seconds. If None, get it from db. Default: None + :param reason: silence reason. Default: empty string :param author: userID of who has silenced the user. Default: 999 (FokaBot) :return: """ - # Silence in db and token - self.silenceEndTime = int(time.time())+seconds - userUtils.silence(self.userID, seconds, reason, author) + if seconds is None: + # Get silence expire from db if needed + seconds = max(0, userUtils.getSilenceEnd(self.userID) - int(time.time())) + else: + # Silence in db and token + userUtils.silence(self.userID, seconds, reason, author) + + # Silence token + self.silenceEndTime = int(time.time()) + seconds - # Send silence packet to target + # Send silence packet to user self.enqueue(serverPackets.silenceEndTime(seconds)) # Send silenced packet to everyone else @@ -394,18 +401,29 @@ def updateCachedStats(self): self.gameRank = stats["gameRank"] self.pp = stats["pp"] - def checkRestricted(self, force=False): + def checkRestricted(self): """ Check if this token is restricted. If so, send fokabot message - :param force: If True, get restricted value from db. - If False, get the cached one. Default: False :return: """ - if force: - self.restricted = userUtils.isRestricted(self.userID) + oldRestricted = self.restricted + self.restricted = userUtils.isRestricted(self.userID) if self.restricted: self.setRestricted() + elif not self.restricted and oldRestricted != self.restricted: + self.resetRestricted() + + def checkBanned(self): + """ + Check if this user is banned. If so, disconnect it. + + :return: + """ + if userUtils.isBanned(self.userID): + self.enqueue(serverPackets.loginBanned()) + logoutEvent.handle(self, deleteToken=False) + def setRestricted(self): """ @@ -415,7 +433,16 @@ def setRestricted(self): :return: """ self.restricted = True - chat.sendMessage("FokaBot",self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.") + chat.sendMessage("FokaBot", self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.") + + def resetRestricted(self): + """ + Send FokaBot message to alert the user that he has been unrestricted + and he has to log in again. + + :return: + """ + chat.sendMessage("FokaBot", self.username, "Your account has been unrestricted! Please log in again.") def joinStream(self, name): """ diff --git a/pep.py b/pep.py index 00b48a23..763baa32 100644 --- a/pep.py +++ b/pep.py @@ -16,7 +16,7 @@ from common.db import dbConnector from common.ddog import datadogClient from common.log import logUtils as log -from common.ripple import userUtils +from common.redis import pubSub from common.web import schiavo from handlers import apiFokabotMessageHandler from handlers import apiIsOnlineHandler @@ -34,6 +34,12 @@ from objects import chatFilters from objects import fokabot from objects import glob +from pubSubHandlers import changeUsernameHandler + +from pubSubHandlers import disconnectHandler +from pubSubHandlers import banHandler +from pubSubHandlers import updateSilenceHandler +from pubSubHandlers import updateStatsHandler def make_app(): @@ -264,6 +270,16 @@ def make_app(): log.logMessage("**pep.py** Server started!", discord="bunker", of="info.txt", stdout=False) consoleHelper.printColored("> Tornado listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN) + # Connect to pubsub channels + pubSub.listener(glob.redis, { + "peppy:disconnect": disconnectHandler.handler(), + "peppy:change_username": changeUsernameHandler.handler(), + "peppy:reload_settings": lambda x: x == b"reload" and glob.banchoConf.reload(), + "peppy:update_cached_stats": updateStatsHandler.handler(), + "peppy:silence": updateSilenceHandler.handler(), + "peppy:ban": banHandler.handler(), + }).start() + # Start tornado glob.application.listen(serverPort) tornado.ioloop.IOLoop.instance().start() diff --git a/pubSubHandlers/__init__.py b/pubSubHandlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pubSubHandlers/banHandler.py b/pubSubHandlers/banHandler.py new file mode 100644 index 00000000..50345f72 --- /dev/null +++ b/pubSubHandlers/banHandler.py @@ -0,0 +1,18 @@ +from common.redis import generalPubSubHandler +from common.ripple import userUtils +from objects import glob + +class handler(generalPubSubHandler.generalPubSubHandler): + def __init__(self): + super().__init__() + self.type = "int" + + def handle(self, userID): + userID = super().parseData(userID) + if userID is None: + return + targetToken = glob.tokens.getTokenFromUserID(userID) + if targetToken is not None: + targetToken.privileges = userUtils.getPrivileges(userID) + targetToken.checkBanned() + targetToken.checkRestricted() \ No newline at end of file diff --git a/pubSubHandlers/changeUsernameHandler.py b/pubSubHandlers/changeUsernameHandler.py new file mode 100644 index 00000000..769cd230 --- /dev/null +++ b/pubSubHandlers/changeUsernameHandler.py @@ -0,0 +1,49 @@ +from common.redis import generalPubSubHandler +from common.ripple import userUtils +from common.log import logUtils as log +from common.constants import actions +from objects import glob + +def handleUsernameChange(userID, newUsername, targetToken=None): + try: + userUtils.changeUsername(userID, newUsername=newUsername) + if targetToken is not None: + targetToken.kick("Your username has been changed to {}. Please log in again.".format(newUsername), "username_change") + except userUtils.usernameAlreadyInUseError: + log.rap(999, "Username change: {} is already in use!", through="Bancho") + if targetToken is not None: + targetToken.kick("There was a critical error while trying to change your username. Please contact a developer.", "username_change_fail") + except userUtils.invalidUsernameError: + log.rap(999, "Username change: {} is not a valid username!", through="Bancho") + if targetToken is not None: + targetToken.kick("There was a critical error while trying to change your username. Please contact a developer.", "username_change_fail") + +class handler(generalPubSubHandler.generalPubSubHandler): + def __init__(self): + super().__init__() + self.structure = { + "userID": 0, + "newUsername": "" + } + + def handle(self, data): + data = super().parseData(data) + if data is None: + return + # Get the user's token + targetToken = glob.tokens.getTokenFromUserID(data["userID"]) + if targetToken is None: + # If the user is offline change username immediately + handleUsernameChange(data["userID"], data["newUsername"]) + else: + if targetToken.irc or (targetToken.actionID != actions.PLAYING and targetToken.actionID != actions.MULTIPLAYING): + # If the user is online and he's connected through IRC or he's not playing, + # change username and kick the user immediately + handleUsernameChange(data["userID"], data["newUsername"], targetToken) + else: + # If the user is playing, delay the username change until he submits the score + # On submit modular, lets will send the username change request again + # through redis once the score has been submitted + # The check is performed on bancho logout too, so if the user disconnects + # without submitting a score, the username gets changed on bancho logout + glob.redis.set("ripple:change_username_pending:{}".format(data["userID"]), data["newUsername"]) \ No newline at end of file diff --git a/pubSubHandlers/disconnectHandler.py b/pubSubHandlers/disconnectHandler.py new file mode 100644 index 00000000..73688241 --- /dev/null +++ b/pubSubHandlers/disconnectHandler.py @@ -0,0 +1,18 @@ +from common.redis import generalPubSubHandler +from objects import glob + +class handler(generalPubSubHandler.generalPubSubHandler): + def __init__(self): + super().__init__() + self.structure = { + "userID": 0, + "reason": "" + } + + def handle(self, data): + data = super().parseData(data) + if data is None: + return + targetToken = glob.tokens.getTokenFromUserID(data["userID"]) + if targetToken is not None: + targetToken.kick(data["reason"], "pubsub_kick") \ No newline at end of file diff --git a/pubSubHandlers/updateSilenceHandler.py b/pubSubHandlers/updateSilenceHandler.py new file mode 100644 index 00000000..d2eda4bf --- /dev/null +++ b/pubSubHandlers/updateSilenceHandler.py @@ -0,0 +1,15 @@ +from common.redis import generalPubSubHandler +from objects import glob + +class handler(generalPubSubHandler.generalPubSubHandler): + def __init__(self): + super().__init__() + self.type = "int" + + def handle(self, userID): + userID = super().parseData(userID) + if userID is None: + return + targetToken = glob.tokens.getTokenFromUserID(userID) + if targetToken is not None: + targetToken.silence() \ No newline at end of file diff --git a/pubSubHandlers/updateStatsHandler.py b/pubSubHandlers/updateStatsHandler.py new file mode 100644 index 00000000..8d9c318f --- /dev/null +++ b/pubSubHandlers/updateStatsHandler.py @@ -0,0 +1,15 @@ +from common.redis import generalPubSubHandler +from objects import glob + +class handler(generalPubSubHandler.generalPubSubHandler): + def __init__(self): + super().__init__() + self.type = "int" + + def handle(self, userID): + userID = super().parseData(userID) + if userID is None: + return + targetToken = glob.tokens.getTokenFromUserID(userID) + if targetToken is not None: + targetToken.updateCachedStats() \ No newline at end of file