Compare commits

..

No commits in common. "master" and "v1.11.0" have entirely different histories.

72 changed files with 926 additions and 2099 deletions

1
.gitignore vendored
View File

@ -7,4 +7,3 @@ filters.txt
redistest.py redistest.py
*.c *.c
*.so *.so
.pyenv

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "common"] [submodule "common"]
path = common path = common
url = https://github.com/osufx/ripple-python-common.git url = git@git.zxq.co:ripple/ripple-python-common.git

View File

@ -1,7 +0,0 @@
python-targets:
- 3
pep8:
none: true
pylint:
disable:
- cyclic-import

View File

@ -1,4 +1,4 @@
## pep.py [![Code Health](https://landscape.io/github/osuripple/pep.py/master/landscape.svg?style=flat)](https://landscape.io/github/osuripple/pep.py/master) ## pep.py
- Origin: https://git.zxq.co/ripple/pep.py - Origin: https://git.zxq.co/ripple/pep.py
- Mirror: https://github.com/osuripple/pep.py - Mirror: https://github.com/osuripple/pep.py

2
common

@ -1 +1 @@
Subproject commit 6103fe96a79cd8f5cbabe24b5fac9cf2a5cacb4a Subproject commit 3288420cd81cdb84912b9debda1e7828ebb7c62a

View File

@ -143,26 +143,6 @@ def transferHost(stream):
def matchInvite(stream): def matchInvite(stream):
return packetHelper.readPacketData(stream, [["userID", dataTypes.UINT32]]) return packetHelper.readPacketData(stream, [["userID", dataTypes.UINT32]])
def matchFrames(stream):
return packetHelper.readPacketData(stream,
[
["time", dataTypes.SINT32],
["id", dataTypes.BYTE],
["count300", dataTypes.UINT16],
["count100", dataTypes.UINT16],
["count50", dataTypes.UINT16],
["countGeki", dataTypes.UINT16],
["countKatu", dataTypes.UINT16],
["countMiss", dataTypes.UINT16],
["totalScore", dataTypes.SINT32],
["maxCombo", dataTypes.UINT16],
["currentCombo", dataTypes.UINT16],
["perfect", dataTypes.BYTE],
["currentHp", dataTypes.BYTE],
["tagByte", dataTypes.BYTE],
["usingScoreV2", dataTypes.BYTE]
])
def tournamentMatchInfoRequest(stream): def tournamentMatchInfoRequest(stream):
return packetHelper.readPacketData(stream, [["matchID", dataTypes.UINT32]]) return packetHelper.readPacketData(stream, [["matchID", dataTypes.UINT32]])

View File

@ -90,21 +90,3 @@ class unknownStreamException(Exception):
class userTournamentException(Exception): class userTournamentException(Exception):
pass pass
class userAlreadyInChannelException(Exception):
pass
class userNotInChannelException(Exception):
pass
class missingReportInfoException(Exception):
pass
class invalidUserException(Exception):
pass
class wrongChannelException(Exception):
pass
class periodicLoopException(Exception):
pass

View File

@ -1,34 +1,19 @@
import json import json
import random import random
import re
import threading
import requests import requests
import time
from common import generalUtils from common import generalUtils
from common.constants import mods from common.constants import mods
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils from common.ripple import userUtils
from constants import exceptions, slotStatuses, matchModModes, matchTeams, matchTeamTypes, matchScoringTypes from constants import exceptions
from common.constants import gameModes from common.constants import gameModes
from common.constants import privileges from common.constants import privileges
from constants import serverPackets from constants import serverPackets
from helpers import systemHelper from helpers import systemHelper
from objects import fokabot from objects import fokabot
from objects import glob from objects import glob
from helpers import chatHelper as chat
from common.web import cheesegull
def bloodcatMessage(beatmapID):
beatmap = glob.db.fetch("SELECT song_name, beatmapset_id FROM beatmaps WHERE beatmap_id = %s LIMIT 1", [beatmapID])
if beatmap is None:
return "Sorry, I'm not able to provide a download link for this map :("
return "Download [https://bloodcat.com/osu/s/{} {}] from Bloodcat".format(
beatmap["beatmapset_id"],
beatmap["song_name"],
)
""" """
Commands callbacks Commands callbacks
@ -46,19 +31,38 @@ TODO: Change False to None, because False doesn't make any sense
""" """
def instantRestart(fro, chan, message): def instantRestart(fro, chan, message):
glob.streams.broadcast("main", serverPackets.notification("We are restarting Bancho. Be right back!")) glob.streams.broadcast("main", serverPackets.notification("We are restarting Bancho. Be right back!"))
systemHelper.scheduleShutdown(0, True, delay=5) systemHelper.scheduleShutdown(0, True, delay=1)
return False return False
def faq(fro, chan, message): def faq(fro, chan, message):
key = message[0].lower() # TODO: Unhardcode this
if key not in glob.conf.extra["pep.py"]["faq"]: if message[0] == "rules":
return "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23]."
elif message[0] == "swearing":
return "Please don't abuse swearing"
elif message[0] == "spam":
return "Please don't spam"
elif message[0] == "offend":
return "Please don't offend other players"
elif message[0] == "github":
return "(Ripple's Github page!)[https://github.com/osuripple/ripple]"
elif message[0] == "discord":
return "(Join Ripple's Discord!)[https://discord.gg/0rJcZruIsA6rXuIx]"
elif message[0] == "blog":
return "You can find the latest Ripple news on the (blog)[https://ripple.moe/blog/]!"
elif message[0] == "changelog":
return "Check the (changelog)[https://ripple.moe/index.php?p=17] !"
elif message[0] == "status":
return "Check the server status (here!)[https://ripple.moe/index.php?p=27]"
elif message[0] == "english":
return "Please keep this channel in english."
else:
return False return False
return glob.conf.extra["pep.py"]["faq"][key]
def roll(fro, chan, message): def roll(fro, chan, message):
maxPoints = 100 maxPoints = 100
if len(message) >= 1: if len(message) >= 1:
if message[0].isdigit() and int(message[0]) > 0: if message[0].isdigit() == True and int(message[0]) > 0:
maxPoints = int(message[0]) maxPoints = int(message[0])
points = random.randrange(0,maxPoints) points = random.randrange(0,maxPoints)
@ -68,20 +72,15 @@ def roll(fro, chan, message):
# return random.choice(["yes", "no", "maybe"]) # return random.choice(["yes", "no", "maybe"])
def alert(fro, chan, message): def alert(fro, chan, message):
msg = ' '.join(message[:]).strip() glob.streams.broadcast("main", serverPackets.notification(' '.join(message[:])))
if not msg:
return False
glob.streams.broadcast("main", serverPackets.notification(msg))
return False return False
def alertUser(fro, chan, message): def alertUser(fro, chan, message):
target = message[0].lower() target = message[0].replace("_", " ")
targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
targetToken = glob.tokens.getTokenFromUsername(target)
if targetToken is not None: if targetToken is not None:
msg = ' '.join(message[1:]).strip() targetToken.enqueue(serverPackets.notification(' '.join(message[1:])))
if not msg:
return False
targetToken.enqueue(serverPackets.notification(msg))
return False return False
else: else:
return "User offline." return "User offline."
@ -107,7 +106,6 @@ def moderated(fro, chan, message):
def kickAll(fro, chan, message): def kickAll(fro, chan, message):
# Kick everyone but mods/admins # Kick everyone but mods/admins
toKick = [] toKick = []
with glob.tokens:
for key, value in glob.tokens.tokens.items(): for key, value in glob.tokens.tokens.items():
if not value.admin: if not value.admin:
toKick.append(key) toKick.append(key)
@ -121,18 +119,15 @@ def kickAll(fro, chan, message):
def kick(fro, chan, message): def kick(fro, chan, message):
# Get parameters # Get parameters
target = message[0].lower() target = message[0].replace("_", " ")
if target == glob.BOT_NAME.lower():
return "Nope."
# Get target token and make sure is connected # Get target token and make sure is connected
tokens = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True, _all=True) targetToken = glob.tokens.getTokenFromUsername(target)
if len(tokens) == 0: if targetToken is None:
return "{} is not online".format(target) return "{} is not online".format(target)
# Kick users # Kick user
for i in tokens: targetToken.kick()
i.kick()
# Bot response # Bot response
return "{} has been kicked from the server.".format(target) return "{} has been kicked from the server.".format(target)
@ -140,25 +135,22 @@ def kick(fro, chan, message):
def fokabotReconnect(fro, chan, message): def fokabotReconnect(fro, chan, message):
# Check if fokabot is already connected # Check if fokabot is already connected
if glob.tokens.getTokenFromUserID(999) is not None: if glob.tokens.getTokenFromUserID(999) is not None:
return "{} is already connected to Bancho".format(glob.BOT_NAME) return "Fokabot is already connected to Bancho"
# Fokabot is not connected, connect it # Fokabot is not connected, connect it
fokabot.connect() fokabot.connect()
return False return False
def silence(fro, chan, message): def silence(fro, chan, message):
message = [x.lower() for x in message] for i in message:
target = message[0] i = i.lower()
target = message[0].replace("_", " ")
amount = message[1] amount = message[1]
unit = message[2] unit = message[2]
reason = ' '.join(message[3:]).strip() reason = ' '.join(message[3:])
if not reason:
return "Please provide a valid reason."
if not amount.isdigit():
return "The amount must be a number."
# Get target user ID # Get target user ID
targetUserID = userUtils.getIDSafe(target) targetUserID = userUtils.getID(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
# Make sure the user exists # Make sure the user exists
@ -182,7 +174,7 @@ def silence(fro, chan, message):
return "Invalid silence time. Max silence time is 7 days." return "Invalid silence time. Max silence time is 7 days."
# Send silence packet to target if he's connected # Send silence packet to target if he's connected
targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True) targetToken = glob.tokens.getTokenFromUsername(target)
if targetToken is not None: if targetToken is not None:
# user online, silence both in db and with packet # user online, silence both in db and with packet
targetToken.silence(silenceTime, reason, userID) targetToken.silence(silenceTime, reason, userID)
@ -198,16 +190,16 @@ def removeSilence(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0] target = message[0].replace("_", " ")
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getIDSafe(target) targetUserID = userUtils.getID(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
# Send new silence end packet to user if he's online # Send new silence end packet to user if he's online
targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True) targetToken = glob.tokens.getTokenFromUsername(target)
if targetToken is not None: if targetToken is not None:
# User online, remove silence both in db and with packet # User online, remove silence both in db and with packet
targetToken.silence(0, "", userID) targetToken.silence(0, "", userID)
@ -221,10 +213,10 @@ def ban(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0] target = message[0].replace("_", " ")
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getIDSafe(target) targetUserID = userUtils.getID(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
@ -233,7 +225,7 @@ def ban(fro, chan, message):
userUtils.ban(targetUserID) userUtils.ban(targetUserID)
# Send ban packet to the user if he's online # Send ban packet to the user if he's online
targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True) targetToken = glob.tokens.getTokenFromUsername(target)
if targetToken is not None: if targetToken is not None:
targetToken.enqueue(serverPackets.loginBanned()) targetToken.enqueue(serverPackets.loginBanned())
@ -244,10 +236,10 @@ def unban(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0] target = message[0].replace("_", " ")
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getIDSafe(target) targetUserID = userUtils.getID(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
@ -262,10 +254,10 @@ def restrict(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0] target = message[0].replace("_", " ")
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getIDSafe(target) targetUserID = userUtils.getID(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
@ -274,7 +266,7 @@ def restrict(fro, chan, message):
userUtils.restrict(targetUserID) userUtils.restrict(targetUserID)
# Send restricted mode packet to this user if he's online # Send restricted mode packet to this user if he's online
targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True) targetToken = glob.tokens.getTokenFromUsername(target)
if targetToken is not None: if targetToken is not None:
targetToken.setRestricted() targetToken.setRestricted()
@ -285,10 +277,10 @@ def unrestrict(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0] target = message[0].replace("_", " ")
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getIDSafe(target) targetUserID = userUtils.getID(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
@ -333,7 +325,6 @@ def systemMaintenance(fro, chan, message):
who = [] who = []
# Disconnect everyone but mod/admins # Disconnect everyone but mod/admins
with glob.tokens:
for _, value in glob.tokens.tokens.items(): for _, value in glob.tokens.tokens.items():
if not value.admin: if not value.admin:
who.append(value.userID) who.append(value.userID)
@ -389,7 +380,7 @@ def getPPMessage(userID, just_data = False):
currentAcc = token.tillerino[2] currentAcc = token.tillerino[2]
# Send request to LETS api # Send request to LETS api
resp = requests.get("http://127.0.0.1:5002/api/v1/pp?b={}&m={}".format(currentMap, currentMods), timeout=10).text resp = requests.get("http://127.0.0.1:5002/api/v1/pp?b={}&m={}".format(currentMap, currentMods, currentAcc), timeout=10).text
data = json.loads(resp) data = json.loads(resp)
# Make sure status is in response data # Make sure status is in response data
@ -399,7 +390,7 @@ def getPPMessage(userID, just_data = False):
# Make sure status is 200 # Make sure status is 200
if data["status"] != 200: if data["status"] != 200:
if "message" in data: if "message" in data:
return "Error in LETS API call ({}).".format(data["message"]) return "Error in LETS API call ({}). Please tell this to a dev.".format(data["message"])
else: else:
raise exceptions.apiException raise exceptions.apiException
@ -435,7 +426,7 @@ def getPPMessage(userID, just_data = False):
return "API Timeout. Please try again in a few seconds." return "API Timeout. Please try again in a few seconds."
except exceptions.apiException: except exceptions.apiException:
# API error # API error
return "Unknown error in LETS API call." return "Unknown error in LETS API call. Please tell this to a dev."
#except: #except:
# Unknown exception # Unknown exception
# TODO: print exception # TODO: print exception
@ -443,14 +434,6 @@ def getPPMessage(userID, just_data = False):
def tillerinoNp(fro, chan, message): def tillerinoNp(fro, chan, message):
try: try:
# Bloodcat trigger for #spect_
if chan.startswith("#spect_"):
spectatorHostUserID = getSpectatorHostUserIDFromChannel(chan)
spectatorHostToken = glob.tokens.getTokenFromUserID(spectatorHostUserID, ignoreIRC=True)
if spectatorHostToken is None:
return False
return bloodcatMessage(spectatorHostToken.beatmapID)
# Run the command in PM only # Run the command in PM only
if chan.startswith("#"): if chan.startswith("#"):
return False return False
@ -581,10 +564,6 @@ def tillerinoAcc(fro, chan, message):
def tillerinoLast(fro, chan, message): def tillerinoLast(fro, chan, message):
try: try:
# Run the command in PM only
if chan.startswith("#"):
return False
data = glob.db.fetch("""SELECT beatmaps.song_name as sn, scores.*, data = glob.db.fetch("""SELECT beatmaps.song_name as sn, scores.*,
beatmaps.beatmap_id as bid, beatmaps.difficulty_std, beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania, beatmaps.max_combo as fc beatmaps.beatmap_id as bid, beatmaps.difficulty_std, beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania, beatmaps.max_combo as fc
FROM scores FROM scores
@ -600,11 +579,11 @@ def tillerinoLast(fro, chan, message):
rank = generalUtils.getRank(data["play_mode"], data["mods"], data["accuracy"], rank = generalUtils.getRank(data["play_mode"], data["mods"], data["accuracy"],
data["300_count"], data["100_count"], data["50_count"], data["misses_count"]) data["300_count"], data["100_count"], data["50_count"], data["misses_count"])
ifPlayer = "{0} | ".format(fro) if chan != glob.BOT_NAME else "" ifPlayer = "{0} | ".format(fro) if chan != "FokaBot" else ""
ifFc = " (FC)" if data["max_combo"] == data["fc"] else " {0}x/{1}x".format(data["max_combo"], data["fc"]) ifFc = " (FC)" if data["max_combo"] == data["fc"] else " {0}x/{1}x".format(data["max_combo"], data["fc"])
beatmapLink = "[http://osu.ppy.sh/b/{1} {0}]".format(data["sn"], data["bid"]) beatmapLink = "[http://osu.ppy.sh/b/{1} {0}]".format(data["sn"], data["bid"])
hasPP = data["play_mode"] != gameModes.CTB hasPP = data["play_mode"] == gameModes.STD or data["play_mode"] == gameModes.MANIA
msg = ifPlayer msg = ifPlayer
msg += beatmapLink msg += beatmapLink
@ -677,7 +656,7 @@ def pp(fro, chan, message):
pp = userUtils.getPP(token.userID, gameMode) pp = userUtils.getPP(token.userID, gameMode)
return "You have {:,} pp".format(pp) return "You have {:,} pp".format(pp)
def updateBeatmap(fro, chan, message): def updateBeatmap(fro, chan, to):
try: try:
# Run the command in PM only # Run the command in PM only
if chan.startswith("#"): if chan.startswith("#"):
@ -692,514 +671,25 @@ def updateBeatmap(fro, chan, message):
if token.tillerino[0] == 0: if token.tillerino[0] == 0:
return "Please give me a beatmap first with /np command." return "Please give me a beatmap first with /np command."
# Send the request to cheesegull # Send request
ok, message = cheesegull.updateBeatmap(token.tillerino[0]) beatmapData = glob.db.fetch("SELECT beatmapset_id, song_name FROM beatmaps WHERE beatmap_id = %s LIMIT 1", [token.tillerino[0]])
if ok:
return "An update request for that beatmap has been queued. Check back in a few minutes and the beatmap should be updated!"
else:
return "Error in beatmap mirror API request: {}".format(message)
except:
return False
def report(fro, chan, message):
msg = ""
try:
# TODO: Rate limit
# Regex on message
reportRegex = re.compile("^(.+) \((.+)\)\:(?: )?(.+)?$")
result = reportRegex.search(" ".join(message))
# Make sure the message matches the regex
if result is None:
raise exceptions.invalidArgumentsException()
# Get username, report reason and report info
target, reason, additionalInfo = result.groups()
target = chat.fixUsernameForBancho(target)
# Make sure the target is not foka
if target.lower() == glob.BOT_NAME.lower():
raise exceptions.invalidUserException()
# Make sure the user exists
targetID = userUtils.getID(target)
if targetID == 0:
raise exceptions.userNotFoundException()
# Make sure that the user has specified additional info if report reason is 'Other'
if reason.lower() == "other" and additionalInfo is None:
raise exceptions.missingReportInfoException()
# Get the token if possible
chatlog = ""
token = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
if token is not None:
chatlog = token.getMessagesBufferString()
# Everything is fine, submit report
glob.db.execute("INSERT INTO reports (id, from_uid, to_uid, reason, chatlog, time) VALUES (NULL, %s, %s, %s, %s, %s)", [userUtils.getID(fro), targetID, "{reason} - ingame {info}".format(reason=reason, info="({})".format(additionalInfo) if additionalInfo is not None else ""), chatlog, int(time.time())])
msg = "You've reported {target} for {reason}{info}. A Community Manager will check your report as soon as possible. Every !report message you may see in chat wasn't sent to anyone, so nobody in chat, but admins, know about your report. Thank you for reporting!".format(target=target, reason=reason, info="" if additionalInfo is None else " (" + additionalInfo + ")")
adminMsg = "{user} has reported {target} for {reason} ({info})".format(user=fro, target=target, reason=reason, info=additionalInfo)
# Log report in #admin and on discord
chat.sendMessage(glob.BOT_NAME, "#admin", adminMsg)
log.warning(adminMsg, discord="cm")
except exceptions.invalidUserException:
msg = "Hello, {} here! You can't report me. I won't forget what you've tried to do. Watch out.".format(glob.BOT_NAME)
except exceptions.invalidArgumentsException:
msg = "Invalid report command syntax. To report an user, click on it and select 'Report user'."
except exceptions.userNotFoundException:
msg = "The user you've tried to report doesn't exist."
except exceptions.missingReportInfoException:
msg = "Please specify the reason of your report."
except:
raise
finally:
if msg != "":
token = glob.tokens.getTokenFromUsername(fro)
if token is not None:
if token.irc:
chat.sendMessage(glob.BOT_NAME, fro, msg)
else:
token.enqueue(serverPackets.notification(msg))
return False
def getMatchIDFromChannel(chan):
if not chan.lower().startswith("#multi_"):
raise exceptions.wrongChannelException()
parts = chan.lower().split("_")
if len(parts) < 2 or not parts[1].isdigit():
raise exceptions.wrongChannelException()
matchID = int(parts[1])
if matchID not in glob.matches.matches:
raise exceptions.matchNotFoundException()
return matchID
def getSpectatorHostUserIDFromChannel(chan):
if not chan.lower().startswith("#spect_"):
raise exceptions.wrongChannelException()
parts = chan.lower().split("_")
if len(parts) < 2 or not parts[1].isdigit():
raise exceptions.wrongChannelException()
userID = int(parts[1])
return userID
def multiplayer(fro, chan, message):
def mpMake():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp make <name>")
matchName = " ".join(message[1:]).strip()
if not matchName:
raise exceptions.invalidArgumentsException("Match name must not be empty!")
matchID = glob.matches.createMatch(matchName, generalUtils.stringMd5(generalUtils.randomString(32)), 0, "Tournament", "", 0, -1, isTourney=True)
glob.matches.matches[matchID].sendUpdates()
return "Tourney match #{} created!".format(matchID)
def mpJoin():
if len(message) < 2 or not message[1].isdigit():
raise exceptions.invalidArgumentsException("Wrong syntax: !mp join <id>")
matchID = int(message[1])
userToken = glob.tokens.getTokenFromUsername(fro, ignoreIRC=True)
if userToken is None:
raise exceptions.invalidArgumentsException(
"No game clients found for {}, can't join the match. "
"If you're a referee and you want to join the chat "
"channel from IRC, use /join #multi_{} instead.".format(fro, matchID)
)
userToken.joinMatch(matchID)
return "Attempting to join match #{}!".format(matchID)
def mpClose():
matchID = getMatchIDFromChannel(chan)
glob.matches.disposeMatch(matchID)
return "Multiplayer match #{} disposed successfully".format(matchID)
def mpLock():
matchID = getMatchIDFromChannel(chan)
glob.matches.matches[matchID].isLocked = True
return "This match has been locked"
def mpUnlock():
matchID = getMatchIDFromChannel(chan)
glob.matches.matches[matchID].isLocked = False
return "This match has been unlocked"
def mpSize():
if len(message) < 2 or not message[1].isdigit() or int(message[1]) < 2 or int(message[1]) > 16:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp size <slots(2-16)>")
matchSize = int(message[1])
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.forceSize(matchSize)
return "Match size changed to {}".format(matchSize)
def mpMove():
if len(message) < 3 or not message[2].isdigit() or int(message[2]) < 0 or int(message[2]) > 16:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp move <username> <slot>")
username = message[1]
newSlotID = int(message[2])
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
success = _match.userChangeSlot(userID, newSlotID)
if success:
result = "Player {} moved to slot {}".format(username, newSlotID)
else:
result = "You can't use that slot: it's either already occupied by someone else or locked"
return result
def mpHost():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp host <username>")
username = message[1].strip()
if not username:
raise exceptions.invalidArgumentsException("Please provide a username")
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
success = _match.setHost(userID)
return "{} is now the host".format(username) if success else "Couldn't give host to {}".format(username)
def mpClearHost():
matchID = getMatchIDFromChannel(chan)
glob.matches.matches[matchID].removeHost()
return "Host has been removed from this match"
def mpStart():
def _start():
matchID = getMatchIDFromChannel(chan)
success = glob.matches.matches[matchID].start()
if not success:
chat.sendMessage(glob.BOT_NAME, chan, "Couldn't start match. Make sure there are enough players and "
"teams are valid. The match has been unlocked.")
else:
chat.sendMessage(glob.BOT_NAME, chan, "Have fun!")
def _decreaseTimer(t):
if t <= 0:
_start()
else:
if t % 10 == 0 or t <= 5:
chat.sendMessage(glob.BOT_NAME, chan, "Match starts in {} seconds.".format(t))
threading.Timer(1.00, _decreaseTimer, [t - 1]).start()
if len(message) < 2 or not message[1].isdigit():
startTime = 0
else:
startTime = int(message[1])
force = False if len(message) < 3 else message[2].lower() == "force"
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
# Force everyone to ready
someoneNotReady = False
for i, slot in enumerate(_match.slots):
if slot.status != slotStatuses.READY and slot.user is not None:
someoneNotReady = True
if force:
_match.toggleSlotReady(i)
if someoneNotReady and not force:
return "Some users aren't ready yet. Use '!mp start force' if you want to start the match, " \
"even with non-ready players."
if startTime == 0:
_start()
return "Starting match"
else:
_match.isStarting = True
threading.Timer(1.00, _decreaseTimer, [startTime - 1]).start()
return "Match starts in {} seconds. The match has been locked. " \
"Please don't leave the match during the countdown " \
"or you might receive a penalty.".format(startTime)
def mpInvite():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp invite <username>")
username = message[1].strip()
if not username:
raise exceptions.invalidArgumentsException("Please provide a username")
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
token = glob.tokens.getTokenFromUserID(userID, ignoreIRC=True)
if token is None:
raise exceptions.invalidUserException("That user is not connected to bancho right now.")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.invite(999, userID)
token.enqueue(serverPackets.notification("Please accept the invite you've just received from {} to "
"enter your tourney match.".format(glob.BOT_NAME)))
return "An invite to this match has been sent to {}".format(username)
def mpMap():
if len(message) < 2 or not message[1].isdigit() or (len(message) == 3 and not message[2].isdigit()):
raise exceptions.invalidArgumentsException("Wrong syntax: !mp map <beatmapid> [<gamemode>]")
beatmapID = int(message[1])
gameMode = int(message[2]) if len(message) == 3 else 0
if gameMode < 0 or gameMode > 3:
raise exceptions.invalidArgumentsException("Gamemode must be 0, 1, 2 or 3")
beatmapData = glob.db.fetch("SELECT * FROM beatmaps WHERE beatmap_id = %s LIMIT 1", [beatmapID])
if beatmapData is None: if beatmapData is None:
raise exceptions.invalidArgumentsException("The beatmap you've selected couldn't be found in the database." return "Couldn't find beatmap data in database. Please load the beatmap's leaderboard and try again."
"If the beatmap id is valid, please load the scoreboard first in "
"order to cache it, then try again.")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.beatmapID = beatmapID
_match.beatmapName = beatmapData["song_name"]
_match.beatmapMD5 = beatmapData["beatmap_md5"]
_match.gameMode = gameMode
_match.resetReady()
_match.sendUpdates()
return "Match map has been updated"
def mpSet(): response = requests.post("{}/api/v1/update_beatmap".format(glob.conf.config["mirror"]["url"]), {
if len(message) < 2 or not message[1].isdigit() or \ "beatmap_set_id": beatmapData["beatmapset_id"],
(len(message) >= 3 and not message[2].isdigit()) or \ "beatmap_name": beatmapData["song_name"],
(len(message) >= 4 and not message[3].isdigit()): "username": token.username,
raise exceptions.invalidArgumentsException("Wrong syntax: !mp set <teammode> [<scoremode>] [<size>]") "key": glob.conf.config["mirror"]["apikey"]
_match = glob.matches.matches[getMatchIDFromChannel(chan)] })
matchTeamType = int(message[1]) if response.status_code == 200:
matchScoringType = int(message[2]) if len(message) >= 3 else _match.matchScoringType return "An update request for that beatmap has been queued. You'll receive a message once the beatmap has been updated on our mirror!"
if not 0 <= matchTeamType <= 3: elif response.status_code == 429:
raise exceptions.invalidArgumentsException("Match team type must be between 0 and 3") return "You are sending too many beatmaps update requests. Wait a bit and retry later."
if not 0 <= matchScoringType <= 3:
raise exceptions.invalidArgumentsException("Match scoring type must be between 0 and 3")
oldMatchTeamType = _match.matchTeamType
_match.matchTeamType = matchTeamType
_match.matchScoringType = matchScoringType
if len(message) >= 4:
_match.forceSize(int(message[3]))
if _match.matchTeamType != oldMatchTeamType:
_match.initializeTeams()
if _match.matchTeamType == matchTeamTypes.TAG_COOP or _match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
_match.matchModMode = matchModModes.NORMAL
_match.sendUpdates()
return "Match settings have been updated!"
def mpAbort():
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.abort()
return "Match aborted!"
def mpKick():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp kick <username>")
username = message[1].strip()
if not username:
raise exceptions.invalidArgumentsException("Please provide a username")
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
slotID = _match.getUserSlotID(userID)
if slotID is None:
raise exceptions.userNotFoundException("The specified user is not in this match")
for i in range(0, 2):
_match.toggleSlotLocked(slotID)
return "{} has been kicked from the match.".format(username)
def mpPassword():
password = "" if len(message) < 2 or not message[1].strip() else message[1]
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.changePassword(password)
return "Match password has been changed!"
def mpRandomPassword():
password = generalUtils.stringMd5(generalUtils.randomString(32))
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.changePassword(password)
return "Match password has been changed to a random one"
def mpMods():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp <mod1> [<mod2>] ...")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
newMods = 0
freeMod = False
for _mod in message[1:]:
if _mod.lower().strip() == "hd":
newMods |= mods.HIDDEN
elif _mod.lower().strip() == "hr":
newMods |= mods.HARDROCK
elif _mod.lower().strip() == "dt":
newMods |= mods.DOUBLETIME
elif _mod.lower().strip() == "fl":
newMods |= mods.FLASHLIGHT
elif _mod.lower().strip() == "fi":
newMods |= mods.FADEIN
elif _mod.lower().strip() == "ez":
newMods |= mods.EASY
if _mod.lower().strip() == "none":
newMods = 0
if _mod.lower().strip() == "freemod":
freeMod = True
_match.matchModMode = matchModModes.FREE_MOD if freeMod else matchModModes.NORMAL
_match.resetReady()
if _match.matchModMode == matchModModes.FREE_MOD:
_match.resetMods()
_match.changeMods(newMods)
return "Match mods have been updated!"
def mpTeam():
if len(message) < 3:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp team <username> <colour>")
username = message[1].strip()
if not username:
raise exceptions.invalidArgumentsException("Please provide a username")
colour = message[2].lower().strip()
if colour not in ["red", "blue"]:
raise exceptions.invalidArgumentsException("Team colour must be red or blue")
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.changeTeam(userID, matchTeams.BLUE if colour == "blue" else matchTeams.RED)
return "{} is now in {} team".format(username, colour)
def mpSettings():
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
single = False if len(message) < 2 else message[1].strip().lower() == "single"
msg = "PLAYERS IN THIS MATCH "
if not single:
msg += "(use !mp settings single for a single-line version):"
msg += "\n"
else: else:
msg += ": " return "Error in beatmap mirror API request. Tell this to a dev: {}".format(response.text)
empty = True
for slot in _match.slots:
if slot.user is None:
continue
readableStatuses = {
slotStatuses.READY: "ready",
slotStatuses.NOT_READY: "not ready",
slotStatuses.NO_MAP: "no map",
slotStatuses.PLAYING: "playing",
}
if slot.status not in readableStatuses:
readableStatus = "???"
else:
readableStatus = readableStatuses[slot.status]
empty = False
msg += "* [{team}] <{status}> ~ {username}{mods}{nl}".format(
team="red" if slot.team == matchTeams.RED else "blue" if slot.team == matchTeams.BLUE else "!! no team !!",
status=readableStatus,
username=glob.tokens.tokens[slot.user].username,
mods=" (+ {})".format(generalUtils.readableMods(slot.mods)) if slot.mods > 0 else "",
nl=" | " if single else "\n"
)
if empty:
msg += "Nobody.\n"
msg = msg.rstrip(" | " if single else "\n")
return msg
def mpScoreV():
if len(message) < 2 or message[1] not in ("1", "2"):
raise exceptions.invalidArgumentsException("Wrong syntax: !mp scorev <1|2>")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.matchScoringType = matchScoringTypes.SCORE_V2 if message[1] == "2" else matchScoringTypes.SCORE
_match.sendUpdates()
return "Match scoring type set to scorev{}".format(message[1])
def mpHelp():
return "Supported subcommands: !mp <{}>".format("|".join(k for k in subcommands.keys()))
try:
subcommands = {
"make": mpMake,
"close": mpClose,
"join": mpJoin,
"lock": mpLock,
"unlock": mpUnlock,
"size": mpSize,
"move": mpMove,
"host": mpHost,
"clearhost": mpClearHost,
"start": mpStart,
"invite": mpInvite,
"map": mpMap,
"set": mpSet,
"abort": mpAbort,
"kick": mpKick,
"password": mpPassword,
"randompassword": mpRandomPassword,
"mods": mpMods,
"team": mpTeam,
"settings": mpSettings,
"scorev": mpScoreV,
"help": mpHelp
}
requestedSubcommand = message[0].lower().strip()
if requestedSubcommand not in subcommands:
raise exceptions.invalidArgumentsException("Invalid subcommand")
return subcommands[requestedSubcommand]()
except (exceptions.invalidArgumentsException, exceptions.userNotFoundException, exceptions.invalidUserException) as e:
return str(e)
except exceptions.wrongChannelException:
return "This command only works in multiplayer chat channels"
except exceptions.matchNotFoundException:
return "Match not found"
except: except:
raise return False
def switchServer(fro, chan, message):
# Get target user ID
target = message[0]
newServer = message[1].strip()
if not newServer:
return "Invalid server IP"
targetUserID = userUtils.getIDSafe(target)
userID = userUtils.getID(fro)
# Make sure the user exists
if not targetUserID:
return "{}: user not found".format(target)
# Connect the user to the end server
userToken = glob.tokens.getTokenFromUserID(userID, ignoreIRC=True, _all=False)
userToken.enqueue(serverPackets.switchServer(newServer))
# Disconnect the user from the origin server
# userToken.kick()
return "{} has been connected to {}".format(target, newServer)
def rtx(fro, chan, message):
target = message[0]
message = " ".join(message[1:]).strip()
if not message:
return "Invalid message"
targetUserID = userUtils.getIDSafe(target)
if not targetUserID:
return "{}: user not found".format(target)
userToken = glob.tokens.getTokenFromUserID(targetUserID, ignoreIRC=True, _all=False)
userToken.enqueue(serverPackets.rtx(message))
return ":ok_hand:"
def bloodcat(fro, chan, message):
try:
matchID = getMatchIDFromChannel(chan)
except exceptions.wrongChannelException:
matchID = None
try:
spectatorHostUserID = getSpectatorHostUserIDFromChannel(chan)
except exceptions.wrongChannelException:
spectatorHostUserID = None
if matchID is not None:
if matchID not in glob.matches.matches:
return "This match doesn't seem to exist... Or does it...?"
beatmapID = glob.matches.matches[matchID].beatmapID
else:
spectatorHostToken = glob.tokens.getTokenFromUserID(spectatorHostUserID, ignoreIRC=True)
if spectatorHostToken is None:
return "The spectator host is offline."
beatmapID = spectatorHostToken.beatmapID
return bloodcatMessage(beatmapID)
""" """
Commands list Commands list
@ -1220,10 +710,10 @@ commands = [
"callback": faq "callback": faq
}, { }, {
"trigger": "!report", "trigger": "!report",
"callback": report "response": "Report command isn't here yet :c"
}, { }, {
"trigger": "!help", "trigger": "!help",
"response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for the full command list" "response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for FokaBot's full command list"
}, #{ }, #{
#"trigger": "!ask", #"trigger": "!ask",
#"syntax": "<question>", #"syntax": "<question>",
@ -1256,7 +746,7 @@ commands = [
"privileges": privileges.ADMIN_KICK_USERS, "privileges": privileges.ADMIN_KICK_USERS,
"callback": kick "callback": kick
}, { }, {
"trigger": "!bot reconnect", "trigger": "!fokabot reconnect",
"privileges": privileges.ADMIN_MANAGE_SERVERS, "privileges": privileges.ADMIN_MANAGE_SERVERS,
"callback": fokabotReconnect "callback": fokabotReconnect
}, { }, {
@ -1335,24 +825,6 @@ commands = [
}, { }, {
"trigger": "!update", "trigger": "!update",
"callback": updateBeatmap "callback": updateBeatmap
}, {
"trigger": "!mp",
"privileges": privileges.USER_TOURNAMENT_STAFF,
"syntax": "<subcommand>",
"callback": multiplayer
}, {
"trigger": "!switchserver",
"privileges": privileges.ADMIN_MANAGE_SERVERS,
"syntax": "<username> <server_address>",
"callback": switchServer
}, {
"trigger": "!rtx",
"privileges": privileges.ADMIN_MANAGE_USERS,
"syntax": "<username> <message>",
"callback": rtx
}, {
"trigger": "!bloodcat",
"callback": bloodcat
} }
# #
# "trigger": "!acc", # "trigger": "!acc",

View File

@ -1,4 +1,3 @@
SCORE = 0 SCORE = 0
ACCURACY = 1 ACCURACY = 1
COMBO = 2 COMBO = 2
SCORE_V2 = 3

View File

@ -78,7 +78,5 @@ server_userSilenced = 94
server_userPresenceBundle = 96 server_userPresenceBundle = 96
client_userPanelRequest = 97 client_userPanelRequest = 97
client_tournamentMatchInfoRequest = 93 client_tournamentMatchInfoRequest = 93
server_matchAbort = 106
server_switchServer = 107
client_tournamentJoinMatchChannel = 108 client_tournamentJoinMatchChannel = 108
client_tournamentLeaveMatchChannel = 109 client_tournamentLeaveMatchChannel = 109

View File

@ -16,12 +16,12 @@ def forceUpdate():
def loginBanned(): def loginBanned():
packets = packetHelper.buildPacket(packetIDs.server_userID, [[-1, dataTypes.SINT32]]) packets = packetHelper.buildPacket(packetIDs.server_userID, [[-1, dataTypes.SINT32]])
packets += notification("You are banned. You can appeal after one month since your ban by sending an email to {} from the email address you've used to sign up.".format(glob.conf.extra["pep.py"]["support-email"])) packets += notification("You are banned. You can appeal after one month since your ban by sending an email to support@ripple.moe from the email address you've used to sign up.")
return packets return packets
def loginLocked(): def loginLocked():
packets = packetHelper.buildPacket(packetIDs.server_userID, [[-1, dataTypes.SINT32]]) packets = packetHelper.buildPacket(packetIDs.server_userID, [[-1, dataTypes.SINT32]])
packets += notification("Your account is locked. You can't log in, but your profile and scores are still visible from the website. If you want to unlock your account, send an email to {} from the email address you've used to sign up.".format(glob.conf.extra["pep.py"]["support-email"])) packets += notification("Your account is locked. You can't log in, but your profile and scores are still visible from the website. If you want to unlock your account, send an email to support@ripple.moe from the email address you've used to sign up.")
return packets return packets
def loginError(): def loginError():
@ -94,12 +94,12 @@ def userPanel(userID, force = False):
# Get username color according to rank # Get username color according to rank
# Only admins and normal users are currently supported # Only admins and normal users are currently supported
userRank = 0 userRank = 0
if username == glob.BOT_NAME: if username == "FokaBot":
userRank |= userRanks.MOD
elif userUtils.isInPrivilegeGroup(userID, "community manager"):
userRank |= userRanks.MOD userRank |= userRanks.MOD
elif userUtils.isInPrivilegeGroup(userID, "developer"): elif userUtils.isInPrivilegeGroup(userID, "developer"):
userRank |= userRanks.ADMIN userRank |= userRanks.ADMIN
elif userUtils.isInPrivilegeGroup(userID, "chat mod"):
userRank |= userRanks.MOD
elif (userToken.privileges & privileges.USER_DONOR) > 0: elif (userToken.privileges & privileges.USER_DONOR) > 0:
userRank |= userRanks.SUPPORTER userRank |= userRanks.SUPPORTER
else: else:
@ -155,13 +155,11 @@ def channelJoinSuccess(userID, chan):
return packetHelper.buildPacket(packetIDs.server_channelJoinSuccess, [[chan, dataTypes.STRING]]) return packetHelper.buildPacket(packetIDs.server_channelJoinSuccess, [[chan, dataTypes.STRING]])
def channelInfo(chan): def channelInfo(chan):
if chan not in glob.channels.channels:
return bytes()
channel = glob.channels.channels[chan] channel = glob.channels.channels[chan]
return packetHelper.buildPacket(packetIDs.server_channelInfo, [ return packetHelper.buildPacket(packetIDs.server_channelInfo, [
[channel.name, dataTypes.STRING], [chan, dataTypes.STRING],
[channel.description, dataTypes.STRING], [channel.description, dataTypes.STRING],
[len(glob.streams.streams["chat/{}".format(chan)].clients), dataTypes.UINT16] [len(channel.connectedUsers), dataTypes.UINT16]
]) ])
def channelInfoEnd(): def channelInfoEnd():
@ -202,18 +200,17 @@ def createMatch(matchID):
# Get match binary data and build packet # Get match binary data and build packet
match = glob.matches.matches[matchID] match = glob.matches.matches[matchID]
matchData = match.getMatchData(censored=True) return packetHelper.buildPacket(packetIDs.server_newMatch, match.getMatchData())
return packetHelper.buildPacket(packetIDs.server_newMatch, matchData)
# TODO: Add match object argument to save some CPU # TODO: Add match object argument to save some CPU
def updateMatch(matchID, censored = False): def updateMatch(matchID):
# Make sure the match exists # Make sure the match exists
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return bytes() return bytes()
# Get match binary data and build packet # Get match binary data and build packet
match = glob.matches.matches[matchID] match = glob.matches.matches[matchID]
return packetHelper.buildPacket(packetIDs.server_updateMatch, match.getMatchData(censored=censored)) return packetHelper.buildPacket(packetIDs.server_updateMatch, match.getMatchData())
def matchStart(matchID): def matchStart(matchID):
# Make sure the match exists # Make sure the match exists
@ -264,11 +261,6 @@ def playerFailed(slotID):
def matchTransferHost(): def matchTransferHost():
return packetHelper.buildPacket(packetIDs.server_matchTransferHost) return packetHelper.buildPacket(packetIDs.server_matchTransferHost)
def matchAbort():
return packetHelper.buildPacket(packetIDs.server_matchAbort)
def switchServer(address):
return packetHelper.buildPacket(packetIDs.server_switchServer, [[address, dataTypes.STRING]])
""" Other packets """ """ Other packets """
def notification(message): def notification(message):
@ -276,6 +268,3 @@ def notification(message):
def banchoRestart(msUntilReconnection): def banchoRestart(msUntilReconnection):
return packetHelper.buildPacket(packetIDs.server_restart, [[msUntilReconnection, dataTypes.UINT32]]) return packetHelper.buildPacket(packetIDs.server_restart, [[msUntilReconnection, dataTypes.UINT32]])
def rtx(message):
return packetHelper.buildPacket(0x69, [[message, dataTypes.STRING]])

View File

@ -1,4 +1,6 @@
from common.constants import actions
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils
from constants import clientPackets from constants import clientPackets
from constants import serverPackets from constants import serverPackets
from objects import glob from objects import glob

View File

@ -15,9 +15,9 @@ def handle(userToken, packetData):
matchID = userToken.matchID matchID = userToken.matchID
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
match = glob.matches.matches[matchID]
# Set slot or match mods according to modType # Set slot or match mods according to modType
with glob.matches.matches[matchID] as match:
if match.matchModMode == matchModModes.FREE_MOD: if match.matchModMode == matchModModes.FREE_MOD:
# Freemod # Freemod
# Host can set global DT/HT # Host can set global DT/HT

View File

@ -10,7 +10,9 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
with glob.matches.matches[matchID] as match: # Get our match
match = glob.matches.matches[matchID]
# Host check # Host check
if userToken.userID != match.hostUserID: if userToken.userID != match.hostUserID:
return return

View File

@ -21,8 +21,10 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Get match object
match = glob.matches.matches[matchID]
# Host check # Host check
with glob.matches.matches[matchID] as match:
if userToken.userID != match.hostUserID: if userToken.userID != match.hostUserID:
return return
@ -69,7 +71,6 @@ def handle(userToken, packetData):
oldBeatmapMD5 = match.beatmapMD5 oldBeatmapMD5 = match.beatmapMD5
oldMods = match.mods oldMods = match.mods
oldMatchTeamType = match.matchTeamType
match.mods = packetData["mods"] match.mods = packetData["mods"]
match.beatmapMD5 = packetData["beatmapMD5"] match.beatmapMD5 = packetData["beatmapMD5"]
@ -79,19 +80,31 @@ def handle(userToken, packetData):
# Reset ready if needed # Reset ready if needed
if oldMods != match.mods or oldBeatmapMD5 != match.beatmapMD5: if oldMods != match.mods or oldBeatmapMD5 != match.beatmapMD5:
match.resetReady() for i in range(0,16):
if match.slots[i].status == slotStatuses.READY:
match.slots[i].status = slotStatuses.NOT_READY
# Reset mods if needed # Reset mods if needed
if match.matchModMode == matchModModes.NORMAL: if match.matchModMode == matchModModes.NORMAL:
# Reset slot mods if not freeMods # Reset slot mods if not freeMods
match.resetMods() for i in range(0,16):
match.slots[i].mods = 0
else: else:
# Reset match mods if freemod # Reset match mods if freemod
match.mods = 0 match.mods = 0
# Initialize teams if team type changed # Set/reset teams
if match.matchTeamType != oldMatchTeamType: if match.matchTeamType == matchTeamTypes.TEAM_VS or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
match.initializeTeams() # Set teams
c=0
for i in range(0,16):
if match.slots[i].team == matchTeams.NO_TEAM:
match.slots[i].team = matchTeams.RED if c % 2 == 0 else matchTeams.BLUE
c+=1
else:
# Reset teams
for i in range(0,16):
match.slots[i].team = matchTeams.NO_TEAM
# Force no freemods if tag coop # Force no freemods if tag coop
if match.matchTeamType == matchTeamTypes.TAG_COOP or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS: if match.matchTeamType == matchTeamTypes.TAG_COOP or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:

View File

@ -8,6 +8,8 @@ def handle(userToken, packetData):
# Read packet data # Read packet data
packetData = clientPackets.changeSlot(packetData) packetData = clientPackets.changeSlot(packetData)
with glob.matches.matches[userToken.matchID] as match: # Get match
match = glob.matches.matches[userToken.matchID]
# Change slot # Change slot
match.userChangeSlot(userID, packetData["slotID"]) match.userChangeSlot(userID, packetData["slotID"])

View File

@ -1,6 +1,7 @@
from common.log import logUtils as log from common.log import logUtils as log
from constants import clientPackets, serverPackets from constants import clientPackets
from constants import exceptions from constants import exceptions
from constants import serverPackets
from objects import glob from objects import glob
@ -12,32 +13,26 @@ def handle(userToken, packetData):
# Read packet data # Read packet data
packetData = clientPackets.createMatch(packetData) packetData = clientPackets.createMatch(packetData)
# Make sure the name is valid
matchName = packetData["matchName"].strip()
if not matchName:
raise exceptions.matchCreateError()
# Create a match object # Create a match object
# TODO: Player number check (Dirty hack below) # TODO: Player number check
matchID = glob.matches.createMatch(matchName, packetData["matchPassword"].strip(), packetData["beatmapID"], packetData["beatmapName"], packetData["beatmapMD5"], packetData["gameMode"], userID) matchID = glob.matches.createMatch(packetData["matchName"], packetData["matchPassword"], packetData["beatmapID"], packetData["beatmapName"], packetData["beatmapMD5"], packetData["gameMode"], userID)
# Make sure the match has been created # Make sure the match has been created
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
raise exceptions.matchCreateError() raise exceptions.matchCreateError
# Get match object
match = glob.matches.matches[matchID]
with glob.matches.matches[matchID] as match:
# Join that match # Join that match
userToken.joinMatch(matchID) userToken.joinMatch(matchID)
# Disable slots (Dirty)
for i in range(0,16):
if match.slots[i].status is not 4:
match.slots[i].status = packetData["slot{}Status".format(i)]
# Give host to match creator # Give host to match creator
match.setHost(userID) match.setHost(userID)
match.sendUpdates() match.sendUpdates()
match.changePassword(packetData["matchPassword"]) match.changePassword(packetData["matchPassword"])
# Console output
log.info("MPROOM{}: Room created!".format(matchID))
except exceptions.matchCreateError: except exceptions.matchCreateError:
log.error("Error while creating match!") log.error("Error while creating match!")
userToken.enqueue(serverPackets.matchJoinFail())

View File

@ -1,3 +1,4 @@
from common import generalUtils
from common.log import logUtils as log from common.log import logUtils as log
from constants import clientPackets from constants import clientPackets
from constants import exceptions from constants import exceptions
@ -17,14 +18,18 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Match exists, get object
match = glob.matches.matches[matchID]
# Hash password if needed # Hash password if needed
# if password != "": #if password != "":
# password = generalUtils.stringMd5(password) # password = generalUtils.stringMd5(password)
# Check password # Check password
with glob.matches.matches[matchID] as match: # TODO: Admins can enter every match
if match.matchPassword != "" and match.matchPassword != password: if match.matchPassword != "":
raise exceptions.matchWrongPasswordException() if match.matchPassword != password:
raise exceptions.matchWrongPasswordException
# Password is correct, join match # Password is correct, join match
userToken.joinMatch(matchID) userToken.joinMatch(matchID)

View File

@ -15,7 +15,6 @@ from objects import glob
def handle(tornadoRequest): def handle(tornadoRequest):
# Data to return # Data to return
responseToken = None
responseTokenString = "ayy" responseTokenString = "ayy"
responseData = bytes() responseData = bytes()
@ -30,6 +29,9 @@ def handle(tornadoRequest):
# 2:-3 thing is because requestData has some escape stuff that we don't need # 2:-3 thing is because requestData has some escape stuff that we don't need
loginData = str(tornadoRequest.request.body)[2:-3].split("\\n") loginData = str(tornadoRequest.request.body)[2:-3].split("\\n")
try: try:
# If true, print error to console
err = False
# Make sure loginData is valid # Make sure loginData is valid
if len(loginData) < 3: if len(loginData) < 3:
raise exceptions.invalidArgumentsException() raise exceptions.invalidArgumentsException()
@ -61,9 +63,9 @@ def handle(tornadoRequest):
# Make sure we are not banned or locked # Make sure we are not banned or locked
priv = userUtils.getPrivileges(userID) priv = userUtils.getPrivileges(userID)
if userUtils.isBanned(userID) and priv & privileges.USER_PENDING_VERIFICATION == 0: if userUtils.isBanned(userID) == True and priv & privileges.USER_PENDING_VERIFICATION == 0:
raise exceptions.loginBannedException() raise exceptions.loginBannedException()
if userUtils.isLocked(userID) and priv & privileges.USER_PENDING_VERIFICATION == 0: if userUtils.isLocked(userID) == True and priv & privileges.USER_PENDING_VERIFICATION == 0:
raise exceptions.loginLockedException() raise exceptions.loginLockedException()
# 2FA check # 2FA check
@ -75,7 +77,7 @@ def handle(tornadoRequest):
# Verify this user (if pending activation) # Verify this user (if pending activation)
firstLogin = False firstLogin = False
if priv & privileges.USER_PENDING_VERIFICATION > 0 or not userUtils.hasVerifiedHardware(userID): if priv & privileges.USER_PENDING_VERIFICATION > 0 or userUtils.hasVerifiedHardware(userID) == False:
if userUtils.verifyUser(userID, clientData): if userUtils.verifyUser(userID, clientData):
# Valid account # Valid account
log.info("Account {} verified successfully!".format(userID)) log.info("Account {} verified successfully!".format(userID))
@ -118,9 +120,6 @@ def handle(tornadoRequest):
expireIn = "{} days".format(expireDays) if expireDays > 1 else "less than 24 hours" expireIn = "{} days".format(expireDays) if expireDays > 1 else "less than 24 hours"
responseToken.enqueue(serverPackets.notification("Your donor tag expires in {}! When your donor tag expires, you won't have any of the donor privileges, like yellow username, custom badge and discord custom role and username color! If you wish to keep supporting Ripple and you don't want to lose your donor privileges, you can donate again by clicking on 'Support us' on Ripple's website.".format(expireIn))) responseToken.enqueue(serverPackets.notification("Your donor tag expires in {}! When your donor tag expires, you won't have any of the donor privileges, like yellow username, custom badge and discord custom role and username color! If you wish to keep supporting Ripple and you don't want to lose your donor privileges, you can donate again by clicking on 'Support us' on Ripple's website.".format(expireIn)))
# Deprecate telegram 2fa and send alert
if userUtils.deprecateTelegram2Fa(userID):
responseToken.enqueue(serverPackets.notification("As stated on our blog, Telegram 2FA has been deprecated on 29th June 2018. Telegram 2FA has just been disabled from your account. If you want to keep your account secure with 2FA, please enable TOTP-based 2FA from our website https://ripple.moe. Thank you for your patience."))
# Set silence end UNIX time in token # Set silence end UNIX time in token
responseToken.silenceEndTime = userUtils.getSilenceEnd(userID) responseToken.silenceEndTime = userUtils.getSilenceEnd(userID)
@ -176,7 +175,7 @@ def handle(tornadoRequest):
# Output channels info # Output channels info
for key, value in glob.channels.channels.items(): for key, value in glob.channels.channels.items():
if value.publicRead and not value.hidden: if value.publicRead == True and value.hidden == False:
responseToken.enqueue(serverPackets.channelInfo(key)) responseToken.enqueue(serverPackets.channelInfo(key))
# Send friends list # Send friends list
@ -186,11 +185,8 @@ def handle(tornadoRequest):
if glob.banchoConf.config["menuIcon"] != "": if glob.banchoConf.config["menuIcon"] != "":
responseToken.enqueue(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) responseToken.enqueue(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
# Send online users' panels # Send online users IDs array
with glob.tokens: responseToken.enqueue(serverPackets.onlineUsers())
for _, token in glob.tokens.tokens.items():
if not token.restricted:
responseToken.enqueue(serverPackets.userPanel(token.userID))
# Get location and country from ip.zxq.co or database # Get location and country from ip.zxq.co or database
if glob.localize: if glob.localize:
@ -224,22 +220,24 @@ def handle(tornadoRequest):
except exceptions.loginFailedException: except exceptions.loginFailedException:
# Login failed error packet # Login failed error packet
# (we don't use enqueue because we don't have a token since login has failed) # (we don't use enqueue because we don't have a token since login has failed)
err = True
responseData += serverPackets.loginFailed() responseData += serverPackets.loginFailed()
except exceptions.invalidArgumentsException: except exceptions.invalidArgumentsException:
# Invalid POST data # Invalid POST data
# (we don't use enqueue because we don't have a token since login has failed) # (we don't use enqueue because we don't have a token since login has failed)
err = True
responseData += serverPackets.loginFailed() responseData += serverPackets.loginFailed()
responseData += serverPackets.notification("I see what you're doing...") responseData += serverPackets.notification("I see what you're doing...")
except exceptions.loginBannedException: except exceptions.loginBannedException:
# Login banned error packet # Login banned error packet
err = True
responseData += serverPackets.loginBanned() responseData += serverPackets.loginBanned()
except exceptions.loginLockedException: except exceptions.loginLockedException:
# Login banned error packet # Login banned error packet
err = True
responseData += serverPackets.loginLocked() responseData += serverPackets.loginLocked()
except exceptions.banchoMaintenanceException: except exceptions.banchoMaintenanceException:
# Bancho is in maintenance mode # Bancho is in maintenance mode
responseData = bytes()
if responseToken is not None:
responseData = responseToken.queue responseData = responseToken.queue
responseData += serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.") responseData += serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.")
responseData += serverPackets.loginFailed() responseData += serverPackets.loginFailed()
@ -253,6 +251,7 @@ def handle(tornadoRequest):
except exceptions.haxException: except exceptions.haxException:
# Using oldoldold client, we don't have client data. Force update. # Using oldoldold client, we don't have client data. Force update.
# (we don't use enqueue because we don't have a token since login has failed) # (we don't use enqueue because we don't have a token since login has failed)
err = True
responseData += serverPackets.forceUpdate() responseData += serverPackets.forceUpdate()
responseData += serverPackets.notification("Hory shitto, your client is TOO old! Nice prehistory! Please turn update it from the settings!") responseData += serverPackets.notification("Hory shitto, your client is TOO old! Nice prehistory! Please turn update it from the settings!")
except: except:
@ -260,7 +259,10 @@ def handle(tornadoRequest):
finally: finally:
# Console and discord log # Console and discord log
if len(loginData) < 3: if len(loginData) < 3:
log.info("Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP), "bunker") msg = "Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP)
else:
msg = "Bancho login request from **{}** for user **{}** ({})".format(requestIP, loginData[0], "failed" if err == True else "success")
log.info(msg, "bunker")
# Return token string and data # Return token string and data
return responseTokenString, responseData return responseTokenString, responseData

View File

@ -17,7 +17,7 @@ def handle(userToken, _=None, deleteToken=True):
# the old logout packet will still be in the queue and will be sent to # the old logout packet will still be in the queue and will be sent to
# the server, so we accept logout packets sent at least 5 seconds after login # the server, so we accept logout packets sent at least 5 seconds after login
# if the user logs out before 5 seconds, he will be disconnected later with timeout check # if the user logs out before 5 seconds, he will be disconnected later with timeout check
if int(time.time() - userToken.loginTime) >= 5 or userToken.irc: if (int(time.time()-userToken.loginTime) >= 5 or userToken.irc):
# Stop spectating # Stop spectating
userToken.stopSpectating() userToken.stopSpectating()
@ -35,7 +35,7 @@ def handle(userToken, _=None, deleteToken=True):
glob.streams.broadcast("main", serverPackets.userLogout(userID)) glob.streams.broadcast("main", serverPackets.userLogout(userID))
# Disconnect from IRC if needed # Disconnect from IRC if needed
if userToken.irc and glob.irc: if userToken.irc == True and glob.irc == True:
glob.ircServer.forceDisconnection(userToken.username) glob.ircServer.forceDisconnection(userToken.username)
# Delete token # Delete token

View File

@ -1,6 +1,6 @@
from objects import glob from objects import glob
def handle(userToken, _, has): def handle(userToken, packetData, has):
# Get usertoken data # Get usertoken data
userID = userToken.userID userID = userToken.userID
@ -15,6 +15,8 @@ def handle(userToken, _, has):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# The match exists, get object
match = glob.matches.matches[matchID]
# Set has beatmap/no beatmap # Set has beatmap/no beatmap
with glob.matches.matches[matchID] as match:
match.userHasBeatmap(userID, has) match.userHasBeatmap(userID, has)

View File

@ -15,6 +15,8 @@ def handle(userToken, _):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Get match object
match = glob.matches.matches[matchID]
# Change team # Change team
with glob.matches.matches[matchID] as match:
match.changeTeam(userID) match.changeTeam(userID)

View File

@ -1,6 +1,6 @@
from objects import glob from objects import glob
def handle(userToken, _): def handle(userToken, packetData):
# Get usertoken data # Get usertoken data
userID = userToken.userID userID = userToken.userID
@ -15,6 +15,8 @@ def handle(userToken, _):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# The match exists, get object
match = glob.matches.matches[matchID]
# Set our match complete # Set our match complete
with glob.matches.matches[matchID] as match:
match.playerCompleted(userID) match.playerCompleted(userID)

View File

@ -15,6 +15,8 @@ def handle(userToken, _):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Match exists, get object
match = glob.matches.matches[matchID]
# Fail user # Fail user
with glob.matches.matches[matchID] as match:
match.playerFailed(userID) match.playerFailed(userID)

View File

@ -1,5 +1,6 @@
from objects import glob from objects import glob
from constants import serverPackets, clientPackets from constants import slotStatuses
from constants import serverPackets
def handle(userToken, packetData): def handle(userToken, packetData):
# Get usertoken data # Get usertoken data
@ -16,16 +17,11 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Parse the data # The match exists, get object
data = clientPackets.matchFrames(packetData) match = glob.matches.matches[matchID]
with glob.matches.matches[matchID] as match:
# Change slot id in packetData # Change slot id in packetData
slotID = match.getUserSlotID(userID) slotID = match.getUserSlotID(userID)
# Update the score
match.updateScore(slotID, data["totalScore"])
match.updateHP(slotID, data["currentHp"])
# Enqueue frames to who's playing # Enqueue frames to who's playing
glob.streams.broadcast(match.playingStreamName, serverPackets.matchFrames(slotID, packetData)) glob.streams.broadcast(match.playingStreamName, serverPackets.matchFrames(slotID, packetData))

View File

@ -17,6 +17,8 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Get match object
match = glob.matches.matches[matchID]
# Send invite # Send invite
with glob.matches.matches[matchID] as match:
match.invite(userID, packetData["userID"]) match.invite(userID, packetData["userID"])

View File

@ -12,8 +12,8 @@ def handle(userToken, packetData):
matchID = userToken.matchID matchID = userToken.matchID
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
match = glob.matches.matches[matchID]
with glob.matches.matches[matchID] as match:
# Host check # Host check
if userID != match.hostUserID: if userID != match.hostUserID:
return return
@ -24,4 +24,4 @@ def handle(userToken, packetData):
return return
# Lock/Unlock slot # Lock/Unlock slot
match.toggleSlotLocked(packetData["slotID"]) match.toggleSlotLock(packetData["slotID"])

View File

@ -1,6 +1,6 @@
from objects import glob from objects import glob
def handle(userToken, _): def handle(userToken, packetData):
# Get userToken data # Get userToken data
userID = userToken.userID userID = userToken.userID
@ -15,6 +15,8 @@ def handle(userToken, _):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# The match exists, get object
match = glob.matches.matches[matchID]
# Set our load status # Set our load status
with glob.matches.matches[matchID] as match:
match.playerLoaded(userID) match.playerLoaded(userID)

View File

@ -8,14 +8,9 @@ def handle(userToken, _):
matchID = userToken.matchID matchID = userToken.matchID
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
match = glob.matches.matches[matchID]
with glob.matches.matches[matchID] as match:
# Get our slotID and change ready status # Get our slotID and change ready status
slotID = match.getUserSlotID(userID) slotID = match.getUserSlotID(userID)
if slotID is not None: if slotID is not None:
match.toggleSlotReady(slotID) match.toggleSlotReady(slotID)
# If this is a tournament match, we should send the current status of ready
# players.
if match.isTourney:
match.sendReadyStatus()

View File

@ -1,6 +1,6 @@
from objects import glob from objects import glob
def handle(userToken, _): def handle(userToken, packetData):
# Get userToken data # Get userToken data
userID = userToken.userID userID = userToken.userID
@ -15,6 +15,8 @@ def handle(userToken, _):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# The match exists, get object
match = glob.matches.matches[matchID]
# Skip # Skip
with glob.matches.matches[matchID] as match:
match.playerSkip(userID) match.playerSkip(userID)

View File

@ -1,4 +1,6 @@
from objects import glob from objects import glob
from constants import slotStatuses
from constants import serverPackets
def handle(userToken, _): def handle(userToken, _):
@ -13,7 +15,9 @@ def handle(userToken, _):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
with glob.matches.matches[matchID] as match: # The match exists, get object
match = glob.matches.matches[matchID]
# Host check # Host check
if userToken.userID != match.hostUserID: if userToken.userID != match.hostUserID:
return return

View File

@ -16,8 +16,10 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Match exists, get object
match = glob.matches.matches[matchID]
# Host check # Host check
with glob.matches.matches[matchID] as match:
if userToken.userID != match.hostUserID: if userToken.userID != match.hostUserID:
return return

View File

@ -1,15 +1,17 @@
from common.log import logUtils as log from common.log import logUtils as log
from helpers import chatHelper as chat from helpers import chatHelper as chat
from objects import glob
def handle(userToken, _): def handle(userToken, _):
# Get usertoken data # Get usertoken data
userID = userToken.userID
username = userToken.username username = userToken.username
# Remove user from users in lobby # Remove user from users in lobby
userToken.leaveStream("lobby") userToken.leaveStream("lobby")
# Part lobby channel # Part lobby channel
# Done automatically by the client
chat.partChannel(channel="#lobby", token=userToken, kick=True) chat.partChannel(channel="#lobby", token=userToken, kick=True)
# Console output # Console output

View File

@ -1,7 +1,6 @@
from common.log import logUtils as log from common.log import logUtils as log
from constants import clientPackets from constants import clientPackets
from constants import serverPackets from constants import serverPackets
from objects import glob
def handle(userToken, packetData): def handle(userToken, packetData):
@ -19,5 +18,5 @@ def handle(userToken, packetData):
fokaMessage = "Your away message has been reset" fokaMessage = "Your away message has been reset"
else: else:
fokaMessage = "Your away message is now: {}".format(packetData["awayMessage"]) fokaMessage = "Your away message is now: {}".format(packetData["awayMessage"])
userToken.enqueue(serverPackets.sendMessage(glob.BOT_NAME, username, fokaMessage)) userToken.enqueue(serverPackets.sendMessage("FokaBot", username, fokaMessage))
log.info("{} has changed their away message to: {}".format(username, packetData["awayMessage"])) log.info("{} has changed their away message to: {}".format(username, packetData["awayMessage"]))

View File

@ -1,15 +1,31 @@
from objects import glob from objects import glob
from constants import serverPackets from constants import serverPackets
from common.log import logUtils as log from constants import exceptions
def handle(userToken, packetData): def handle(userToken, packetData):
# get token data # get token data
userID = userToken.userID userID = userToken.userID
# Send spectator frames to every spectator # Send spectator frames to every spectator
streamName = "spect/{}".format(userID) glob.streams.broadcast("spect/{}".format(userID), serverPackets.spectatorFrames(packetData[7:]))
glob.streams.broadcast(streamName, serverPackets.spectatorFrames(packetData[7:])) '''for i in userToken.spectators:
log.debug("Broadcasting {}'s frames to {} clients".format( # Send to every user but host
userID, if i != userID:
len(glob.streams.streams[streamName].clients)) try:
) # Get spectator token object
spectatorToken = glob.tokens.getTokenFromUserID(i)
# Make sure the token exists
if spectatorToken is None:
raise exceptions.stopSpectating
# Make sure this user is spectating us
if spectatorToken.spectating != userID:
raise exceptions.stopSpectating
# Everything seems fine, send spectator frames to this spectator
spectatorToken.enqueue(serverPackets.spectatorFrames(packetData[7:]))
except exceptions.stopSpectating:
# Remove this user from spectators
userToken.removeSpectator(i)
userToken.enqueue(serverPackets.removeSpectator(i))'''

View File

@ -8,11 +8,6 @@ def handle(userToken, packetData):
# Start spectating packet # Start spectating packet
packetData = clientPackets.startSpectating(packetData) packetData = clientPackets.startSpectating(packetData)
# If the user id is less than 0, treat this as a stop spectating packet
if packetData["userID"] < 0:
userToken.stopSpectating()
return
# Get host token # Get host token
targetToken = glob.tokens.getTokenFromUserID(packetData["userID"]) targetToken = glob.tokens.getTokenFromUserID(packetData["userID"])
if targetToken is None: if targetToken is None:

View File

@ -5,7 +5,7 @@ from helpers import chatHelper as chat
def handle(userToken, packetData): def handle(userToken, packetData):
packetData = clientPackets.tournamentJoinMatchChannel(packetData) packetData = clientPackets.tournamentJoinMatchChannel(packetData)
matchID = packetData["matchID"] matchID = packetData["matchID"]
if matchID not in glob.matches.matches or not userToken.tournament: if matchID not in glob.matches.matches:
return return
userToken.matchID = matchID userToken.matchID = matchID
chat.joinChannel(token=userToken, channel="#multi_{}".format(matchID), force=True) chat.joinChannel(token=userToken, channel="#multi_{}".format(matchID))

View File

@ -5,7 +5,7 @@ from helpers import chatHelper as chat
def handle(userToken, packetData): def handle(userToken, packetData):
packetData = clientPackets.tournamentLeaveMatchChannel(packetData) packetData = clientPackets.tournamentLeaveMatchChannel(packetData)
matchID = packetData["matchID"] matchID = packetData["matchID"]
if matchID not in glob.matches.matches or not userToken.tournament: if matchID not in glob.matches.matches:
return return
chat.partChannel(token=userToken, channel="#multi_{}".format(matchID), force=True) chat.partChannel(token=userToken, channel="#multi_{}".format(matchID))
userToken.matchID = 0 userToken.matchID = 0

View File

@ -1,10 +1,10 @@
from constants import clientPackets from constants import clientPackets
from constants import serverPackets
from objects import glob from objects import glob
def handle(userToken, packetData): def handle(userToken, packetData):
packetData = clientPackets.tournamentMatchInfoRequest(packetData) packetData = clientPackets.tournamentMatchInfoRequest(packetData)
matchID = packetData["matchID"] matchID = packetData["matchID"]
if matchID not in glob.matches.matches or not userToken.tournament: if matchID not in glob.matches.matches:
return return
with glob.matches.matches[matchID] as m: userToken.enqueue(glob.matches.matches[matchID].matchDataCache)
userToken.enqueue(m.matchDataCache)

View File

@ -14,10 +14,3 @@ boobs=bob
tits=teeth tits=teeth
cum=yogurt cum=yogurt
cunt=count cunt=count
nigger=flowers
ngger=flowers
niggers=flowers
weed=grass
AQN=meme
theaquila=meme
aquila=meme

View File

@ -1,9 +1,6 @@
import json import json
import tornado.web from common.log import logUtils as log
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager from common.web import requestsManager
from constants import exceptions from constants import exceptions
from helpers import chatHelper from helpers import chatHelper
@ -11,9 +8,6 @@ from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}
@ -27,11 +21,7 @@ class handler(requestsManager.asyncRequestHandler):
if key is None or key != glob.conf.config["server"]["cikey"]: if key is None or key != glob.conf.config["server"]["cikey"]:
raise exceptions.invalidArgumentsException() raise exceptions.invalidArgumentsException()
chatHelper.sendMessage( chatHelper.sendMessage("FokaBot", self.get_argument("to"), self.get_argument("msg"))
glob.BOT_NAME,
self.get_argument("to").encode().decode("ASCII", "ignore"),
self.get_argument("msg").encode().decode("ASCII", "ignore")
)
# Status code and message # Status code and message
statusCode = 200 statusCode = 200

View File

@ -1,19 +1,11 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.ripple import userUtils
from common.web import requestsManager from common.web import requestsManager
from constants import exceptions from constants import exceptions
from objects import glob from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}
@ -26,8 +18,7 @@ class handler(requestsManager.asyncRequestHandler):
username = None username = None
userID = None userID = None
if "u" in self.request.arguments: if "u" in self.request.arguments:
#username = self.get_argument("u").lower().replace(" ", "_") username = self.get_argument("u").lower().replace(" ", "_")
username = userUtils.safeUsername(self.get_argument("u"))
else: else:
try: try:
userID = int(self.get_argument("id")) userID = int(self.get_argument("id"))

View File

@ -1,17 +1,10 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager from common.web import requestsManager
from objects import glob from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}

View File

@ -1,23 +1,16 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager from common.web import requestsManager
from objects import glob from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}
try: try:
# Get online users count # Get online users count
data["result"] = -1 if glob.restarting else 1 data["result"] = -1 if glob.restarting == True else 1
# Status code and message # Status code and message
statusCode = 200 statusCode = 200

View File

@ -1,18 +1,11 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager from common.web import requestsManager
from constants import exceptions from constants import exceptions
from objects import glob from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}

View File

@ -1,9 +1,5 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.log import logUtils as log from common.log import logUtils as log
from common.web import requestsManager from common.web import requestsManager
from constants import exceptions from constants import exceptions
@ -12,9 +8,6 @@ from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}

View File

@ -56,14 +56,13 @@ from events import tournamentJoinMatchChannelEvent
from events import tournamentLeaveMatchChannelEvent from events import tournamentLeaveMatchChannelEvent
from helpers import packetHelper from helpers import packetHelper
from objects import glob from objects import glob
from common.sentry import sentry
class handler(requestsManager.asyncRequestHandler): class handler(SentryMixin, requestsManager.asyncRequestHandler):
@tornado.web.asynchronous @tornado.web.asynchronous
@tornado.gen.engine @tornado.gen.engine
@sentry.captureTornado
def asyncPost(self): def asyncPost(self):
try:
# Track time if needed # Track time if needed
if glob.outputRequestTime: if glob.outputRequestTime:
# Start time # Start time
@ -93,7 +92,7 @@ class handler(requestsManager.asyncRequestHandler):
# Token exists, get its object and lock it # Token exists, get its object and lock it
userToken = glob.tokens.tokens[requestTokenString] userToken = glob.tokens.tokens[requestTokenString]
userToken.processingLock.acquire() userToken.lock.acquire()
# Keep reading packets until everything has been read # Keep reading packets until everything has been read
while pos < len(requestData): while pos < len(requestData):
@ -106,7 +105,7 @@ class handler(requestsManager.asyncRequestHandler):
packetData = requestData[pos:(pos+dataLength+7)] packetData = requestData[pos:(pos+dataLength+7)]
# Console output if needed # Console output if needed
if glob.outputPackets and packetID != 4: if glob.outputPackets == True and packetID != 4:
log.debug("Incoming packet ({})({}):\n\nPacket code: {}\nPacket length: {}\nSingle packet data: {}\n".format(requestTokenString, userToken.username, str(packetID), str(dataLength), str(packetData))) log.debug("Incoming packet ({})({}):\n\nPacket code: {}\nPacket length: {}\nSingle packet data: {}\n".format(requestTokenString, userToken.username, str(packetID), str(dataLength), str(packetData)))
# Event handler # Event handler
@ -179,7 +178,7 @@ class handler(requestsManager.asyncRequestHandler):
# Process/ignore packet # Process/ignore packet
if packetID != 4: if packetID != 4:
if packetID in eventHandler: if packetID in eventHandler:
if not userToken.restricted or (userToken.restricted and packetID in packetsRestricted): if userToken.restricted == False or (userToken.restricted == True and packetID in packetsRestricted):
eventHandler[packetID]() eventHandler[packetID]()
else: else:
log.warning("Ignored packet id from {} ({}) (user is restricted)".format(requestTokenString, packetID)) log.warning("Ignored packet id from {} ({}) (user is restricted)".format(requestTokenString, packetID))
@ -205,8 +204,8 @@ class handler(requestsManager.asyncRequestHandler):
if userToken is not None: if userToken is not None:
# Update ping time for timeout # Update ping time for timeout
userToken.updatePingTime() userToken.updatePingTime()
# Release processing lock # Release token lock
userToken.processingLock.release() userToken.lock.release()
# Delete token if kicked # Delete token if kicked
if userToken.kicked: if userToken.kicked:
glob.tokens.deleteToken(userToken) glob.tokens.deleteToken(userToken)
@ -239,11 +238,17 @@ class handler(requestsManager.asyncRequestHandler):
self.add_header("Connection", "keep-alive") self.add_header("Connection", "keep-alive")
self.add_header("Keep-Alive", "timeout=5, max=100") self.add_header("Keep-Alive", "timeout=5, max=100")
self.add_header("Content-Type", "text/html; charset=UTF-8") self.add_header("Content-Type", "text/html; charset=UTF-8")
except:
log.error("Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc()))
if glob.sentry:
yield tornado.gen.Task(self.captureException, exc_info=True)
#finally:
# self.finish()
@tornado.web.asynchronous @tornado.web.asynchronous
@tornado.gen.engine @tornado.gen.engine
def asyncGet(self): def asyncGet(self):
html = "<html><head><title>MA MAURO ESISTE?</title><style type='text/css'>body{width:30%;background:#222;color:#fff;}</style></head><body><pre>" html = "<html><head><title>MA MAURO ESISTE?</title><style type='text/css'>body{width:30%}</style></head><body><pre>"
html += " _ __<br>" html += " _ __<br>"
html += " (_) / /<br>" html += " (_) / /<br>"
html += " ______ __ ____ ____ / /____<br>" html += " ______ __ ____ ____ / /____<br>"
@ -262,5 +267,5 @@ class handler(requestsManager.asyncRequestHandler):
html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/<br>" html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/<br>"
html += " \\ . .. .. . /<br>" html += " \\ . .. .. . /<br>"
html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<br>" html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<br>"
html += "</marquee><br><strike>reverse engineering a protocol impossible to reverse engineer since always</strike><br>we are actually reverse engineering bancho successfully. for the third time.<br><br>Running osufx branch.<br><i>&copy; Ripple team, 2016</i></pre></body></html>" html += "</marquee><br><strike>reverse engineering a protocol impossible to reverse engineer since always</strike><br>we are actually reverse engineering bancho successfully. for the third time.<br><br><i>&copy; Ripple team, 2016</i></pre></body></html>"
self.write(html) self.write(html)

View File

@ -8,7 +8,7 @@ from objects import fokabot
from objects import glob from objects import glob
def joinChannel(userID = 0, channel = "", token = None, toIRC = True, force=False): def joinChannel(userID = 0, channel = "", token = None, toIRC = True):
""" """
Join a channel Join a channel
@ -16,7 +16,6 @@ def joinChannel(userID = 0, channel = "", token = None, toIRC = True, force=Fals
:param token: user token object of user that joins the channel. Optional. userID can be used instead. :param token: user token object of user that joins the channel. Optional. userID can be used instead.
:param channel: channel name :param channel: channel name
:param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Default: True :param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Default: True
:param force: whether to allow game clients to join #spect_ and #multi_ channels
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
""" """
try: try:
@ -28,43 +27,50 @@ def joinChannel(userID = 0, channel = "", token = None, toIRC = True, force=Fals
raise exceptions.userNotFoundException raise exceptions.userNotFoundException
else: else:
token = token token = token
userID = token.userID
# Get usertoken data
username = token.username
# Normal channel, do check stuff # Normal channel, do check stuff
# Make sure the channel exists # Make sure the channel exists
if channel not in glob.channels.channels: if channel not in glob.channels.channels:
raise exceptions.channelUnknownException() raise exceptions.channelUnknownException
# Make sure a game client is not trying to join a #multi_ or #spect_ channel manually # Check channel permissions
channelObject = glob.channels.channels[channel] channelObject = glob.channels.channels[channel]
if channelObject.isSpecial and not token.irc and not force: if channelObject.publicRead == False and token.admin == False:
raise exceptions.channelUnknownException() raise exceptions.channelNoPermissionsException
# Add our userID to users in that channel
channelObject.userJoin(userID)
# Add the channel to our joined channel # Add the channel to our joined channel
token.joinChannel(channelObject) token.joinChannel(channel)
# Send channel joined (bancho). We use clientName here because of #multiplayer and #spectator channels
token.enqueue(serverPackets.channelJoinSuccess(userID, channelObject.clientName))
# Send channel joined (IRC) # Send channel joined (IRC)
if glob.irc and not toIRC: if glob.irc == True and toIRC == True:
glob.ircServer.banchoJoinChannel(token.username, channel) glob.ircServer.banchoJoinChannel(username, channel)
# Console output # Console output
log.info("{} joined channel {}".format(token.username, channel)) log.info("{} joined channel {}".format(username, channel))
# IRC code return # IRC code return
return 0 return 0
except exceptions.channelNoPermissionsException: except exceptions.channelNoPermissionsException:
log.warning("{} attempted to join channel {}, but they have no read permissions".format(token.username, channel)) log.warning("{} attempted to join channel {}, but they have no read permissions".format(username, channel))
return 403 return 403
except exceptions.channelUnknownException: except exceptions.channelUnknownException:
log.warning("{} attempted to join an unknown channel ({})".format(token.username, channel)) log.warning("{} attempted to join an unknown channel ({})".format(username, channel))
return 403
except exceptions.userAlreadyInChannelException:
log.warning("User {} already in channel {}".format(token.username, channel))
return 403 return 403
except exceptions.userNotFoundException: except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho") log.warning("User not connected to IRC/Bancho")
return 403 # idk return 403 # idk
def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = False, force=False): def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = False):
""" """
Part a channel Part a channel
@ -73,22 +79,21 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
:param channel: channel name :param channel: channel name
:param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Optional. Default: True :param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Optional. Default: True
:param kick: if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False :param kick: if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False
:param force: whether to allow game clients to part #spect_ and #multi_ channels
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
""" """
try: try:
# Make sure the client is not drunk and sends partChannel when closing a PM tab
if not channel.startswith("#"):
return
# Get token if not defined # Get token if not defined
if token is None: if token is None:
token = glob.tokens.getTokenFromUserID(userID) token = glob.tokens.getTokenFromUserID(userID)
# Make sure the token exists # Make sure the token exists
if token is None: if token is None:
raise exceptions.userNotFoundException() raise exceptions.userNotFoundException
else: else:
token = token token = token
userID = token.userID
# Get usertoken data
username = token.username
# Determine internal/client name if needed # Determine internal/client name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels) # (toclient is used clientwise for #multiplayer and #spectator channels)
@ -108,24 +113,12 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
# Make sure the channel exists # Make sure the channel exists
if channel not in glob.channels.channels: if channel not in glob.channels.channels:
raise exceptions.channelUnknownException() raise exceptions.channelUnknownException
# Make sure a game client is not trying to join a #multi_ or #spect_ channel manually
channelObject = glob.channels.channels[channel]
if channelObject.isSpecial and not token.irc and not force:
raise exceptions.channelUnknownException()
# Make sure the user is in the channel
if channel not in token.joinedChannels:
raise exceptions.userNotInChannelException()
# Part channel (token-side and channel-side) # Part channel (token-side and channel-side)
token.partChannel(channelObject) channelObject = glob.channels.channels[channel]
token.partChannel(channel)
# Delete temporary channel if everyone left channelObject.userPart(userID)
if "chat/{}".format(channelObject.name) in glob.streams.streams:
if channelObject.temp and len(glob.streams.streams["chat/{}".format(channelObject.name)].clients) - 1 == 0:
glob.channels.removeChannel(channelObject.name)
# Force close tab if needed # Force close tab if needed
# NOTE: Maybe always needed, will check later # NOTE: Maybe always needed, will check later
@ -133,20 +126,17 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
token.enqueue(serverPackets.channelKicked(channelClient)) token.enqueue(serverPackets.channelKicked(channelClient))
# IRC part # IRC part
if glob.irc and toIRC: if glob.irc == True and toIRC == True:
glob.ircServer.banchoPartChannel(token.username, channel) glob.ircServer.banchoPartChannel(username, channel)
# Console output # Console output
log.info("{} parted channel {} ({})".format(token.username, channel, channelClient)) log.info("{} parted channel {} ({})".format(username, channel, channelClient))
# Return IRC code # Return IRC code
return 0 return 0
except exceptions.channelUnknownException: except exceptions.channelUnknownException:
log.warning("{} attempted to part an unknown channel ({})".format(token.username, channel)) log.warning("{} attempted to part an unknown channel ({})".format(username, channel))
return 403 return 403
except exceptions.userNotInChannelException:
log.warning("{} attempted to part {}, but he's not in that channel".format(token.username, channel))
return 442
except exceptions.userNotFoundException: except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho") log.warning("User not connected to IRC/Bancho")
return 442 # idk return 442 # idk
@ -163,20 +153,24 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
""" """
try: try:
#tokenString = "" tokenString = ""
# Get token object if not passed # Get token object if not passed
if token is None: if token is None:
token = glob.tokens.getTokenFromUsername(fro) token = glob.tokens.getTokenFromUsername(fro)
if token is None: if token is None:
raise exceptions.userNotFoundException() raise exceptions.userNotFoundException
else: else:
# token object alredy passed, get its string and its username (fro) # token object alredy passed, get its string and its username (fro)
fro = token.username fro = token.username
#tokenString = token.token tokenString = token.token
# Set some variables
userID = token.userID
username = token.username
# Make sure this is not a tournament client # Make sure this is not a tournament client
# if token.tournament: if token.tournament:
# raise exceptions.userTournamentException() raise exceptions.userTournamentException()
# Make sure the user is not in restricted mode # Make sure the user is not in restricted mode
if token.restricted: if token.restricted:
@ -186,16 +180,12 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
if token.isSilenced(): if token.isSilenced():
raise exceptions.userSilencedException() raise exceptions.userSilencedException()
# Redirect !report to FokaBot
if message.startswith("!report"):
to = glob.BOT_NAME
# Determine internal name if needed # Determine internal name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels) # (toclient is used clientwise for #multiplayer and #spectator channels)
toClient = to toClient = to
if to == "#spectator": if to == "#spectator":
if token.spectating is None: if token.spectating is None:
s = token.userID s = userID
else: else:
s = token.spectatingUserID s = token.spectatingUserID
to = "#spect_{}".format(s) to = "#spect_{}".format(s)
@ -206,10 +196,6 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
elif to.startswith("#multi_"): elif to.startswith("#multi_"):
toClient = "#multiplayer" toClient = "#multiplayer"
# Make sure the message is valid
if not message.strip():
raise exceptions.invalidArgumentsException()
# Truncate message if > 2048 characters # Truncate message if > 2048 characters
message = message[:2048]+"..." if len(message) > 2048 else message message = message[:2048]+"..." if len(message) > 2048 else message
@ -217,7 +203,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
message = glob.chatFilters.filterMessage(message) message = glob.chatFilters.filterMessage(message)
# Build packet bytes # Build packet bytes
packet = serverPackets.sendMessage(token.username, toClient, message) packet = serverPackets.sendMessage(username, toClient, message)
# Send the message # Send the message
isChannel = to.startswith("#") isChannel = to.startswith("#")
@ -225,103 +211,94 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# CHANNEL # CHANNEL
# Make sure the channel exists # Make sure the channel exists
if to not in glob.channels.channels: if to not in glob.channels.channels:
raise exceptions.channelUnknownException() raise exceptions.channelUnknownException
# Make sure the channel is not in moderated mode # Make sure the channel is not in moderated mode
if glob.channels.channels[to].moderated and not token.admin: if glob.channels.channels[to].moderated == True and token.admin == False:
raise exceptions.channelModeratedException() raise exceptions.channelModeratedException
# Make sure we are in the channel
if to not in token.joinedChannels:
# I'm too lazy to put and test the correct IRC error code here...
# but IRC is not strict at all so who cares
raise exceptions.channelNoPermissionsException()
# Make sure we have write permissions # Make sure we have write permissions
if not glob.channels.channels[to].publicWrite and not token.admin: if glob.channels.channels[to].publicWrite == False and token.admin == False:
raise exceptions.channelNoPermissionsException() raise exceptions.channelNoPermissionsException
# Add message in buffer
token.addMessageInBuffer(to, message)
# Everything seems fine, build recipients list and send packet # Everything seems fine, build recipients list and send packet
glob.streams.broadcast("chat/{}".format(to), packet, but=[token.token]) recipients = glob.channels.channels[to].connectedUsers[:]
for key, value in glob.tokens.tokens.items():
# Skip our client and irc clients
if key == tokenString or value.irc == True:
continue
# Send to this client if it's connected to the channel
if value.userID in recipients:
value.enqueue(packet)
else: else:
# USER # USER
# Make sure recipient user is connected # Make sure recipient user is connected
recipientToken = glob.tokens.getTokenFromUsername(to) recipientToken = glob.tokens.getTokenFromUsername(to)
if recipientToken is None: if recipientToken is None:
raise exceptions.userNotFoundException() raise exceptions.userNotFoundException
# Make sure the recipient is not a tournament client # Make sure the recipient is not a tournament client
#if recipientToken.tournament: if recipientToken.tournament:
# raise exceptions.userTournamentException() raise exceptions.userTournamentException()
# Make sure the recipient is not restricted or we are FokaBot # Make sure the recipient is not restricted or we are FokaBot
if recipientToken.restricted and fro.lower() != glob.BOT_NAME.lower(): if recipientToken.restricted == True and fro.lower() != "fokabot":
raise exceptions.userRestrictedException() raise exceptions.userRestrictedException()
# TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend # TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend
# Away check # Away check
if recipientToken.awayCheck(token.userID): if recipientToken.awayCheck(userID):
sendMessage(to, fro, "\x01ACTION is away: {}\x01".format(recipientToken.awayMessage)) sendMessage(to, fro, "\x01ACTION is away: {message}\x01".format(code=chr(int(1)), message=recipientToken.awayMessage))
# Check message templates (mods/admins only) # Check message templates (mods/admins only)
if message in messageTemplates.templates and token.admin: if message in messageTemplates.templates and token.admin == True:
sendMessage(fro, to, messageTemplates.templates[message]) sendMessage(fro, to, messageTemplates.templates[message])
# Everything seems fine, send packet # Everything seems fine, send packet
recipientToken.enqueue(packet) recipientToken.enqueue(packet)
# Send the message to IRC # Send the message to IRC
if glob.irc and toIRC: if glob.irc == True and toIRC == True:
messageSplitInLines = message.encode("latin-1").decode("utf-8").split("\n") glob.ircServer.banchoMessage(fro, to, message)
for line in messageSplitInLines:
if line == messageSplitInLines[:1] and line == "":
continue
glob.ircServer.banchoMessage(fro, to, line)
# Spam protection (ignore FokaBot) # Spam protection (ignore FokaBot)
if token.userID > 999: if userID > 999:
token.spamProtection() token.spamProtection()
# Fokabot message # Fokabot message
if isChannel or to.lower() == glob.BOT_NAME.lower(): if isChannel == True or to.lower() == "fokabot":
fokaMessage = fokabot.fokabotResponse(token.username, to, message) fokaMessage = fokabot.fokabotResponse(username, to, message)
if fokaMessage: if fokaMessage:
sendMessage(glob.BOT_NAME, to if isChannel else fro, fokaMessage) sendMessage("FokaBot", to if isChannel else fro, fokaMessage)
# File and discord logs (public chat only) # File and discord logs (public chat only)
if to.startswith("#") and not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")): if to.startswith("#") and not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")):
log.chat("{fro} @ {to}: {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8"))) log.chat("{fro} @ {to}: {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))))
glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8"))) glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))[2:-1]))
return 0 return 0
except exceptions.userSilencedException: except exceptions.userSilencedException:
token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft())) token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft()))
log.warning("{} tried to send a message during silence".format(token.username)) log.warning("{} tried to send a message during silence".format(username))
return 404 return 404
except exceptions.channelModeratedException: except exceptions.channelModeratedException:
log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(token.username, to)) log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(username, to))
return 404 return 404
except exceptions.channelUnknownException: except exceptions.channelUnknownException:
log.warning("{} tried to send a message to an unknown channel ({})".format(token.username, to)) log.warning("{} tried to send a message to an unknown channel ({})".format(username, to))
return 403 return 403
except exceptions.channelNoPermissionsException: except exceptions.channelNoPermissionsException:
log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(token.username, to)) log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(username, to))
return 404 return 404
except exceptions.userRestrictedException: except exceptions.userRestrictedException:
log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(token.username, to)) log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(username, to))
return 404 return 404
except exceptions.userTournamentException: except exceptions.userTournamentException:
log.warning("{} tried to send a message {}, but the recipient is a tournament client".format(token.username, to)) log.warning("{} tried to send a message {}, but the recipient is a tournament client".format(username, to))
return 404 return 404
except exceptions.userNotFoundException: except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho") log.warning("User not connected to IRC/Bancho")
return 401 return 401
except exceptions.invalidArgumentsException:
log.warning("{} tried to send an invalid message to {}".format(token.username, to))
return 404
""" IRC-Bancho Connect/Disconnect/Join/Part interfaces""" """ IRC-Bancho Connect/Disconnect/Join/Part interfaces"""

View File

@ -48,16 +48,16 @@ class config:
self.config.get("server","gziplevel") self.config.get("server","gziplevel")
self.config.get("server","cikey") self.config.get("server","cikey")
self.config.get("cheesegull", "apiurl") self.config.get("mirror","url")
self.config.get("cheesegull", "apikey") self.config.get("mirror","apikey")
self.config.get("debug","enable") self.config.get("debug","enable")
self.config.get("debug","packets") self.config.get("debug","packets")
self.config.get("debug","time") self.config.get("debug","time")
self.config.get("sentry","enable") self.config.get("sentry","enable")
self.config.get("sentry","banchodsn") self.config.get("sentry","banchodns")
self.config.get("sentry","ircdsn") self.config.get("sentry","ircdns")
self.config.get("discord","enable") self.config.get("discord","enable")
self.config.get("discord","boturl") self.config.get("discord","boturl")
@ -73,10 +73,8 @@ class config:
self.config.get("localize","enable") self.config.get("localize","enable")
self.config.get("localize","ipapiurl") self.config.get("localize","ipapiurl")
self.config.get("custom", "config")
return True return True
except configparser.Error: except:
return False return False
def generateDefaultConfig(self): def generateDefaultConfig(self):
@ -109,9 +107,9 @@ class config:
self.config.set("server", "gziplevel", "6") self.config.set("server", "gziplevel", "6")
self.config.set("server", "cikey", "changeme") self.config.set("server", "cikey", "changeme")
self.config.add_section("cheesegull") self.config.add_section("mirror")
self.config.set("cheesegull", "apiurl", "http://cheesegu.ll/api") self.config.set("mirror", "url", "http://storage.ripple.moe")
self.config.set("cheesegull", "apikey", "") self.config.set("mirror", "apikey", "anotherkey")
self.config.add_section("debug") self.config.add_section("debug")
self.config.set("debug", "enable", "0") self.config.set("debug", "enable", "0")
@ -120,8 +118,8 @@ class config:
self.config.add_section("sentry") self.config.add_section("sentry")
self.config.set("sentry", "enable", "0") self.config.set("sentry", "enable", "0")
self.config.set("sentry", "banchodsn", "") self.config.set("sentry", "banchodns", "")
self.config.set("sentry", "ircdsn", "") self.config.set("sentry", "ircdns", "")
self.config.add_section("discord") self.config.add_section("discord")
self.config.set("discord", "enable", "0") self.config.set("discord", "enable", "0")
@ -142,9 +140,6 @@ class config:
self.config.set("localize", "enable", "1") self.config.set("localize", "enable", "1")
self.config.set("localize", "ipapiurl", "http://ip.zxq.co") self.config.set("localize", "ipapiurl", "http://ip.zxq.co")
self.config.add_section("custom")
self.config.set("custom", "config", "common/config.json")
# Write ini to file and close # Write ini to file and close
self.config.write(f) self.config.write(f)
f.close() f.close()

View File

@ -27,11 +27,8 @@ def printServerStartHeader(asciiArt=True):
print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^{}".format(bcolors.ENDC)) print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^{}".format(bcolors.ENDC))
printColored("> Welcome to pep.py osu!bancho server v{}".format(glob.VERSION), bcolors.GREEN) printColored("> Welcome to pep.py osu!bancho server v{}".format(glob.VERSION), bcolors.GREEN)
printColored("> Common submodule v{}".format(glob.COMMON_VERSION), bcolors.GREEN)
printColored("> Made by the Ripple team", bcolors.GREEN) printColored("> Made by the Ripple team", bcolors.GREEN)
printColored("> {}https://zxq.co/ripple/pep.py".format(bcolors.UNDERLINE), bcolors.GREEN) printColored("> {}https://git.zxq.co/ripple/pep.py".format(bcolors.UNDERLINE), bcolors.GREEN)
printColored("> Custom branch by the osufx team (just Sunpy)", bcolors.GREEN)
printColored("> {}https://github.com/osufx/pep.py".format(bcolors.UNDERLINE), bcolors.GREEN)
printColored("> Press CTRL+C to exit\n", bcolors.GREEN) printColored("> Press CTRL+C to exit\n", bcolors.GREEN)
def printNoNl(string): def printNoNl(string):

View File

@ -30,7 +30,7 @@ def getLocation(ip):
try: try:
# Try to get position from Pikolo Aul's Go-Sanic ip API # Try to get position from Pikolo Aul's Go-Sanic ip API
result = json.loads(urllib.request.urlopen("{}/{}".format(glob.conf.config["localize"]["ipapiurl"], ip), timeout=3).read().decode())["loc"].split(",") result = json.loads(urllib.request.urlopen("{}/{}".format(glob.conf.config["localize"]["ipapiurl"], ip), timeout=3).read().decode())["loc"].split(",")
return float(result[0]), float(result[1]) return (float(result[0]), float(result[1]))
except: except:
log.error("Error in get position") log.error("Error in get position")
return 0, 0 return (0, 0)

View File

@ -136,7 +136,7 @@ cpdef bytes packData(__data, int dataType):
return data return data
cpdef bytes buildPacket(int __packet, list __packetData = None): cpdef bytes buildPacket(int __packet, list __packetData = []):
""" """
Builds a packet Builds a packet
@ -144,9 +144,6 @@ cpdef bytes buildPacket(int __packet, list __packetData = None):
:param __packetData: packet structure [[data, dataType], [data, dataType], ...] :param __packetData: packet structure [[data, dataType], [data, dataType], ...]
:return: packet bytes :return: packet bytes
""" """
# Default argument
if __packetData is None:
__packetData = []
# Set some variables # Set some variables
cdef bytes packetData = bytes() cdef bytes packetData = bytes()
cdef int packetLength = 0 cdef int packetLength = 0
@ -186,7 +183,7 @@ cpdef int readPacketLength(bytes stream):
return unpackData(stream[3:7], dataTypes.UINT32) return unpackData(stream[3:7], dataTypes.UINT32)
cpdef readPacketData(bytes stream, list structure=None, bint hasFirstBytes = True): cpdef readPacketData(bytes stream, list structure=[], bint hasFirstBytes = True):
""" """
Read packet data from `stream` according to `structure` Read packet data from `stream` according to `structure`
:param stream: packet bytes :param stream: packet bytes
@ -195,10 +192,6 @@ cpdef readPacketData(bytes stream, list structure=None, bint hasFirstBytes = Tru
if False, `stream` has only packet data. Default: True if False, `stream` has only packet data. Default: True
:return: {name: unpackedValue, ...} :return: {name: unpackedValue, ...}
""" """
# Default list argument
if structure is None:
structure = []
# Read packet ID (first 2 bytes) # Read packet ID (first 2 bytes)
cdef dict data = {} cdef dict data = {}

View File

@ -43,7 +43,7 @@ def scheduleShutdown(sendRestartTime, restart, message = "", delay=20):
:return: :return:
""" """
# Console output # Console output
log.info("Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+delay), "bunker") log.info("Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+delay))
log.info("Sending server restart packets in {} seconds...".format(sendRestartTime)) log.info("Sending server restart packets in {} seconds...".format(sendRestartTime))
# Send notification if set # Send notification if set

View File

@ -17,7 +17,6 @@ import traceback
import raven import raven
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils
from helpers import chatHelper as chat from helpers import chatHelper as chat
from objects import glob from objects import glob
@ -45,7 +44,6 @@ class Client:
self.IRCUsername = "" self.IRCUsername = ""
self.banchoUsername = "" self.banchoUsername = ""
self.supposedUsername = "" self.supposedUsername = ""
self.supposedUserID = 0
self.joinedChannels = [] self.joinedChannels = []
def messageChannel(self, channel, command, message, includeSelf=False): def messageChannel(self, channel, command, message, includeSelf=False):
@ -138,7 +136,7 @@ class Client:
self.server.removeClient(self, quitmsg) self.server.removeClient(self, quitmsg)
# Bancho logout # Bancho logout
if callLogout and self.banchoUsername != "": if callLogout:
chat.IRCDisconnect(self.IRCUsername) chat.IRCDisconnect(self.IRCUsername)
@ -282,10 +280,9 @@ class Client:
m = hashlib.md5() m = hashlib.md5()
m.update(arguments[0].encode("utf-8")) m.update(arguments[0].encode("utf-8"))
tokenHash = m.hexdigest() tokenHash = m.hexdigest()
supposedUser = glob.db.fetch("SELECT users.username, users.id FROM users LEFT JOIN irc_tokens ON users.id = irc_tokens.userid WHERE irc_tokens.token = %s LIMIT 1", [tokenHash]) supposedUsername = glob.db.fetch("SELECT users.username FROM users LEFT JOIN irc_tokens ON users.id = irc_tokens.userid WHERE irc_tokens.token = %s LIMIT 1", [tokenHash])
if supposedUser: if supposedUsername:
self.supposedUsername = chat.fixUsernameForIRC(supposedUser["username"]) self.supposedUsername = chat.fixUsernameForIRC(supposedUsername["username"])
self.supposedUserID = supposedUser["id"]
self.__handleCommand = self.registerHandler self.__handleCommand = self.registerHandler
else: else:
# Wrong IRC Token # Wrong IRC Token
@ -313,11 +310,6 @@ class Client:
self.reply("464 :Password incorrect") self.reply("464 :Password incorrect")
return return
# Make sure that the user is not banned/restricted:
if not userUtils.isAllowed(self.supposedUserID):
self.reply("465 :You're banned")
return
# Make sure we are not connected to Bancho # Make sure we are not connected to Bancho
token = glob.tokens.getTokenFromUsername(chat.fixUsernameForBancho(nick), True) token = glob.tokens.getTokenFromUsername(chat.fixUsernameForBancho(nick), True)
if token is not None: if token is not None:
@ -358,11 +350,11 @@ class Client:
self.sendMotd() self.sendMotd()
self.__handleCommand = self.mainHandler self.__handleCommand = self.mainHandler
def quitHandler(self, _, arguments): def quitHandler(self, command, arguments):
"""QUIT command handler""" """QUIT command handler"""
self.disconnect(self.IRCUsername if len(arguments) < 1 else arguments[0]) self.disconnect(self.IRCUsername if len(arguments) < 1 else arguments[0])
def joinHandler(self, _, arguments): def joinHandler(self, command, arguments):
"""JOIN command handler""" """JOIN command handler"""
if len(arguments) < 1: if len(arguments) < 1:
self.reply461("JOIN") self.reply461("JOIN")
@ -410,15 +402,13 @@ class Client:
self.replyCode(332, description, channel=channel) self.replyCode(332, description, channel=channel)
# Build connected users list # Build connected users list
if "chat/{}".format(channel) not in glob.streams.streams: users = glob.channels.channels[channel].connectedUsers[:]
self.reply403(channel)
continue
users = glob.streams.streams["chat/{}".format(channel)].clients
usernames = [] usernames = []
for user in users: for user in users:
if user not in glob.tokens.tokens: token = glob.tokens.getTokenFromUserID(user)
if token is None:
continue continue
usernames.append(chat.fixUsernameForIRC(glob.tokens.tokens[user].username)) usernames.append(chat.fixUsernameForIRC(token.username))
usernames = " ".join(usernames) usernames = " ".join(usernames)
# Send IRC users list # Send IRC users list
@ -429,7 +419,7 @@ class Client:
self.reply403(channel) self.reply403(channel)
continue continue
def partHandler(self, _, arguments): def partHandler(self, command, arguments):
"""PART command handler""" """PART command handler"""
if len(arguments) < 1: if len(arguments) < 1:
self.reply461("PART") self.reply461("PART")
@ -513,7 +503,7 @@ class Client:
"""LUSERS command handler""" """LUSERS command handler"""
self.sendLusers() self.sendLusers()
def pingHandler(self, _, arguments): def pingHandler(self, command, arguments):
"""PING command handler""" """PING command handler"""
if len(arguments) < 1: if len(arguments) < 1:
self.replyCode(409, "No origin specified") self.replyCode(409, "No origin specified")
@ -524,7 +514,7 @@ class Client:
"""(fake) PONG command handler""" """(fake) PONG command handler"""
pass pass
def awayHandler(self, _, arguments): def awayHandler(self, command, arguments):
"""AWAY command handler""" """AWAY command handler"""
response = chat.IRCAway(self.banchoUsername, " ".join(arguments)) response = chat.IRCAway(self.banchoUsername, " ".join(arguments))
self.replyCode(response, "You are no longer marked as being away" if response == 305 else "You have been marked as being away") self.replyCode(response, "You are no longer marked as being away" if response == 305 else "You have been marked as being away")
@ -629,11 +619,12 @@ class Server:
value.message(":{} PRIVMSG {} :{}".format(fro, to, message)) value.message(":{} PRIVMSG {} :{}".format(fro, to, message))
def removeClient(self, client, _): def removeClient(self, client, quitmsg):
""" """
Remove a client from connected clients Remove a client from connected clients
:param client: client object :param client: client object
:param quitmsg: QUIT argument, useless atm
:return: :return:
""" """
if client.socket in self.clients: if client.socket in self.clients:
@ -646,9 +637,8 @@ class Server:
:return: :return:
""" """
# Sentry # Sentry
sentryClient = None
if glob.sentry: if glob.sentry:
sentryClient = raven.Client(glob.conf.config["sentry"]["ircdsn"]) sentryClient = raven.Client(glob.conf.config["sentry"]["ircdns"])
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@ -679,7 +669,7 @@ class Server:
try: try:
self.clients[conn] = Client(self, conn) self.clients[conn] = Client(self, conn)
log.info("[IRC] Accepted connection from {}:{}".format(addr[0], addr[1])) log.info("[IRC] Accepted connection from {}:{}".format(addr[0], addr[1]))
except socket.error: except socket.error as e:
try: try:
conn.close() conn.close()
except: except:
@ -698,7 +688,7 @@ class Server:
lastAliveCheck = now lastAliveCheck = now
except: except:
log.error("[IRC] Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc())) log.error("[IRC] Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc()))
if glob.sentry and sentryClient is not None: if glob.sentry:
sentryClient.captureException() sentryClient.captureException()
def main(port=6667): def main(port=6667):

View File

@ -2,7 +2,6 @@
from common import generalUtils from common import generalUtils
from constants import serverPackets from constants import serverPackets
from objects import glob from objects import glob
from common.log import logUtils as log
class banchoConfig: class banchoConfig:
@ -31,12 +30,7 @@ class banchoConfig:
""" """
self.config["banchoMaintenance"] = generalUtils.stringToBool(glob.db.fetch("SELECT value_int FROM bancho_settings WHERE name = 'bancho_maintenance'")["value_int"]) self.config["banchoMaintenance"] = generalUtils.stringToBool(glob.db.fetch("SELECT value_int FROM bancho_settings WHERE name = 'bancho_maintenance'")["value_int"])
self.config["freeDirect"] = generalUtils.stringToBool(glob.db.fetch("SELECT value_int FROM bancho_settings WHERE name = 'free_direct'")["value_int"]) self.config["freeDirect"] = generalUtils.stringToBool(glob.db.fetch("SELECT value_int FROM bancho_settings WHERE name = 'free_direct'")["value_int"])
mainMenuIcon = glob.db.fetch("SELECT file_id, url FROM main_menu_icons WHERE is_current = 1 LIMIT 1") self.config["menuIcon"] = glob.db.fetch("SELECT value_string FROM bancho_settings WHERE name = 'menu_icon'")["value_string"]
if mainMenuIcon is None:
self.config["menuIcon"] = ""
else:
imageURL = "https://i.ppy.sh/{}.png".format(mainMenuIcon["file_id"])
self.config["menuIcon"] = "{}|{}".format(imageURL, mainMenuIcon["url"])
self.config["loginNotification"] = glob.db.fetch("SELECT value_string FROM bancho_settings WHERE name = 'login_notification'")["value_string"] self.config["loginNotification"] = glob.db.fetch("SELECT value_string FROM bancho_settings WHERE name = 'login_notification'")["value_string"]
@ -63,5 +57,5 @@ class banchoConfig:
glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
glob.streams.broadcast("main", serverPackets.channelInfoEnd()) glob.streams.broadcast("main", serverPackets.channelInfoEnd())
for key, value in glob.channels.channels.items(): for key, value in glob.channels.channels.items():
if value.publicRead and not value.hidden: if value.publicRead == True and value.hidden == False:
glob.streams.broadcast("main", serverPackets.channelInfo(key)) glob.streams.broadcast("main", serverPackets.channelInfo(key))

View File

@ -1,6 +1,3 @@
import logging
from constants import exceptions
from objects import glob from objects import glob
class channel: class channel:
@ -21,24 +18,37 @@ class channel:
self.publicWrite = publicWrite self.publicWrite = publicWrite
self.moderated = False self.moderated = False
self.temp = temp self.temp = temp
self.connectedUsers = [999] # Fokabot is always connected to every channels (otherwise it doesn't show up in IRC users list)
self.hidden = hidden self.hidden = hidden
# Make Foka join the channel # Client name (#spectator/#multiplayer)
fokaToken = glob.tokens.getTokenFromUserID(999) self.clientName = self.name
if fokaToken is not None:
try:
fokaToken.joinChannel(self)
except exceptions.userAlreadyInChannelException:
logging.warning("FokaBot has already joined channel {}".format(self.name))
@property
def isSpecial(self):
return any(self.name.startswith(x) for x in ("#spect_", "#multi_"))
@property
def clientName(self):
if self.name.startswith("#spect_"): if self.name.startswith("#spect_"):
return "#spectator" self.clientName = "#spectator"
elif self.name.startswith("#multi_"): elif self.name.startswith("#multi_"):
return "#multiplayer" self.clientName = "#multiplayer"
return self.name
def userJoin(self, userID):
"""
Add a user to connected users
:param userID:
:return:
"""
if userID not in self.connectedUsers:
self.connectedUsers.append(userID)
def userPart(self, userID):
"""
Remove a user from connected users
:param userID:
:return:
"""
if userID in self.connectedUsers:
self.connectedUsers.remove(userID)
# Remove temp channels if empty or there's only fokabot connected
l = len(self.connectedUsers)
if self.temp == True and ((l == 0) or (l == 1 and 999 in self.connectedUsers)):
glob.channels.removeChannel(self.name)

View File

@ -1,7 +1,6 @@
from common.log import logUtils as log from common.log import logUtils as log
from objects import channel from objects import channel
from objects import glob from objects import glob
from helpers import chatHelper as chat
class channelList: class channelList:
@ -35,7 +34,6 @@ class channelList:
:param hidden: if True, thic channel won't be shown in channels list :param hidden: if True, thic channel won't be shown in channels list
:return: :return:
""" """
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, description, publicRead, publicWrite, temp, hidden) self.channels[name] = channel.channel(name, description, publicRead, publicWrite, temp, hidden)
log.info("Created channel {}".format(name)) log.info("Created channel {}".format(name))
@ -49,24 +47,9 @@ class channelList:
""" """
if name in self.channels: if name in self.channels:
return False return False
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, "Chat", True, True, True, True) self.channels[name] = channel.channel(name, "Chat", True, True, True, True)
log.info("Created temp channel {}".format(name)) log.info("Created temp channel {}".format(name))
def addHiddenChannel(self, name):
"""
Add a hidden channel. It's like a normal channel and must be deleted manually,
but it's not shown in channels list.
:param name: channel name
:return: True if the channel was created, otherwise False
"""
if name in self.channels:
return False
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, "Chat", True, True, False, True)
log.info("Created hidden channel {}".format(name))
def removeChannel(self, name): def removeChannel(self, name):
""" """
Removes a channel from channels list Removes a channel from channels list
@ -77,13 +60,5 @@ class channelList:
if name not in self.channels: if name not in self.channels:
log.debug("{} is not in channels list".format(name)) log.debug("{} is not in channels list".format(name))
return return
#glob.streams.broadcast("chat/{}".format(name), serverPackets.channelKicked(name))
stream = glob.streams.getStream("chat/{}".format(name))
if stream is not None:
for token in stream.clients:
if token in glob.tokens.tokens:
chat.partChannel(channel=name, token=glob.tokens.tokens[token], kick=True)
glob.streams.dispose("chat/{}".format(name))
glob.streams.remove("chat/{}".format(name))
self.channels.pop(name) self.channels.pop(name)
log.info("Removed channel {}".format(name)) log.info("Removed channel {}".format(name))

View File

@ -36,8 +36,6 @@ class chatFilters:
:param message: normal message :param message: normal message
:return: filtered message :return: filtered message
""" """
return message
"""
# Split words by spaces # Split words by spaces
messageTemp = message.split(" ") messageTemp = message.split(" ")
@ -51,4 +49,3 @@ class chatFilters:
# Return filtered message # Return filtered message
return message return message
"""

View File

@ -17,7 +17,6 @@ def connect():
:return: :return:
""" """
glob.BOT_NAME = userUtils.getUsername(999)
token = glob.tokens.addToken(999) token = glob.tokens.addToken(999)
token.actionID = actions.IDLE token.actionID = actions.IDLE
glob.streams.broadcast("main", serverPackets.userPanel(999)) glob.streams.broadcast("main", serverPackets.userPanel(999))
@ -42,7 +41,7 @@ def fokabotResponse(fro, chan, message):
""" """
for i in fokabotCommands.commands: for i in fokabotCommands.commands:
# Loop though all commands # Loop though all commands
if re.compile("^{}( (.+)?)?$".format(i["trigger"])).match(message.strip()): if generalUtils.strContains(message, i["trigger"]):
# message has triggered a command # message has triggered a command
# Make sure the user has right permissions # Make sure the user has right permissions

View File

@ -11,14 +11,13 @@ from common.web import schiavo
try: try:
with open("version") as f: with open("version") as f:
VERSION = f.read().strip() VERSION = f.read()
if VERSION == "": if VERSION == "":
raise Exception raise Exception
except: except:
VERSION = "Unknown" VERSION = "Unknown"
DATADOG_PREFIX = "peppy" DATADOG_PREFIX = "peppy"
BOT_NAME = "FokaBot"
application = None application = None
db = None db = None
redis = None redis = None
@ -49,11 +48,3 @@ restarting = False
startTime = int(time.time()) startTime = int(time.time())
streams = streamList.streamList() streams = streamList.streamList()
# Additional modifications
COMMON_VERSION_REQ = "1.2.1"
try:
with open("common/version") as f:
COMMON_VERSION = f.read().strip()
except:
COMMON_VERSION = "Unknown"

View File

@ -1,9 +1,4 @@
import copy import copy
import json
import threading
import time
from common.log import logUtils as log from common.log import logUtils as log
from constants import dataTypes from constants import dataTypes
from constants import matchModModes from constants import matchModModes
@ -19,19 +14,16 @@ from objects import glob
class slot: class slot:
def __init__(self): def __init__(self):
self.status = slotStatuses.FREE self.status = slotStatuses.FREE
self.team = matchTeams.NO_TEAM self.team = 0
self.userID = -1 self.userID = -1
self.user = None self.user = None
self.mods = 0 self.mods = 0
self.loaded = False self.loaded = False
self.skip = False self.skip = False
self.complete = False self.complete = False
self.score = 0
self.failed = False
self.passed = True
class match: class match:
def __init__(self, matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID, isTourney=False): def __init__(self, matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID):
""" """
Create a new match object Create a new match object
@ -61,13 +53,6 @@ class match:
self.matchModMode = matchModModes.NORMAL # default value self.matchModMode = matchModModes.NORMAL # default value
self.seed = 0 self.seed = 0
self.matchDataCache = bytes() self.matchDataCache = bytes()
self.isTourney = isTourney
self.isLocked = False # if True, users can't change slots/teams. Used in tourney matches
self.isStarting = False
self._lock = threading.Lock()
self.createTime = int(time.time())
self.vinseID = None
self.bloodcatAlert = False
# Create all slots and reset them # Create all slots and reset them
self.slots = [] self.slots = []
@ -79,67 +64,59 @@ class match:
glob.streams.add(self.playingStreamName) glob.streams.add(self.playingStreamName)
# Create #multiplayer channel # Create #multiplayer channel
glob.channels.addHiddenChannel("#multi_{}".format(self.matchID)) glob.channels.addTempChannel("#multi_{}".format(self.matchID))
log.info("MPROOM{}: {} match created!".format(self.matchID, "Tourney" if self.isTourney else "Normal"))
def getMatchData(self, censored = False): def getMatchData(self):
""" """
Return binary match data structure for packetHelper Return binary match data structure for packetHelper
Return binary match data structure for packetHelper
:return: :return:
""" """
# General match info # General match info
# TODO: Test without safe copy, the error might have been caused by outdated python bytecode cache # TODO: Test without safe copy, the error might have been caused by outdated python bytecode cache
# safeMatch = copy.deepcopy(self) safeMatch = copy.deepcopy(self)
struct = [ struct = [
[self.matchID, dataTypes.UINT16], [safeMatch.matchID, dataTypes.UINT16],
[int(self.inProgress), dataTypes.BYTE], [int(safeMatch.inProgress), dataTypes.BYTE],
[0, dataTypes.BYTE], [0, dataTypes.BYTE],
[self.mods, dataTypes.UINT32], [safeMatch.mods, dataTypes.UINT32],
[self.matchName, dataTypes.STRING] [safeMatch.matchName, dataTypes.STRING],
[safeMatch.matchPassword, dataTypes.STRING],
[safeMatch.beatmapName, dataTypes.STRING],
[safeMatch.beatmapID, dataTypes.UINT32],
[safeMatch.beatmapMD5, dataTypes.STRING],
] ]
if censored and self.matchPassword:
struct.append(["redacted", dataTypes.STRING])
else:
struct.append([self.matchPassword, dataTypes.STRING])
struct.extend([
[self.beatmapName, dataTypes.STRING],
[self.beatmapID, dataTypes.UINT32],
[self.beatmapMD5, dataTypes.STRING]
])
# Slots status IDs, always 16 elements # Slots status IDs, always 16 elements
for i in range(0,16): for i in range(0,16):
struct.append([self.slots[i].status, dataTypes.BYTE]) struct.append([safeMatch.slots[i].status, dataTypes.BYTE])
# Slot teams, always 16 elements # Slot teams, always 16 elements
for i in range(0,16): for i in range(0,16):
struct.append([self.slots[i].team, dataTypes.BYTE]) struct.append([safeMatch.slots[i].team, dataTypes.BYTE])
# Slot user ID. Write only if slot is occupied # Slot user ID. Write only if slot is occupied
for i in range(0,16): for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].user in glob.tokens.tokens: if safeMatch.slots[i].user is not None and safeMatch.slots[i].user in glob.tokens.tokens:
struct.append([glob.tokens.tokens[self.slots[i].user].userID, dataTypes.UINT32]) struct.append([glob.tokens.tokens[safeMatch.slots[i].user].userID, dataTypes.UINT32])
# Other match data # Other match data
struct.extend([ struct.extend([
[self.hostUserID, dataTypes.SINT32], [safeMatch.hostUserID, dataTypes.SINT32],
[self.gameMode, dataTypes.BYTE], [safeMatch.gameMode, dataTypes.BYTE],
[self.matchScoringType, dataTypes.BYTE], [safeMatch.matchScoringType, dataTypes.BYTE],
[self.matchTeamType, dataTypes.BYTE], [safeMatch.matchTeamType, dataTypes.BYTE],
[self.matchModMode, dataTypes.BYTE], [safeMatch.matchModMode, dataTypes.BYTE],
]) ])
# Slot mods if free mod is enabled # Slot mods if free mod is enabled
if self.matchModMode == matchModModes.FREE_MOD: if safeMatch.matchModMode == matchModModes.FREE_MOD:
for i in range(0,16): for i in range(0,16):
struct.append([self.slots[i].mods, dataTypes.UINT32]) struct.append([safeMatch.slots[i].mods, dataTypes.UINT32])
# Seed idk # Seed idk
# TODO: Implement this, it should be used for mania "random" mod # TODO: Implement this, it should be used for mania "random" mod
struct.append([self.seed, dataTypes.UINT32]) struct.append([safeMatch.seed, dataTypes.UINT32])
return struct return struct
@ -152,22 +129,12 @@ class match:
""" """
slotID = self.getUserSlotID(newHost) slotID = self.getUserSlotID(newHost)
if slotID is None or self.slots[slotID].user not in glob.tokens.tokens: if slotID is None or self.slots[slotID].user not in glob.tokens.tokens:
return False return
token = glob.tokens.tokens[self.slots[slotID].user] token = glob.tokens.tokens[self.slots[slotID].user]
self.hostUserID = newHost self.hostUserID = newHost
token.enqueue(serverPackets.matchTransferHost()) token.enqueue(serverPackets.matchTransferHost())
self.sendUpdates() self.sendUpdates()
log.info("MPROOM{}: {} is now the host".format(self.matchID, token.username)) log.info("MPROOM{}: {} is now the host".format(self.matchID, token.username))
return True
def removeHost(self):
"""
Removes the host (for tourney matches)
:return:
"""
self.hostUserID = -1
self.sendUpdates()
log.info("MPROOM{}: Removed host".format(self.matchID))
def setSlot(self, slotID, status = None, team = None, user = "", mods = None, loaded = None, skip = None, complete = None): def setSlot(self, slotID, status = None, team = None, user = "", mods = None, loaded = None, skip = None, complete = None):
""" """
@ -228,8 +195,6 @@ class match:
:return: :return:
""" """
# Update ready status and setnd update # Update ready status and setnd update
if self.slots[slotID].user is None or self.isStarting:
return
oldStatus = self.slots[slotID].status oldStatus = self.slots[slotID].status
if oldStatus == slotStatuses.READY: if oldStatus == slotStatuses.READY:
newStatus = slotStatuses.NOT_READY newStatus = slotStatuses.NOT_READY
@ -239,7 +204,7 @@ class match:
self.sendUpdates() self.sendUpdates()
log.info("MPROOM{}: Slot{} changed ready status to {}".format(self.matchID, slotID, self.slots[slotID].status)) log.info("MPROOM{}: Slot{} changed ready status to {}".format(self.matchID, slotID, self.slots[slotID].status))
def toggleSlotLocked(self, slotID): def toggleSlotLock(self, slotID):
""" """
Lock a slot Lock a slot
Same as calling setSlot and then sendUpdate Same as calling setSlot and then sendUpdate
@ -253,7 +218,7 @@ class match:
else: else:
newStatus = slotStatuses.LOCKED newStatus = slotStatuses.LOCKED
# Send updated settings to kicked user, so they will return to the lobby. # Send updated settings to kicked user, so he returns to lobby
if self.slots[slotID].user is not None and self.slots[slotID].user in glob.tokens.tokens: if self.slots[slotID].user is not None and self.slots[slotID].user in glob.tokens.tokens:
glob.tokens.tokens[self.slots[slotID].user].enqueue(serverPackets.updateMatch(self.matchID)) glob.tokens.tokens[self.slots[slotID].user].enqueue(serverPackets.updateMatch(self.matchID))
@ -340,26 +305,6 @@ class match:
glob.streams.broadcast(self.playingStreamName, serverPackets.allPlayersSkipped()) glob.streams.broadcast(self.playingStreamName, serverPackets.allPlayersSkipped())
log.info("MPROOM{}: All players have skipped!".format(self.matchID)) log.info("MPROOM{}: All players have skipped!".format(self.matchID))
def updateScore(self, slotID, score):
"""
Update score for a slot
:param slotID: the slot that the user that is updating their score is in
:param score: the new score to update
:return:
"""
self.slots[slotID].score = score
def updateHP(self, slotID, hp):
"""
Update HP for a slot
:param slotID: the slot that the user that is updating their hp is in
:param hp: the new hp to update
:return:
"""
self.slots[slotID].failed = True if hp == 254 else False
def playerCompleted(self, userID): def playerCompleted(self, userID):
""" """
Set userID's slot completed to True Set userID's slot completed to True
@ -392,35 +337,16 @@ class match:
:return: :return:
""" """
# Collect some info about the match that just ended to send to the api
infoToSend = {
"id": self.matchID,
"name": self.matchName,
"beatmap_id": self.beatmapID,
"mods": self.mods,
"game_mode": self.gameMode,
"scores": {}
}
# Add score info for each player
for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
infoToSend["scores"][glob.tokens.tokens[self.slots[i].user].userID] = {
"score": self.slots[i].score,
"mods": self.slots[i].mods,
"failed": self.slots[i].failed,
"pass": self.slots[i].passed,
"team": self.slots[i].team
}
# Send the info to the api
glob.redis.publish("api:mp_complete_match", json.dumps(infoToSend))
# Reset inProgress # Reset inProgress
self.inProgress = False self.inProgress = False
# Reset slots # Reset slots
self.resetSlots() for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
self.slots[i].status = slotStatuses.NOT_READY
self.slots[i].loaded = False
self.slots[i].skip = False
self.slots[i].complete = False
# Send match update # Send match update
self.sendUpdates() self.sendUpdates()
@ -429,46 +355,11 @@ class match:
glob.streams.broadcast(self.streamName, serverPackets.matchComplete()) glob.streams.broadcast(self.streamName, serverPackets.matchComplete())
# Destroy playing stream # Destroy playing stream
glob.streams.dispose(self.playingStreamName)
glob.streams.remove(self.playingStreamName) glob.streams.remove(self.playingStreamName)
# Console output # Console output
log.info("MPROOM{}: Match completed".format(self.matchID)) log.info("MPROOM{}: Match completed".format(self.matchID))
# Set vinse id if needed
chanName = "#multi_{}".format(self.matchID)
if self.vinseID is None:
self.vinseID = (int(time.time()) // (60 * 15)) << 32 | self.matchID
chat.sendMessage("FokaBot", chanName, "Match history available [{} here]".format(
"https://vinse.ripple.moe/match/{}".format(self.vinseID)
))
if not self.bloodcatAlert:
chat.sendMessage(
"FokaBot",
chanName,
"Oh by the way, in case you're playing unranked or broken maps "
"that are now available through ripple's osu!direct, you can "
"type '!bloodcat' in the chat to get a download link for the "
"currently selected map from Bloodcat!"
)
self.bloodcatAlert = True
# If this is a tournament match, then we send a notification in the chat
# saying that the match has completed.
if self.isTourney and (chanName in glob.channels.channels):
chat.sendMessage(glob.BOT_NAME, chanName, "Match has just finished.")
def resetSlots(self):
for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
self.slots[i].status = slotStatuses.NOT_READY
self.slots[i].loaded = False
self.slots[i].skip = False
self.slots[i].complete = False
self.slots[i].score = 0
self.slots[i].failed = False
self.slots[i].passed = True
def getUserSlotID(self, userID): def getUserSlotID(self, userID):
""" """
Get slot ID occupied by userID Get slot ID occupied by userID
@ -484,7 +375,7 @@ class match:
""" """
Add someone to users in match Add someone to users in match
:param user: user object of the user :param userID: user id of the user
:return: True if join success, False if fail (room is full) :return: True if join success, False if fail (room is full)
""" """
# Make sure we're not in this match # Make sure we're not in this match
@ -497,10 +388,7 @@ class match:
for i in range(0,16): for i in range(0,16):
if self.slots[i].status == slotStatuses.FREE: if self.slots[i].status == slotStatuses.FREE:
# Occupy slot # Occupy slot
team = matchTeams.NO_TEAM self.setSlot(i, slotStatuses.NOT_READY, 0, user.token, 0)
if self.matchTeamType == matchTeamTypes.TEAM_VS or self.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
team = matchTeams.RED if i % 2 == 0 else matchTeams.BLUE
self.setSlot(i, slotStatuses.NOT_READY, team, user.token, 0)
# Send updated match data # Send updated match data
self.sendUpdates() self.sendUpdates()
@ -511,12 +399,11 @@ class match:
return False return False
def userLeft(self, user, disposeMatch=True): def userLeft(self, user):
""" """
Remove someone from users in match Remove someone from users in match
:param user: user object of the user :param userID: user if of the user
:param disposeMatch: if `True`, will try to dispose match if there are no users in the room
:return: :return:
""" """
# Make sure the user is in room # Make sure the user is in room
@ -528,10 +415,10 @@ class match:
self.setSlot(slotID, slotStatuses.FREE, 0, None, 0) self.setSlot(slotID, slotStatuses.FREE, 0, None, 0)
# Check if everyone left # Check if everyone left
if self.countUsers() == 0 and disposeMatch and not self.isTourney: if self.countUsers() == 0:
# Dispose match # Dispose match
glob.matches.disposeMatch(self.matchID) glob.matches.disposeMatch(self.matchID)
log.info("MPROOM{}: Room disposed because all users left".format(self.matchID)) log.info("MPROOM{}: Room disposed".format(self.matchID))
return return
# Check if host left # Check if host left
@ -556,18 +443,14 @@ class match:
:param newSlotID: slot id of new slot :param newSlotID: slot id of new slot
:return: :return:
""" """
# Make sure the match is not locked
if self.isLocked or self.isStarting:
return False
# Make sure the user is in room # Make sure the user is in room
oldSlotID = self.getUserSlotID(userID) oldSlotID = self.getUserSlotID(userID)
if oldSlotID is None: if oldSlotID is None:
return False return
# Make sure there is no one inside new slot # Make sure there is no one inside new slot
if self.slots[newSlotID].user is not None or self.slots[newSlotID].status != slotStatuses.FREE: if self.slots[newSlotID].user is not None and self.slots[newSlotID].status != slotStatuses.FREE:
return False return
# Get old slot data # Get old slot data
#oldData = dill.copy(self.slots[oldSlotID]) #oldData = dill.copy(self.slots[oldSlotID])
@ -584,7 +467,6 @@ class match:
# Console output # Console output
log.info("MPROOM{}: {} moved to slot {}".format(self.matchID, userID, newSlotID)) log.info("MPROOM{}: {} moved to slot {}".format(self.matchID, userID, newSlotID))
return True
def changePassword(self, newPassword): def changePassword(self, newPassword):
""" """
@ -650,7 +532,7 @@ class match:
self.setHost(glob.tokens.tokens[self.slots[slotID].user].userID) self.setHost(glob.tokens.tokens[self.slots[slotID].user].userID)
# Send updates # Send updates
# self.sendUpdates() self.sendUpdates()
def playerFailed(self, userID): def playerFailed(self, userID):
""" """
@ -664,8 +546,6 @@ class match:
if slotID is None: if slotID is None:
return return
self.slots[slotID].passed = False
# Send packet to everyone # Send packet to everyone
glob.streams.broadcast(self.playingStreamName, serverPackets.playerFailed(slotID)) glob.streams.broadcast(self.playingStreamName, serverPackets.playerFailed(slotID))
@ -688,7 +568,7 @@ class match:
# FokaBot is too busy # FokaBot is too busy
if to == 999: if to == 999:
chat.sendMessage(glob.BOT_NAME, froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.") chat.sendMessage("FokaBot", froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.")
# Send message # Send message
message = "Come join my multiplayer match: \"[osump://{}/{} {}]\"".format(self.matchID, self.matchPassword.replace(" ", "_"), self.matchName) message = "Come join my multiplayer match: \"[osump://{}/{} {}]\"".format(self.matchID, self.matchPassword.replace(" ", "_"), self.matchName)
@ -706,28 +586,19 @@ class match:
c+=1 c+=1
return c return c
def changeTeam(self, userID, newTeam=None): def changeTeam(self, userID):
""" """
Change userID's team Change userID's team
:param userID: id of user :param userID: id of user
:return: :return:
""" """
# Make sure this match's mode has teams
if self.matchTeamType != matchTeamTypes.TEAM_VS and self.matchTeamType != matchTeamTypes.TAG_TEAM_VS:
return
# Make sure the match is not locked
if self.isLocked or self.isStarting:
return
# Make sure the user is in room # Make sure the user is in room
slotID = self.getUserSlotID(userID) slotID = self.getUserSlotID(userID)
if slotID is None: if slotID is None:
return return
# Update slot and send update # Update slot and send update
if newTeam is None:
newTeam = matchTeams.BLUE if self.slots[slotID].team == matchTeams.RED else matchTeams.RED newTeam = matchTeams.BLUE if self.slots[slotID].team == matchTeams.RED else matchTeams.RED
self.setSlot(slotID, None, newTeam) self.setSlot(slotID, None, newTeam)
self.sendUpdates() self.sendUpdates()
@ -739,11 +610,9 @@ class match:
:return: :return:
""" """
self.matchDataCache = serverPackets.updateMatch(self.matchID) self.matchDataCache = serverPackets.updateMatch(self.matchID)
censoredDataCache = serverPackets.updateMatch(self.matchID, censored=True)
if self.matchDataCache is not None: if self.matchDataCache is not None:
glob.streams.broadcast(self.streamName, self.matchDataCache) glob.streams.broadcast(self.streamName, self.matchDataCache)
if censoredDataCache is not None: glob.streams.broadcast("lobby", self.matchDataCache)
glob.streams.broadcast("lobby", censoredDataCache)
else: else:
log.error("MPROOM{}: Can't send match update packet, match data is None!!!".format(self.matchID)) log.error("MPROOM{}: Can't send match update packet, match data is None!!!".format(self.matchID))
@ -754,7 +623,7 @@ class match:
:return: True if valid, False if invalid :return: True if valid, False if invalid
:return: :return:
""" """
if self.matchTeamType != matchTeamTypes.TEAM_VS and self.matchTeamType != matchTeamTypes.TAG_TEAM_VS: if self.matchTeamType != matchTeamTypes.TEAM_VS or self.matchTeamType != matchTeamTypes.TAG_TEAM_VS:
# Teams are always valid if we have no teams # Teams are always valid if we have no teams
return True return True
@ -777,23 +646,20 @@ class match:
:return: :return:
""" """
# Remove isStarting timer flag thingie
self.isStarting = False
# Make sure we have enough players # Make sure we have enough players
if self.countUsers() < 2 or not self.checkTeams(): if self.countUsers() < 2 or not self.checkTeams():
return False return
# Create playing channel # Create playing channel
glob.streams.add(self.playingStreamName) glob.streams.add(self.playingStreamName)
# Change inProgress value # Change inProgress value
self.inProgress = True match.inProgress = True
# Set playing to ready players and set load, skip and complete to False # Set playing to ready players and set load, skip and complete to False
# Make clients join playing stream # Make clients join playing stream
for i in range(0, 16): for i in range(0, 16):
if self.slots[i].user in glob.tokens.tokens: if (self.slots[i].status & slotStatuses.READY) > 0 and self.slots[i].user in glob.tokens.tokens:
self.slots[i].status = slotStatuses.PLAYING self.slots[i].status = slotStatuses.PLAYING
self.slots[i].loaded = False self.slots[i].loaded = False
self.slots[i].skip = False self.slots[i].skip = False
@ -805,85 +671,3 @@ class match:
# Send updates # Send updates
self.sendUpdates() self.sendUpdates()
return True
def forceSize(self, matchSize):
for i in range(0, matchSize):
if self.slots[i].status == slotStatuses.LOCKED:
self.toggleSlotLocked(i)
for i in range(matchSize, 16):
if self.slots[i].status != slotStatuses.LOCKED:
self.toggleSlotLocked(i)
def abort(self):
if not self.inProgress:
log.warning("MPROOM{}: Match is not in progress!".format(self.matchID))
return
self.inProgress = False
self.isStarting = False
self.resetSlots()
self.sendUpdates()
glob.streams.broadcast(self.playingStreamName, serverPackets.matchAbort())
glob.streams.dispose(self.playingStreamName)
glob.streams.remove(self.playingStreamName)
log.info("MPROOM{}: Match aborted".format(self.matchID))
def initializeTeams(self):
if self.matchTeamType == matchTeamTypes.TEAM_VS or self.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
# Set teams
for i, _slot in enumerate(self.slots):
_slot.team = matchTeams.RED if i % 2 == 0 else matchTeams.BLUE
else:
# Reset teams
for _slot in self.slots:
_slot.team = matchTeams.NO_TEAM
def resetMods(self):
for _slot in self.slots:
_slot.mods = 0
def resetReady(self):
for _slot in self.slots:
if _slot.status == slotStatuses.READY:
_slot.status = slotStatuses.NOT_READY
def sendReadyStatus(self):
chanName = "#multi_{}".format(self.matchID)
# Make sure match exists before attempting to do anything else
if chanName not in glob.channels.channels:
return
totalUsers = 0
readyUsers = 0
for slot in self.slots:
# Make sure there is a user in this slot
if slot.user is None:
continue
# In this slot there is a user, so we increase the amount of total users
# in this multi room.
totalUsers += 1
if slot.status == slotStatuses.READY:
readyUsers += 1
message = "{} users ready out of {}.".format(readyUsers, totalUsers)
if totalUsers == readyUsers:
message += " All users ready!"
# Check whether there is anyone left in this match.
if totalUsers == 0:
message = "The match is now empty."
chat.sendMessage(glob.BOT_NAME, chanName, message)
def __enter__(self):
# 🌚🌚🌚🌚🌚
self._lock.acquire()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._lock.release()

View File

@ -1,8 +1,3 @@
import threading
import time
from common.sentry import sentry
from constants.exceptions import periodicLoopException
from objects import match from objects import match
from objects import glob from objects import glob
from constants import serverPackets from constants import serverPackets
@ -14,7 +9,7 @@ class matchList:
self.matches = {} self.matches = {}
self.lastID = 1 self.lastID = 1
def createMatch(self, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID, isTourney=False): def createMatch(self, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID):
""" """
Add a new match to matches list Add a new match to matches list
@ -30,7 +25,7 @@ class matchList:
# Add a new match to matches list and create its stream # Add a new match to matches list and create its stream
matchID = self.lastID matchID = self.lastID
self.lastID+=1 self.lastID+=1
self.matches[matchID] = match.match(matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID, isTourney) self.matches[matchID] = match.match(matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID)
return matchID return matchID
def disposeMatch(self, matchID): def disposeMatch(self, matchID):
@ -44,69 +39,10 @@ class matchList:
if matchID not in self.matches: if matchID not in self.matches:
return return
# Get match and disconnect all players # Remove match object and stream
_match = self.matches[matchID] match = self.matches.pop(matchID)
for slot in _match.slots: glob.streams.remove(match.streamName)
_token = glob.tokens.getTokenFromUserID(slot.userID, ignoreIRC=True) glob.streams.remove(match.playingStreamName)
if _token is None:
continue
_match.userLeft(_token, disposeMatch=False) # don't dispose the match twice when we remove all players
# Delete chat channel
glob.channels.removeChannel("#multi_{}".format(_match.matchID))
# Send matchDisposed packet before disposing streams
glob.streams.broadcast(_match.streamName, serverPackets.disposeMatch(_match.matchID))
# Dispose all streams
glob.streams.dispose(_match.streamName)
glob.streams.dispose(_match.playingStreamName)
glob.streams.remove(_match.streamName)
glob.streams.remove(_match.playingStreamName)
# Send match dispose packet to everyone in lobby # Send match dispose packet to everyone in lobby
glob.streams.broadcast("lobby", serverPackets.disposeMatch(matchID)) glob.streams.broadcast("lobby", serverPackets.disposeMatch(matchID))
del self.matches[matchID]
log.info("MPROOM{}: Room disposed manually".format(_match.matchID))
@sentry.capture()
def cleanupLoop(self):
"""
Start match cleanup loop.
Empty matches that have been created more than 60 seconds ago will get deleted.
Useful when people create useless lobbies with `!mp make`.
The check is done every 30 seconds.
This method starts an infinite loop, call it only once!
:return:
"""
try:
log.debug("Checking empty matches")
t = int(time.time())
emptyMatches = []
exceptions = []
# Collect all empty matches
for key, m in self.matches.items():
if [x for x in m.slots if x.user is not None]:
continue
if t - m.createTime >= 120:
log.debug("Match #{} marked for cleanup".format(m.matchID))
emptyMatches.append(m.matchID)
# Dispose all empty matches
for matchID in emptyMatches:
try:
self.disposeMatch(matchID)
except Exception as e:
exceptions.append(e)
log.error(
"Something wrong happened while disposing a timed out match. Reporting to Sentry when "
"the loop ends."
)
# Re-raise exception if needed
if exceptions:
raise periodicLoopException(exceptions)
finally:
# Schedule a new check (endless loop)
threading.Timer(30, self.cleanupLoop).start()

View File

@ -5,7 +5,6 @@ import uuid
from common.constants import gameModes, actions from common.constants import gameModes, actions
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils from common.ripple import userUtils
from constants import exceptions
from constants import serverPackets from constants import serverPackets
from events import logoutEvent from events import logoutEvent
from helpers import chatHelper as chat from helpers import chatHelper as chat
@ -30,18 +29,16 @@ class token:
self.username = userUtils.getUsername(self.userID) self.username = userUtils.getUsername(self.userID)
self.safeUsername = userUtils.getSafeUsername(self.userID) self.safeUsername = userUtils.getSafeUsername(self.userID)
self.privileges = userUtils.getPrivileges(self.userID) self.privileges = userUtils.getPrivileges(self.userID)
self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer")\ self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer") or userUtils.isInPrivilegeGroup(self.userID, "community manager")
or userUtils.isInPrivilegeGroup(self.userID, "community manager")\
or userUtils.isInPrivilegeGroup(self.userID, "chat mod")
self.irc = irc self.irc = irc
self.kicked = False self.kicked = False
self.restricted = userUtils.isRestricted(self.userID) self.restricted = userUtils.isRestricted(self.userID)
self.loginTime = int(time.time()) self.loginTime = int(time.time())
self.pingTime = self.loginTime self.pingTime = self.loginTime
self.timeOffset = timeOffset self.timeOffset = timeOffset
self.lock = threading.Lock() # Sync primitive
self.streams = [] self.streams = []
self.tournament = tournament self.tournament = tournament
self.messagesBuffer = []
# Default variables # Default variables
self.spectators = [] self.spectators = []
@ -85,11 +82,6 @@ class token:
else: else:
self.token = str(uuid.uuid4()) self.token = str(uuid.uuid4())
# Locks
self.processingLock = threading.Lock() # Acquired while there's an incoming packet from this user
self._bufferLock = threading.Lock() # Acquired while writing to packets buffer
self._spectLock = threading.RLock()
# Set stats # Set stats
self.updateCachedStats() self.updateCachedStats()
@ -104,64 +96,41 @@ class token:
""" """
Add bytes (packets) to queue Add bytes (packets) to queue
:param bytes_: (packet) bytes to enqueue :param bytes: (packet) bytes to enqueue
""" """
try: # TODO: reduce max queue size
# Acquire the buffer lock
self._bufferLock.acquire()
# Never enqueue for IRC clients or Foka
if self.irc or self.userID < 999:
return
# Avoid memory leaks
if len(bytes_) < 10 * 10 ** 6: if len(bytes_) < 10 * 10 ** 6:
self.queue += bytes_ self.queue += bytes_
else: else:
log.warning("{}'s packets buffer is above 10M!! Lost some data!".format(self.username)) log.warning("{}'s packets buffer is above 10M!! Lost some data!".format(self.username))
finally:
# Release the buffer lock
self._bufferLock.release()
def resetQueue(self): def resetQueue(self):
"""Resets the queue. Call when enqueued packets have been sent""" """Resets the queue. Call when enqueued packets have been sent"""
try:
self._bufferLock.acquire()
self.queue = bytes() self.queue = bytes()
finally:
self._bufferLock.release()
def joinChannel(self, channelObject): def joinChannel(self, channel):
""" """
Join a channel Add channel to joined channels list
:param channelObject: channel object :param channel: channel name
:raises: exceptions.userAlreadyInChannelException()
exceptions.channelNoPermissionsException()
""" """
if channelObject.name in self.joinedChannels: if channel not in self.joinedChannels:
raise exceptions.userAlreadyInChannelException() self.joinedChannels.append(channel)
if not channelObject.publicRead and not self.admin:
raise exceptions.channelNoPermissionsException()
self.joinedChannels.append(channelObject.name)
self.joinStream("chat/{}".format(channelObject.name))
self.enqueue(serverPackets.channelJoinSuccess(self.userID, channelObject.clientName))
def partChannel(self, channelObject): def partChannel(self, channel):
""" """
Remove channel from joined channels list Remove channel from joined channels list
:param channelObject: channel object :param channel: channel name
""" """
self.joinedChannels.remove(channelObject.name) if channel in self.joinedChannels:
self.leaveStream("chat/{}".format(channelObject.name)) self.joinedChannels.remove(channel)
def setLocation(self, latitude, longitude): def setLocation(self, latitude, longitude):
""" """
Set client location Set client location
:param latitude: latitude :param location: [latitude, longitude]
:param longitude: longitude
""" """
self.location = (latitude, longitude) self.location = (latitude, longitude)
@ -188,9 +157,6 @@ class token:
:param host: host osuToken object :param host: host osuToken object
""" """
try:
self._spectLock.acquire()
# Stop spectating old client # Stop spectating old client
self.stopSpectating() self.stopSpectating()
@ -212,10 +178,10 @@ class token:
# Create and join #spectator (#spect_userid) channel # Create and join #spectator (#spect_userid) channel
glob.channels.addTempChannel("#spect_{}".format(host.userID)) glob.channels.addTempChannel("#spect_{}".format(host.userID))
chat.joinChannel(token=self, channel="#spect_{}".format(host.userID), force=True) chat.joinChannel(token=self, channel="#spect_{}".format(host.userID))
if len(host.spectators) == 1: if len(host.spectators) == 1:
# First spectator, send #spectator join to host too # First spectator, send #spectator join to host too
chat.joinChannel(token=host, channel="#spect_{}".format(host.userID), force=True) chat.joinChannel(token=host, channel="#spect_{}".format(host.userID))
# Send fellow spectator join to all clients # Send fellow spectator join to all clients
glob.streams.broadcast(streamName, serverPackets.fellowSpectatorJoined(self.userID)) glob.streams.broadcast(streamName, serverPackets.fellowSpectatorJoined(self.userID))
@ -227,8 +193,6 @@ class token:
# Log # Log
log.info("{} is spectating {}".format(self.username, host.username)) log.info("{} is spectating {}".format(self.username, host.username))
finally:
self._spectLock.release()
def stopSpectating(self): def stopSpectating(self):
""" """
@ -237,11 +201,8 @@ class token:
:return: :return:
""" """
try:
self._spectLock.acquire()
# Remove our userID from host's spectators # Remove our userID from host's spectators
if self.spectating is None or self.spectatingUserID <= 0: if self.spectating is None:
return return
if self.spectating in glob.tokens.tokens: if self.spectating in glob.tokens.tokens:
hostToken = glob.tokens.tokens[self.spectating] hostToken = glob.tokens.tokens[self.spectating]
@ -265,20 +226,18 @@ class token:
# If nobody is spectating the host anymore, close #spectator channel # If nobody is spectating the host anymore, close #spectator channel
# and remove host from spect stream too # and remove host from spect stream too
if len(hostToken.spectators) == 0: if len(hostToken.spectators) == 0:
chat.partChannel(token=hostToken, channel="#spect_{}".format(hostToken.userID), kick=True, force=True) chat.partChannel(token=hostToken, channel="#spect_{}".format(hostToken.userID), kick=True)
hostToken.leaveStream(streamName) hostToken.leaveStream(streamName)
# Console output
log.info("{} is no longer spectating {}. Current spectators: {}".format(self.username, self.spectatingUserID, hostToken.spectators))
# Part #spectator channel # Part #spectator channel
chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True, force=True) chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True)
# Console output
log.info("{} is no longer spectating {}".format(self.username, self.spectatingUserID))
# Set our spectating user to 0 # Set our spectating user to 0
self.spectating = None self.spectating = None
self.spectatingUserID = 0 self.spectatingUserID = 0
finally:
self._spectLock.release()
def updatePingTime(self): def updatePingTime(self):
""" """
@ -318,16 +277,9 @@ class token:
# Set matchID, join stream, channel and send packet # Set matchID, join stream, channel and send packet
self.matchID = matchID self.matchID = matchID
self.joinStream(match.streamName) self.joinStream(match.streamName)
chat.joinChannel(token=self, channel="#multi_{}".format(self.matchID), force=True) chat.joinChannel(token=self, channel="#multi_{}".format(self.matchID))
self.enqueue(serverPackets.matchJoinSuccess(matchID)) self.enqueue(serverPackets.matchJoinSuccess(matchID))
if match.isTourney:
# Alert the user if we have just joined a tourney match
self.enqueue(serverPackets.notification("You are now in a tournament match."))
# If an user joins, then the ready status of the match changes and
# maybe not all users are ready.
match.sendReadyStatus()
def leaveMatch(self): def leaveMatch(self):
""" """
Leave joined match, match stream and match channel Leave joined match, match stream and match channel
@ -339,28 +291,22 @@ class token:
return return
# Part #multiplayer channel and streams (/ and /playing) # Part #multiplayer channel and streams (/ and /playing)
chat.partChannel(token=self, channel="#multi_{}".format(self.matchID), kick=True, force=True) chat.partChannel(token=self, channel="#multi_{}".format(self.matchID), kick=True)
self.leaveStream("multi/{}".format(self.matchID)) self.leaveStream("multi/{}".format(self.matchID))
self.leaveStream("multi/{}/playing".format(self.matchID)) # optional self.leaveStream("multi/{}/playing".format(self.matchID)) # optional
# Set usertoken match to -1
leavingMatchID = self.matchID
self.matchID = -1
# Make sure the match exists # Make sure the match exists
if leavingMatchID not in glob.matches.matches: if self.matchID not in glob.matches.matches:
return return
# The match exists, get object # The match exists, get object
match = glob.matches.matches[leavingMatchID] match = glob.matches.matches[self.matchID]
# Set slot to free # Set slot to free
match.userLeft(self) match.userLeft(self)
if match.isTourney: # Set usertoken match to -1
# If an user leaves, then the ready status of the match changes and self.matchID = -1
# maybe all users are ready. Or maybe nobody is in the match anymore
match.sendReadyStatus()
def kick(self, message="You have been kicked from the server. Please login again.", reason="kick"): def kick(self, message="You have been kicked from the server. Please login again.", reason="kick"):
""" """
@ -378,7 +324,7 @@ class token:
self.enqueue(serverPackets.loginFailed()) self.enqueue(serverPackets.loginFailed())
# Logout event # Logout event
logoutEvent.handle(self, deleteToken=self.irc) logoutEvent.handle(self, deleteToken=False)
def silence(self, seconds = None, reason = "", author = 999): def silence(self, seconds = None, reason = "", author = 999):
""" """
@ -487,7 +433,7 @@ class token:
:return: :return:
""" """
self.restricted = True self.restricted = True
chat.sendMessage(glob.BOT_NAME, 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): def resetRestricted(self):
""" """
@ -496,7 +442,7 @@ class token:
:return: :return:
""" """
chat.sendMessage(glob.BOT_NAME, self.username, "Your account has been unrestricted! Please log in again.") chat.sendMessage("FokaBot", self.username, "Your account has been unrestricted! Please log in again.")
def joinStream(self, name): def joinStream(self, name):
""" """
@ -541,24 +487,3 @@ class token:
return False return False
self.sentAway.append(userID) self.sentAway.append(userID)
return True return True
def addMessageInBuffer(self, chan, message):
"""
Add a message in messages buffer (10 messages, truncated at 50 chars).
Used as proof when the user gets reported.
:param chan: channel
:param message: message content
:return:
"""
if len(self.messagesBuffer) > 9:
self.messagesBuffer = self.messagesBuffer[1:]
self.messagesBuffer.append("{time} - {user}@{channel}: {message}".format(time=time.strftime("%H:%M", time.localtime()), user=self.username, channel=chan, message=message[:50]))
def getMessagesBufferString(self):
"""
Get the content of the messages buffer as a string
:return: messages buffer content as a string
"""
return "\n".join(x for x in self.messagesBuffer)

View File

@ -43,29 +43,15 @@ class stream:
log.info("{} has left stream {}".format(token, self.name)) log.info("{} has left stream {}".format(token, self.name))
self.clients.remove(token) self.clients.remove(token)
def broadcast(self, data, but=None): def broadcast(self, data):
""" """
Send some data to all (or some) clients connected to this stream Send some data to all clients connected to this stream
:param data: data to send :param data: data to send
:param but: array of tokens to ignore. Default: None (send to everyone)
:return: :return:
""" """
if but is None:
but = []
for i in self.clients: for i in self.clients:
if i in glob.tokens.tokens: if i in glob.tokens.tokens:
if i not in but:
glob.tokens.tokens[i].enqueue(data) glob.tokens.tokens[i].enqueue(data)
else: else:
self.removeClient(token=i) self.removeClient(token=i)
def dispose(self):
"""
Tell every client in this stream to leave the stream
:return:
"""
for i in self.clients:
if i in glob.tokens.tokens:
glob.tokens.tokens[i].leaveStream(self.name)

View File

@ -1,7 +1,6 @@
from objects import stream from objects import stream
from objects import glob from objects import glob
# TODO: use *args and **kwargs
class streamList: class streamList:
def __init__(self): def __init__(self):
self.streams = {} self.streams = {}
@ -56,39 +55,25 @@ class streamList:
return return
self.streams[streamName].removeClient(client=client, token=token) self.streams[streamName].removeClient(client=client, token=token)
def broadcast(self, streamName, data, but=None): def broadcast(self, streamName, data):
""" """
Send some data to all clients in a stream Send some data to all clients in a stream
:param streamName: stream name :param streamName: stream name
:param data: data to send :param data: data to send
:param but: array of tokens to ignore. Default: None (send to everyone)
:return: :return:
""" """
if streamName not in self.streams: if streamName not in self.streams:
return return
self.streams[streamName].broadcast(data, but) self.streams[streamName].broadcast(data)
def dispose(self, streamName, *args, **kwargs): '''def getClients(self, streamName):
""" """
Call `dispose` on `streamName` Get all clients in a stream
:param streamName: name of the stream :param streamName: name of the stream
:param args:
:param kwargs:
:return: :return:
""" """
if streamName not in self.streams: if streamName not in self.streams:
return return
self.streams[streamName].dispose(*args, **kwargs) return self.streams[streamName].clients'''
def getStream(self, streamName):
"""
Returns streamName's stream object or None if it doesn't exist
:param streamName:
:return:
"""
if streamName in self.streams:
return self.streams[streamName]
return None

View File

@ -1,35 +1,22 @@
import threading import threading
import time import time
import redis
from common.ripple import userUtils from common.ripple import userUtils
from common.log import logUtils as log from common.log import logUtils as log
from common.sentry import sentry
from constants import serverPackets from constants import serverPackets
from constants.exceptions import periodicLoopException
from events import logoutEvent from events import logoutEvent
from objects import glob from objects import glob
from objects import osuToken from objects import osuToken
class tokenList: class tokenList:
def __init__(self): def __init__(self):
self.tokens = {} self.tokens = {}
self._lock = threading.Lock()
def __enter__(self):
self._lock.acquire()
def __exit__(self, exc_type, exc_val, exc_tb):
self._lock.release()
def addToken(self, userID, ip = "", irc = False, timeOffset=0, tournament=False): def addToken(self, userID, ip = "", irc = False, timeOffset=0, tournament=False):
""" """
Add a token object to tokens list Add a token object to tokens list
:param userID: user id associated to that token :param userID: user id associated to that token
:param ip: ip address of the client
:param irc: if True, set this token as IRC client :param irc: if True, set this token as IRC client
:param timeOffset: the time offset from UTC for this user. Default: 0. :param timeOffset: the time offset from UTC for this user. Default: 0.
:param tournament: if True, flag this client as a tournement client. Default: True. :param tournament: if True, flag this client as a tournement client. Default: True.
@ -50,8 +37,7 @@ class tokenList:
if token in self.tokens: if token in self.tokens:
if self.tokens[token].ip != "": if self.tokens[token].ip != "":
userUtils.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip) userUtils.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip)
t = self.tokens.pop(token) self.tokens.pop(token)
del t
glob.redis.decr("ripple:online_users") glob.redis.decr("ripple:online_users")
def getUserIDFromToken(self, token): def getUserIDFromToken(self, token):
@ -68,34 +54,25 @@ class tokenList:
# Get userID associated to that token # Get userID associated to that token
return self.tokens[token].userID return self.tokens[token].userID
def getTokenFromUserID(self, userID, ignoreIRC=False, _all=False): def getTokenFromUserID(self, userID, ignoreIRC=False):
""" """
Get token from a user ID Get token from a user ID
:param userID: user ID to find :param userID: user ID to find
:param ignoreIRC: if True, consider bancho clients only and skip IRC clients :param ignoreIRC: if True, consider bancho clients only and skip IRC clients
:param _all: if True, return a list with all clients that match given username, otherwise return
only the first occurrence.
:return: False if not found, token object if found :return: False if not found, token object if found
""" """
# Make sure the token exists # Make sure the token exists
ret = []
for _, value in self.tokens.items(): for _, value in self.tokens.items():
if value.userID == userID: if value.userID == userID:
if ignoreIRC and value.irc: if ignoreIRC and value.irc:
continue continue
if _all:
ret.append(value)
else:
return value return value
# Return full list or None if not found # Return none if not found
if _all:
return ret
else:
return None return None
def getTokenFromUsername(self, username, ignoreIRC=False, safe=False, _all=False): def getTokenFromUsername(self, username, ignoreIRC=False, safe=False):
""" """
Get an osuToken object from an username Get an osuToken object from an username
@ -103,28 +80,19 @@ class tokenList:
:param ignoreIRC: if True, consider bancho clients only and skip IRC clients :param ignoreIRC: if True, consider bancho clients only and skip IRC clients
:param safe: if True, username is a safe username, :param safe: if True, username is a safe username,
compare it with token's safe username rather than normal username compare it with token's safe username rather than normal username
:param _all: if True, return a list with all clients that match given username, otherwise return
only the first occurrence.
:return: osuToken object or None :return: osuToken object or None
""" """
# lowercase # lowercase
who = username.lower() if not safe else username who = username.lower() if not safe else username
# Make sure the token exists # Make sure the token exists
ret = []
for _, value in self.tokens.items(): for _, value in self.tokens.items():
if (not safe and value.username.lower() == who) or (safe and value.safeUsername == who): if (not safe and value.username.lower() == who) or (safe and value.safeUsername == who):
if ignoreIRC and value.irc: if ignoreIRC and value.irc:
continue continue
if _all:
ret.append(value)
else:
return value return value
# Return full list or None if not found # Return none if not found
if _all:
return ret
else:
return None return None
def deleteOldTokens(self, userID): def deleteOldTokens(self, userID):
@ -174,22 +142,22 @@ class tokenList:
for _, value in self.tokens.items(): for _, value in self.tokens.items():
value.enqueue(packet) value.enqueue(packet)
@sentry.capture() def usersTimeoutCheckLoop(self, timeoutTime = 100, checkTime = 100):
def usersTimeoutCheckLoop(self):
""" """
Start timed out users disconnect loop. Start timed out users disconnect loop.
This function will be called every `checkTime` seconds and so on, forever. This function will be called every `checkTime` seconds and so on, forever.
CALL THIS FUNCTION ONLY ONCE! CALL THIS FUNCTION ONLY ONCE!
:param timeoutTime: seconds of inactivity required to disconnect someone. Default: 100
:param checkTime: seconds between loops. Default: 100
:return: :return:
""" """
try:
log.debug("Checking timed out clients") log.debug("Checking timed out clients")
exceptions = []
timedOutTokens = [] # timed out users timedOutTokens = [] # timed out users
timeoutLimit = int(time.time()) - 100 timeoutLimit = int(time.time())-timeoutTime
for key, value in self.tokens.items(): for key, value in self.tokens.items():
# Check timeout (fokabot is ignored) # Check timeout (fokabot is ignored)
if value.pingTime < timeoutLimit and value.userID != 999 and not value.irc and not value.tournament: if value.pingTime < timeoutLimit and value.userID != 999 and value.irc == False and value.tournament == False:
# That user has timed out, add to disconnected tokens # That user has timed out, add to disconnected tokens
# We can't delete it while iterating or items() throws an error # We can't delete it while iterating or items() throws an error
timedOutTokens.append(key) timedOutTokens.append(key)
@ -199,24 +167,11 @@ class tokenList:
for i in timedOutTokens: for i in timedOutTokens:
log.debug("{} timed out!!".format(self.tokens[i].username)) log.debug("{} timed out!!".format(self.tokens[i].username))
self.tokens[i].enqueue(serverPackets.notification("Your connection to the server timed out.")) self.tokens[i].enqueue(serverPackets.notification("Your connection to the server timed out."))
try:
logoutEvent.handle(self.tokens[i], None) logoutEvent.handle(self.tokens[i], None)
except Exception as e:
exceptions.append(e)
log.error(
"Something wrong happened while disconnecting a timed out client. Reporting to Sentry "
"when the loop ends."
)
del timedOutTokens
# Re-raise exceptions if needed
if exceptions:
raise periodicLoopException(exceptions)
finally:
# Schedule a new check (endless loop) # Schedule a new check (endless loop)
threading.Timer(100, self.usersTimeoutCheckLoop).start() threading.Timer(checkTime, self.usersTimeoutCheckLoop, [timeoutTime, checkTime]).start()
@sentry.capture()
def spamProtectionResetLoop(self): def spamProtectionResetLoop(self):
""" """
Start spam protection reset loop. Start spam protection reset loop.
@ -225,11 +180,10 @@ class tokenList:
:return: :return:
""" """
try:
# Reset spamRate for every token # Reset spamRate for every token
for _, value in self.tokens.items(): for _, value in self.tokens.items():
value.spamRate = 0 value.spamRate = 0
finally:
# Schedule a new check (endless loop) # Schedule a new check (endless loop)
threading.Timer(10, self.spamProtectionResetLoop).start() threading.Timer(10, self.spamProtectionResetLoop).start()
@ -243,7 +197,7 @@ class tokenList:
try: try:
# TODO: Make function or some redis meme # TODO: Make function or some redis meme
glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:sessions:*") glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:sessions:*")
except redis.RedisError: except:
pass pass

77
pep.py
View File

@ -1,7 +1,9 @@
"""Hello, pep.py here, ex-owner of ripple and prime minister of Ripwot."""
import os import os
import sys import sys
import threading import threading
from multiprocessing.pool import ThreadPool from multiprocessing.pool import ThreadPool
import tornado.gen import tornado.gen
import tornado.httpserver import tornado.httpserver
import tornado.ioloop import tornado.ioloop
@ -9,11 +11,7 @@ import tornado.web
from raven.contrib.tornado import AsyncSentryClient from raven.contrib.tornado import AsyncSentryClient
import redis import redis
import json from common import generalUtils
import shutil
from distutils.version import LooseVersion
from common import generalUtils, agpl
from common.constants import bcolors from common.constants import bcolors
from common.db import dbConnector from common.db import dbConnector
from common.ddog import datadogClient from common.ddog import datadogClient
@ -40,7 +38,6 @@ from pubSubHandlers import changeUsernameHandler
from pubSubHandlers import disconnectHandler from pubSubHandlers import disconnectHandler
from pubSubHandlers import banHandler from pubSubHandlers import banHandler
from pubSubHandlers import notificationHandler
from pubSubHandlers import updateSilenceHandler from pubSubHandlers import updateSilenceHandler
from pubSubHandlers import updateStatsHandler from pubSubHandlers import updateStatsHandler
@ -57,15 +54,7 @@ def make_app():
(r"/stress", heavyHandler.handler) (r"/stress", heavyHandler.handler)
]) ])
if __name__ == "__main__": if __name__ == "__main__":
# AGPL license agreement
try:
agpl.check_license("ripple", "pep.py")
except agpl.LicenseError as e:
print(str(e))
sys.exit(1)
try: try:
# Server start # Server start
consoleHelper.printServerStartHeader(True) consoleHelper.printServerStartHeader(True)
@ -90,34 +79,6 @@ if __name__ == "__main__":
else: else:
consoleHelper.printDone() consoleHelper.printDone()
# Read additional config file
consoleHelper.printNoNl("> Loading additional config file... ")
try:
if not os.path.isfile(glob.conf.config["custom"]["config"]):
consoleHelper.printWarning()
consoleHelper.printColored("[!] Missing config file at {}; A default one has been generated at this location.".format(glob.conf.config["custom"]["config"]), bcolors.YELLOW)
shutil.copy("common/default_config.json", glob.conf.config["custom"]["config"])
with open(glob.conf.config["custom"]["config"], "r") as f:
glob.conf.extra = json.load(f)
consoleHelper.printDone()
except:
consoleHelper.printWarning()
consoleHelper.printColored("[!] Unable to load custom config at {}".format(glob.conf.config["custom"]["config"]), bcolors.RED)
consoleHelper.printColored("[!] Make sure you have the latest osufx common submodule!", bcolors.RED)
sys.exit()
# Check if running common module is usable
if glob.COMMON_VERSION == "Unknown":
consoleHelper.printWarning()
consoleHelper.printColored("[!] You do not seem to be using osufx's common submodule... nothing will work...", bcolors.RED)
consoleHelper.printColored("[!] You can download or fork the submodule from {}https://github.com/osufx/ripple-python-common".format(bcolors.UNDERLINE), bcolors.RED)
sys.exit()
elif LooseVersion(glob.COMMON_VERSION_REQ) > LooseVersion(glob.COMMON_VERSION):
consoleHelper.printColored("[!] Your common submodule version is below the required version number for this version of pep.py.", bcolors.RED)
consoleHelper.printColored("[!] You are highly adviced to update your common submodule as stability may vary with outdated modules.", bcolors.RED)
# Create data folder if needed # Create data folder if needed
consoleHelper.printNoNl("> Checking folders... ") consoleHelper.printNoNl("> Checking folders... ")
paths = [".data"] paths = [".data"]
@ -183,7 +144,7 @@ if __name__ == "__main__":
consoleHelper.printNoNl("> Creating threads pool... ") consoleHelper.printNoNl("> Creating threads pool... ")
glob.pool = ThreadPool(int(glob.conf.config["server"]["threads"])) glob.pool = ThreadPool(int(glob.conf.config["server"]["threads"]))
consoleHelper.printDone() consoleHelper.printDone()
except ValueError: except:
consoleHelper.printError() consoleHelper.printError()
consoleHelper.printColored("[!] Error while creating threads pool. Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Error while creating threads pool. Please check your config.ini and run the server again", bcolors.RED)
@ -196,11 +157,6 @@ if __name__ == "__main__":
consoleHelper.printColored("[!] Error while loading chat filters. Make sure there is a filters.txt file present", bcolors.RED) consoleHelper.printColored("[!] Error while loading chat filters. Make sure there is a filters.txt file present", bcolors.RED)
raise raise
# Start fokabot
consoleHelper.printNoNl("> Connecting bot... ")
fokabot.connect()
consoleHelper.printDone()
# Initialize chat channels # Initialize chat channels
print("> Initializing chat channels... ") print("> Initializing chat channels... ")
glob.channels.loadChannels() glob.channels.loadChannels()
@ -212,6 +168,11 @@ if __name__ == "__main__":
glob.streams.add("lobby") glob.streams.add("lobby")
consoleHelper.printDone() consoleHelper.printDone()
# Start fokabot
consoleHelper.printNoNl("> Connecting FokaBot... ")
fokabot.connect()
consoleHelper.printDone()
# Initialize user timeout check loop # Initialize user timeout check loop
consoleHelper.printNoNl("> Initializing user timeout check loop... ") consoleHelper.printNoNl("> Initializing user timeout check loop... ")
glob.tokens.usersTimeoutCheckLoop() glob.tokens.usersTimeoutCheckLoop()
@ -222,11 +183,6 @@ if __name__ == "__main__":
glob.tokens.spamProtectionResetLoop() glob.tokens.spamProtectionResetLoop()
consoleHelper.printDone() consoleHelper.printDone()
# Initialize multiplayer cleanup loop
consoleHelper.printNoNl("> Initializing multiplayer cleanup loop... ")
glob.matches.cleanupLoop()
consoleHelper.printDone()
# Localize warning # Localize warning
glob.localize = generalUtils.stringToBool(glob.conf.config["localize"]["enable"]) glob.localize = generalUtils.stringToBool(glob.conf.config["localize"]["enable"])
if not glob.localize: if not glob.localize:
@ -234,7 +190,7 @@ if __name__ == "__main__":
# Discord # Discord
if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]): if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]):
glob.schiavo = schiavo.schiavo(glob.conf.config["discord"]["boturl"], "**pep.py**") glob.schiavo = schiavo.schiavo(glob.conf.config["discord"]["boturl"])
else: else:
consoleHelper.printColored("[!] Warning! Discord logging is disabled!", bcolors.YELLOW) consoleHelper.printColored("[!] Warning! Discord logging is disabled!", bcolors.YELLOW)
@ -258,7 +214,7 @@ if __name__ == "__main__":
try: try:
glob.sentry = generalUtils.stringToBool(glob.conf.config["sentry"]["enable"]) glob.sentry = generalUtils.stringToBool(glob.conf.config["sentry"]["enable"])
if glob.sentry: if glob.sentry:
glob.application.sentry_client = AsyncSentryClient(glob.conf.config["sentry"]["banchodsn"], release=glob.VERSION) glob.application.sentry_client = AsyncSentryClient(glob.conf.config["sentry"]["banchodns"], release=glob.VERSION)
else: else:
consoleHelper.printColored("[!] Warning! Sentry logging is disabled!", bcolors.YELLOW) consoleHelper.printColored("[!] Warning! Sentry logging is disabled!", bcolors.YELLOW)
except: except:
@ -294,26 +250,24 @@ if __name__ == "__main__":
glob.irc = generalUtils.stringToBool(glob.conf.config["irc"]["enable"]) glob.irc = generalUtils.stringToBool(glob.conf.config["irc"]["enable"])
if glob.irc: if glob.irc:
# IRC port # IRC port
ircPort = 0
try: try:
ircPort = int(glob.conf.config["irc"]["port"]) ircPort = int(glob.conf.config["irc"]["port"])
except ValueError: except:
consoleHelper.printColored("[!] Invalid IRC port! Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Invalid IRC port! Please check your config.ini and run the server again", bcolors.RED)
log.logMessage("IRC server started!", discord="bunker", of="info.txt", stdout=False) log.logMessage("**pep.py** IRC server started!", discord="bunker", of="info.txt", stdout=False)
consoleHelper.printColored("> IRC server listening on 127.0.0.1:{}...".format(ircPort), bcolors.GREEN) consoleHelper.printColored("> IRC server listening on 127.0.0.1:{}...".format(ircPort), bcolors.GREEN)
threading.Thread(target=lambda: ircserver.main(port=ircPort)).start() threading.Thread(target=lambda: ircserver.main(port=ircPort)).start()
else: else:
consoleHelper.printColored("[!] Warning! IRC server is disabled!", bcolors.YELLOW) consoleHelper.printColored("[!] Warning! IRC server is disabled!", bcolors.YELLOW)
# Server port # Server port
serverPort = 0
try: try:
serverPort = int(glob.conf.config["server"]["port"]) serverPort = int(glob.conf.config["server"]["port"])
except ValueError: except:
consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED)
# Server start message and console output # Server start message and console output
log.logMessage("Server started!", discord="bunker", of="info.txt", stdout=False) 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) consoleHelper.printColored("> Tornado listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN)
# Connect to pubsub channels # Connect to pubsub channels
@ -324,7 +278,6 @@ if __name__ == "__main__":
"peppy:update_cached_stats": updateStatsHandler.handler(), "peppy:update_cached_stats": updateStatsHandler.handler(),
"peppy:silence": updateSilenceHandler.handler(), "peppy:silence": updateSilenceHandler.handler(),
"peppy:ban": banHandler.handler(), "peppy:ban": banHandler.handler(),
"peppy:notification": notificationHandler.handler(),
}).start() }).start()
# Start tornado # Start tornado

View File

@ -6,7 +6,6 @@ from objects import glob
def handleUsernameChange(userID, newUsername, targetToken=None): def handleUsernameChange(userID, newUsername, targetToken=None):
try: try:
userUtils.appendNotes(userID, "Username change: '{}' -> '{}'".format(userUtils.getUsername(userID), newUsername))
userUtils.changeUsername(userID, newUsername=newUsername) userUtils.changeUsername(userID, newUsername=newUsername)
if targetToken is not None: if targetToken is not None:
targetToken.kick("Your username has been changed to {}. Please log in again.".format(newUsername), "username_change") targetToken.kick("Your username has been changed to {}. Please log in again.".format(newUsername), "username_change")

View File

@ -1,19 +0,0 @@
from common.redis import generalPubSubHandler
from objects import glob
from constants import serverPackets
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"message": ""
}
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.enqueue(serverPackets.notification(data["message"]))

View File

@ -1,10 +1,9 @@
requests==2.18.1 requests
tornado==4.4.2 tornado
mysqlclient==1.3.9 mysqlclient
psutil==5.2.2 psutil
raven==5.32.0 raven
bcrypt==3.1.1 bcrypt>=3.1.1
dill==0.2.7.1 dill
redis==2.10.5 redis
cython==0.27.3 cython
datadog==0.14.0

View File

@ -7,7 +7,7 @@ import os
cythonExt = [] cythonExt = []
for root, dirs, files in os.walk(os.getcwd()): for root, dirs, files in os.walk(os.getcwd()):
for file in files: for file in files:
if file.endswith(".pyx") and ".pyenv" not in root: # im sorry if file.endswith(".pyx"):
filePath = os.path.relpath(os.path.join(root, file)) filePath = os.path.relpath(os.path.join(root, file))
cythonExt.append(Extension(filePath.replace("/", ".")[:-4], [filePath])) cythonExt.append(Extension(filePath.replace("/", ".")[:-4], [filePath]))

View File

@ -1 +1 @@
1.13.7 1.11.0