50 Commits

Author SHA1 Message Date
Nyo
e92cbe47bd ⬆️ 1.11.0 ⬆️ 2016-12-09 13:19:04 +01:00
Nyo
6ca2016f7b .BANCHO. Disabled datadog ram usage tracking 2016-12-09 11:47:00 +01:00
Nyo
2f54a56b7a Add full build script 2016-12-08 15:46:21 +01:00
Nyo
cf9e506875 .HIDE. Update submodules 2016-12-08 12:04:59 +01:00
Nyo
5c93d692ea .BANCHO. Cythonized mainHandler 2016-12-08 11:44:27 +01:00
Nyo
a8a1dfb1bc .HIDE. Update .gitignore 2016-12-08 11:44:12 +01:00
Nyo
9d562e7acd .BANCHO. Dynamic setup.py file 2016-12-08 11:43:23 +01:00
Nyo
4f4253afce .HIDE. Update README.txt 2016-12-07 22:31:28 +01:00
Nyo
04898c24ae .BANCHO. Ported packet encoder/decoder to Cython, add distutils setup file, update .gitignore, README and requirements.txt 2016-12-07 22:25:16 +01:00
Nyo
1b94936092 .BANCHO. .FIX. Fix wrong default configuration file 2016-12-07 21:15:55 +01:00
Nyo
d4591b42a3 .BANCHO. !kickall command now requires 'manage server' privilege 2016-12-07 21:15:23 +01:00
Nyo
69508f9a0e Add Google auth 2fa check at login 2016-11-30 23:33:56 +01:00
Nyo
5cf8c1bde8 Merge branch 'master' of git.zxq.co:ripple/pep.py 2016-11-30 20:08:54 +01:00
Nyo
20be60d9db Update submodules 2016-11-30 20:08:43 +01:00
Howl
61935f323c add link to github mirror 2016-11-29 17:21:24 +01:00
Nyo
cecef18d13 .HIDE. Update submodules 2016-11-20 14:17:35 +01:00
Nyo
5723c0e68f .BANCHO. Move online users count to redis 2016-11-20 14:17:05 +01:00
Nyo
525235a27e .BANCHO. Move bancho sessions to redis 2016-11-20 13:03:07 +01:00
Nyo
3bc390e3e6 .HIDE. Update submodules 2016-11-20 12:32:45 +01:00
Nyo
f6ae673401 .HIDE. Update submodules 2016-11-20 11:32:21 +01:00
Nyo
aa32e8bea6 .BANCHO. Add pubsub handlers for username changes, bans, restrictions, silences, stats update, kicks and bancho settings reload. 2016-11-20 11:31:51 +01:00
Nyo
fb00063e0f ⬆️ v1.10.0 ⬆️ 2016-11-17 20:13:36 +01:00
Nyo
e30893d66f .BANCHO. Add lets version in !system status 2016-11-17 20:11:11 +01:00
Nyo
8078596a0a .HIDE. Removed shrug version 2016-11-17 20:10:32 +01:00
Nyo
eefec1f47b .BANCHO. Save pep.py version in redis 2016-11-17 20:07:06 +01:00
Nyo
2992dc6190 .HIDE. Update submodules 2016-11-17 19:39:50 +01:00
Nyo
b6e2319e8c .HIDE. Remove random prints, fix #9 2016-11-17 19:15:14 +01:00
Nyo
a2ef03c887 .HIDE. General refactoring and documentation 2016-11-17 19:13:06 +01:00
Nyo
abad698fe3 .HIDE. avail pls 2016-11-17 15:28:07 +01:00
Nyo
030d556b9c .BANCHO. /api/v1/isOnline now supports both safe and unsafe usernames 2016-11-17 15:27:27 +01:00
Nyo
d51b304fbe .HIDE. Update submodules 2016-11-17 13:39:57 +01:00
Nyo
1ecc73e0dc .HIDE. Update submodules 2016-11-16 23:22:25 +01:00
Nyo
b1315815b2 Merge branch 'master' of git.zxq.co:ripple/pep.py 2016-11-15 20:38:58 +01:00
Nyo
38bcf3a735 .BANCHO. Add redis support, remove userID cache 2016-11-15 20:38:15 +01:00
Nyo
a6292c7374 Add redis support, remove userID cache 2016-11-15 20:36:29 +01:00
Nyo
ef940771d8 .HIDE. Update submodules 2016-11-13 19:06:47 +01:00
Nyo
b03d51abff .HIDE. Update submodules 2016-11-13 18:47:10 +01:00
Nyo
8ef02faf36 .HIDE. Update submodules 2016-11-13 12:36:10 +01:00
Nyo
d29dcd25f7 .BANCHO. Remove schiavo logs for /np in #spect_* 2016-11-13 12:35:15 +01:00
Nyo
cd75d1ad8d .BANCHO. Add bancho components RAM usage as datadog stats 2016-11-13 12:25:38 +01:00
Nyo
c7c5528588 .BANCHO. .FIX. Fix /away command, add support for /away command on IRC 2016-11-13 12:23:45 +01:00
Nyo
3b150d70cd .BANCHO. Decrease IRC polling time to 1 second 2016-11-12 22:55:43 +01:00
Nyo
d249dd593f .HIDE. Update requirements.txt 2016-11-01 09:57:43 +01:00
Nyo
78a6931805 .HIDE. Update submodules 2016-10-31 16:50:34 +01:00
Nyo
555c9cca1f .HIDE. Update requirements.txt 2016-10-31 16:40:54 +01:00
Nyo
83c514b75e .HIDE. Update submodules 2016-10-31 16:39:54 +01:00
Nyo
0e5471383e .BANCHO. Update submodules 2016-10-20 20:23:49 +02:00
Nyo
abc8e058a1 .BANCHO. .HIDE. Remove cloudflare option in config.ini 2016-10-20 20:22:23 +02:00
Nyo
0a53b31e42 .BANCHO. Enabled update command 2016-10-16 10:56:41 +02:00
Nyo
b61ac0e9b6 .BANCHO. .HIDE. Changed mirror/apiurl to mirror/api in config file 2016-10-16 10:43:04 +02:00
53 changed files with 1006 additions and 567 deletions

7
.gitignore vendored
View File

@@ -1,8 +1,9 @@
**/__pycache__ **/__pycache__
**/build
config.ini config.ini
filters.txt filters.txt
.data .data
.idea .idea
common_funzia redistest.py
common_refractor *.c
common_memato *.so

View File

@@ -1,4 +1,8 @@
## pep.py ## pep.py
- Origin: https://git.zxq.co/ripple/pep.py
- Mirror: https://github.com/osuripple/pep.py
This is Ripple's bancho server. It handles: This is Ripple's bancho server. It handles:
- Client login - Client login
- Online users listing and statuses - Online users listing and statuses
@@ -9,6 +13,8 @@ This is Ripple's bancho server. It handles:
## Requirements ## Requirements
- Python 3.5 - Python 3.5
- Cython
- C compiler
- MySQLdb (`mysqlclient`) - MySQLdb (`mysqlclient`)
- Tornado - Tornado
- Bcrypt - Bcrypt
@@ -23,9 +29,14 @@ afterwards, install the required dependencies with pip
``` ```
$ pip install -r requirements.txt $ pip install -r requirements.txt
``` ```
then, run pep.py once to create the default config file and edit it then, compile all `*.pyx` files to `*.so` or `*.dll` files using `setup.py` (distutils file)
```
$ python3 setup.py build_ext --inplace
```
finally, run pep.py once to create the default config file and edit it
``` ```
$ python3 pep.py $ python3 pep.py
...
$ nano config.ini $ nano config.ini
``` ```
you can run pep.py by typing you can run pep.py by typing

2
common

Submodule common updated: a899c0be89...3288420cd8

View File

@@ -1,4 +1,3 @@
""" Contains functions used to read specific client packets from byte stream """
from constants import dataTypes from constants import dataTypes
from helpers import packetHelper from helpers import packetHelper
from constants import slotStatuses from constants import slotStatuses
@@ -100,7 +99,7 @@ def matchSettings(stream):
start += 2 start += 2
for i in range(0,16): for i in range(0,16):
s = data[0]["slot{}Status".format(str(i))] s = data[0]["slot{}Status".format(str(i))]
if s != slotStatuses.free and s != slotStatuses.locked: if s != slotStatuses.FREE and s != slotStatuses.LOCKED:
start += 4 start += 4
# Other settings # Other settings

View File

@@ -1,5 +1,3 @@
"""Bancho exceptions"""
# TODO: Prints in exceptions
class loginFailedException(Exception): class loginFailedException(Exception):
pass pass

View File

@@ -19,12 +19,12 @@ from objects import glob
Commands callbacks Commands callbacks
Must have fro, chan and messages as arguments Must have fro, chan and messages as arguments
fro -- name of who triggered the command :param fro: username of who triggered the command
chan -- channel where the message was sent :param chan: channel"(or username, if PM) where the message was sent
message -- list containing arguments passed from the message :param message: list containing arguments passed from the message
[0] = first argument [0] = first argument
[1] = second argument [1] = second argument
. . . . . .
return the message or **False** if there's no response by the bot return the message or **False** if there's no response by the bot
TODO: Change False to None, because False doesn't make any sense TODO: Change False to None, because False doesn't make any sense
@@ -35,6 +35,7 @@ def instantRestart(fro, chan, message):
return False return False
def faq(fro, chan, message): def faq(fro, chan, message):
# TODO: Unhardcode this
if message[0] == "rules": if message[0] == "rules":
return "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23]." return "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23]."
elif message[0] == "swearing": elif message[0] == "swearing":
@@ -160,11 +161,11 @@ def silence(fro, chan, message):
if unit == 's': if unit == 's':
silenceTime = int(amount) silenceTime = int(amount)
elif unit == 'm': elif unit == 'm':
silenceTime = int(amount)*60 silenceTime = int(amount) * 60
elif unit == 'h': elif unit == 'h':
silenceTime = int(amount)*3600 silenceTime = int(amount) * 3600
elif unit == 'd': elif unit == 'd':
silenceTime = int(amount)*86400 silenceTime = int(amount) * 86400
else: else:
return "Invalid time unit (s/m/h/d)." return "Invalid time unit (s/m/h/d)."
@@ -303,22 +304,7 @@ def systemShutdown(fro, chan, message):
return restartShutdown(False) return restartShutdown(False)
def systemReload(fro, chan, message): def systemReload(fro, chan, message):
# Reload settings from bancho_settings glob.banchoConf.reload()
glob.banchoConf.loadSettings()
# Reload channels too
glob.channels.loadChannels()
# And chat filters
glob.chatFilters.loadFilters()
# Send new channels and new bottom icon to everyone
glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
glob.streams.broadcast("main", serverPackets.channelInfoEnd())
for key, value in glob.channels.channels.items():
if value.publicRead == True and value.hidden == False:
glob.streams.broadcast("main", serverPackets.channelInfo(key))
return "Bancho settings reloaded!" return "Bancho settings reloaded!"
def systemMaintenance(fro, chan, message): def systemMaintenance(fro, chan, message):
@@ -359,7 +345,13 @@ def systemStatus(fro, chan, message):
data = systemHelper.getSystemInfo() data = systemHelper.getSystemInfo()
# Final message # Final message
letsVersion = glob.redis.get("lets:version")
if letsVersion is None:
letsVersion = "\_(xd)_/"
else:
letsVersion = letsVersion.decode("utf-8")
msg = "pep.py bancho server v{}\n".format(glob.VERSION) msg = "pep.py bancho server v{}\n".format(glob.VERSION)
msg += "LETS scores server v{}\n".format(letsVersion)
msg += "made by the Ripple team\n" msg += "made by the Ripple team\n"
msg += "\n" msg += "\n"
msg += "=== BANCHO STATS ===\n" msg += "=== BANCHO STATS ===\n"
@@ -684,7 +676,7 @@ def updateBeatmap(fro, chan, to):
if beatmapData is None: if beatmapData is None:
return "Couldn't find beatmap data in database. Please load the beatmap's leaderboard and try again." return "Couldn't find beatmap data in database. Please load the beatmap's leaderboard and try again."
response = requests.post("{}/api/v1/update_beatmap".format(glob.conf.config["mirror"]["apiurl"]), { response = requests.post("{}/api/v1/update_beatmap".format(glob.conf.config["mirror"]["url"]), {
"beatmap_set_id": beatmapData["beatmapset_id"], "beatmap_set_id": beatmapData["beatmapset_id"],
"beatmap_name": beatmapData["song_name"], "beatmap_name": beatmapData["song_name"],
"username": token.username, "username": token.username,
@@ -707,11 +699,6 @@ callback: function to call when the command is triggered. Optional.
response: text to return when the command is triggered. Optional. response: text to return when the command is triggered. Optional.
syntax: command syntax. Arguments must be separated by spaces (eg: <arg1> <arg2>) syntax: command syntax. Arguments must be separated by spaces (eg: <arg1> <arg2>)
privileges: privileges needed to execute the command. Optional. privileges: privileges needed to execute the command. Optional.
NOTES:
- You CAN'T use both rank and minRank at the same time.
- If both rank and minrank are **not** present, everyone will be able to run that command.
- You MUST set trigger and callback/response, or the command won't work.
""" """
commands = [ commands = [
{ {
@@ -751,7 +738,7 @@ commands = [
"callback": moderated "callback": moderated
}, { }, {
"trigger": "!kickall", "trigger": "!kickall",
"privileges": privileges.ADMIN_KICK_USERS, "privileges": privileges.ADMIN_MANAGE_SERVERS,
"callback": kickAll "callback": kickAll
}, { }, {
"trigger": "!kick", "trigger": "!kick",
@@ -837,8 +824,7 @@ commands = [
"callback": pp "callback": pp
}, { }, {
"trigger": "!update", "trigger": "!update",
"callback": updateBeatmap, "callback": updateBeatmap
"privileges": privileges.ADMIN_MANAGE_SERVERS, # TODO: Remove privileges for !update
} }
# #
# "trigger": "!acc", # "trigger": "!acc",

View File

@@ -1,2 +1,2 @@
normal = 0 NORMAL = 0
freeMod = 1 FREE_MOD = 1

View File

@@ -1,3 +1,3 @@
score = 0 SCORE = 0
accuracy = 1 ACCURACY = 1
combo = 2 COMBO = 2

View File

@@ -1,4 +1,4 @@
headToHead = 0 HEAD_TO_HEAD = 0
tagCoop = 1 TAG_COOP = 1
teamVs = 2 TEAM_VS = 2
tagTeamVs = 3 TAG_TEAM_VS = 3

View File

@@ -1,3 +1,3 @@
noTeam = 0 NO_TEAM = 0
blue = 1 BLUE = 1
red = 2 RED = 2

View File

@@ -159,7 +159,7 @@ def channelInfo(chan):
return packetHelper.buildPacket(packetIDs.server_channelInfo, [ return packetHelper.buildPacket(packetIDs.server_channelInfo, [
[chan, dataTypes.STRING], [chan, dataTypes.STRING],
[channel.description, dataTypes.STRING], [channel.description, dataTypes.STRING],
[channel.getConnectedUsersCount(), dataTypes.UINT16] [len(channel.connectedUsers), dataTypes.UINT16]
]) ])
def channelInfoEnd(): def channelInfoEnd():

View File

@@ -1,8 +1,8 @@
free = 1 FREE = 1
locked = 2 LOCKED = 2
notReady = 4 NOT_READY = 4
ready = 8 READY = 8
noMap = 16 NO_MAP = 16
playing = 32 PLAYING = 32
occupied = 124 OCCUPIED = 124
playingQuit = 128 PLAYING_QUIT = 128

View File

@@ -1,4 +1,3 @@
"""Bancho user ranks"""
NORMAL = 0 NORMAL = 0
PLAYER = 1 PLAYER = 1
BAT = 2 BAT = 2

View File

@@ -5,21 +5,19 @@ from constants import clientPackets
from constants import serverPackets from constants import serverPackets
from objects import glob from objects import glob
def handle(userToken, packetData): def handle(userToken, packetData):
# Get usertoken data # Get usertoken data
userID = userToken.userID userID = userToken.userID
username = userToken.username username = userToken.username
# Make sure we are not banned # Make sure we are not banned
if userUtils.isBanned(userID): #if userUtils.isBanned(userID):
userToken.enqueue(serverPackets.loginBanned()) # userToken.enqueue(serverPackets.loginBanned())
return # return
# Send restricted message if needed # Send restricted message if needed
if not userToken.restricted: #if userToken.restricted:
if userUtils.isRestricted(userID): # userToken.checkRestricted(True)
userToken.setRestricted()
# Change action packet # Change action packet
packetData = clientPackets.userActionChange(packetData) packetData = clientPackets.userActionChange(packetData)
@@ -35,8 +33,10 @@ if userToken.matchID != -1 and userToken.actionID != actions.MULTIPLAYING and us
''' '''
# Update cached stats if our pp changed if we've just submitted a score or we've changed gameMode # Update cached stats if our pp changed if we've just submitted a score or we've changed gameMode
if (userToken.actionID == actions.PLAYING or userToken.actionID == actions.MULTIPLAYING) or (userToken.pp != userUtils.getPP(userID, userToken.gameMode)) or (userToken.gameMode != packetData["gameMode"]): #if (userToken.actionID == actions.PLAYING or userToken.actionID == actions.MULTIPLAYING) or (userToken.pp != userUtils.getPP(userID, userToken.gameMode)) or (userToken.gameMode != packetData["gameMode"]):
# Always update game mode, or we'll cache stats from the wrong game mode if we've changed it
# Update cached stats if we've changed gamemode
if userToken.gameMode != packetData["gameMode"]:
userToken.gameMode = packetData["gameMode"] userToken.gameMode = packetData["gameMode"]
userToken.updateCachedStats() userToken.updateCachedStats()

View File

@@ -18,7 +18,7 @@ def handle(userToken, packetData):
match = glob.matches.matches[matchID] match = glob.matches.matches[matchID]
# Set slot or match mods according to modType # Set slot or match mods according to modType
if match.matchModMode == matchModModes.freeMod: if match.matchModMode == matchModModes.FREE_MOD:
# Freemod # Freemod
# Host can set global DT/HT # Host can set global DT/HT
if userID == match.hostUserID: if userID == match.hostUserID:

View File

@@ -81,11 +81,11 @@ 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:
for i in range(0,16): for i in range(0,16):
if match.slots[i].status == slotStatuses.ready: if match.slots[i].status == slotStatuses.READY:
match.slots[i].status = slotStatuses.notReady 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
for i in range(0,16): for i in range(0,16):
match.slots[i].mods = 0 match.slots[i].mods = 0
@@ -94,21 +94,21 @@ def handle(userToken, packetData):
match.mods = 0 match.mods = 0
# Set/reset teams # Set/reset teams
if match.matchTeamType == matchTeamTypes.teamVs or match.matchTeamType == matchTeamTypes.tagTeamVs: if match.matchTeamType == matchTeamTypes.TEAM_VS or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
# Set teams # Set teams
c=0 c=0
for i in range(0,16): for i in range(0,16):
if match.slots[i].team == matchTeams.noTeam: if match.slots[i].team == matchTeams.NO_TEAM:
match.slots[i].team = matchTeams.red if c % 2 == 0 else matchTeams.blue match.slots[i].team = matchTeams.RED if c % 2 == 0 else matchTeams.BLUE
c+=1 c+=1
else: else:
# Reset teams # Reset teams
for i in range(0,16): for i in range(0,16):
match.slots[i].team = matchTeams.noTeam match.slots[i].team = matchTeams.NO_TEAM
# Force no freemods if tag coop # Force no freemods if tag coop
if match.matchTeamType == matchTeamTypes.tagCoop or match.matchTeamType == matchTeamTypes.tagTeamVs: if match.matchTeamType == matchTeamTypes.TAG_COOP or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
match.matchModMode = matchModModes.normal match.matchModMode = matchModModes.NORMAL
# Send updated settings # Send updated settings
match.sendUpdates() match.sendUpdates()

View File

@@ -46,7 +46,6 @@ def handle(tornadoRequest):
splitData = loginData[2].split("|") splitData = loginData[2].split("|")
osuVersion = splitData[0] osuVersion = splitData[0]
timeOffset = int(splitData[1]) timeOffset = int(splitData[1])
print(str(timeOffset))
clientData = splitData[3].split(":")[:5] clientData = splitData[3].split(":")[:5]
if len(clientData) < 4: if len(clientData) < 4:
raise exceptions.forceUpdateException() raise exceptions.forceUpdateException()
@@ -192,19 +191,20 @@ def handle(tornadoRequest):
# 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:
# Get location and country from IP # Get location and country from IP
location = locationHelper.getLocation(requestIP) latitude, longitude = locationHelper.getLocation(requestIP)
countryLetters = locationHelper.getCountry(requestIP) countryLetters = locationHelper.getCountry(requestIP)
country = countryHelper.getCountryID(countryLetters) country = countryHelper.getCountryID(countryLetters)
else: else:
# Set location to 0,0 and get country from db # Set location to 0,0 and get country from db
log.warning("Location skipped") log.warning("Location skipped")
location = [0,0] latitude = 0
longitude = 0
countryLetters = "XX" countryLetters = "XX"
country = countryHelper.getCountryID(userUtils.getCountry(userID)) country = countryHelper.getCountryID(userUtils.getCountry(userID))
# Set location and country # Set location and country
responseToken.setLocation(location) responseToken.setLocation(latitude, longitude)
responseToken.setCountry(country) responseToken.country = country
# Set country in db if user has no country (first bancho login) # Set country in db if user has no country (first bancho login)
if userUtils.getCountry(userID) == "XX": if userUtils.getCountry(userID) == "XX":

View File

@@ -1,4 +1,5 @@
import time import time
import json
from common.log import logUtils as log from common.log import logUtils as log
from constants import serverPackets from constants import serverPackets
@@ -6,7 +7,7 @@ from helpers import chatHelper as chat
from objects import glob from objects import glob
def handle(userToken, _=None): def handle(userToken, _=None, deleteToken=True):
# get usertoken data # get usertoken data
userID = userToken.userID userID = userToken.userID
username = userToken.username username = userToken.username
@@ -38,7 +39,19 @@ def handle(userToken, _=None):
glob.ircServer.forceDisconnection(userToken.username) glob.ircServer.forceDisconnection(userToken.username)
# Delete token # Delete token
glob.tokens.deleteToken(requestToken) if deleteToken:
glob.tokens.deleteToken(requestToken)
else:
userToken.kicked = True
# Change username if needed
newUsername = glob.redis.get("ripple:change_username_pending:{}".format(userID))
if newUsername is not None:
log.debug("Sending username change request for user {}".format(userID))
glob.redis.publish("peppy:change_username", json.dumps({
"userID": userID,
"newUsername": newUsername.decode("utf-8")
}))
# Console output # Console output
log.info("{} has been disconnected. (logout)".format(username)) log.info("{} has been disconnected. (logout)".format(username))

View File

@@ -11,7 +11,7 @@ def handle(userToken, packetData):
packetData = clientPackets.setAwayMessage(packetData) packetData = clientPackets.setAwayMessage(packetData)
# Set token away message # Set token away message
userToken.setAwayMessage(packetData["awayMessage"]) userToken.awayMessage = packetData["awayMessage"]
# Send private message from fokabot # Send private message from fokabot
if packetData["awayMessage"] == "": if packetData["awayMessage"] == "":

4
full_build.sh Normal file
View File

@@ -0,0 +1,4 @@
find . -name "*.c" -type f -delete
find . -name "*.o" -type f -delete
find . -name "*.so" -type f -delete
python3 setup.py build_ext --inplace

View File

@@ -21,7 +21,6 @@ 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()
log.info("API REQUEST FOR FOKABOT MESSAGE AAAAAAA")
chatHelper.sendMessage("FokaBot", self.get_argument("to"), self.get_argument("msg")) chatHelper.sendMessage("FokaBot", self.get_argument("to"), self.get_argument("msg"))
# Status code and message # Status code and message
@@ -35,7 +34,5 @@ class handler(requestsManager.asyncRequestHandler):
data["status"] = statusCode data["status"] = statusCode
# Send response # Send response
#self.clear()
self.write(json.dumps(data)) self.write(json.dumps(data))
self.set_status(statusCode) self.set_status(statusCode)
#self.finish(json.dumps(data))

View File

@@ -18,7 +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") username = self.get_argument("u").lower().replace(" ", "_")
else: else:
try: try:
userID = int(self.get_argument("id")) userID = int(self.get_argument("id"))
@@ -29,7 +29,7 @@ class handler(requestsManager.asyncRequestHandler):
data["result"] = False data["result"] = False
else: else:
if username is not None: if username is not None:
data["result"] = True if glob.tokens.getTokenFromUsername(username) is not None else False data["result"] = True if glob.tokens.getTokenFromUsername(username, safe=True) is not None else False
else: else:
data["result"] = True if glob.tokens.getTokenFromUserID(userID) is not None else False data["result"] = True if glob.tokens.getTokenFromUserID(userID) is not None else False
@@ -44,7 +44,5 @@ class handler(requestsManager.asyncRequestHandler):
data["status"] = statusCode data["status"] = statusCode
# Send response # Send response
#self.clear()
self.write(json.dumps(data)) self.write(json.dumps(data))
self.set_status(statusCode) self.set_status(statusCode)
#self.finish(json.dumps(data))

View File

@@ -10,7 +10,7 @@ class handler(requestsManager.asyncRequestHandler):
data = {"message": "unknown error"} data = {"message": "unknown error"}
try: try:
# Get online users count # Get online users count
data["result"] = len(glob.tokens.tokens) data["result"] = int(glob.redis.get("ripple:online_users").decode("utf-8"))
# Status code and message # Status code and message
statusCode = 200 statusCode = 200
@@ -20,7 +20,5 @@ class handler(requestsManager.asyncRequestHandler):
data["status"] = statusCode data["status"] = statusCode
# Send response # Send response
#self.clear()
self.write(json.dumps(data)) self.write(json.dumps(data))
self.set_status(statusCode) self.set_status(statusCode)
#self.finish(json.dumps(data))

View File

@@ -20,7 +20,5 @@ class handler(requestsManager.asyncRequestHandler):
data["status"] = statusCode data["status"] = statusCode
# Send response # Send response
#self.clear()
self.write(json.dumps(data)) self.write(json.dumps(data))
self.set_status(statusCode) self.set_status(statusCode)
#self.finish(json.dumps(data))

View File

@@ -115,8 +115,6 @@ class handler(SentryMixin, requestsManager.asyncRequestHandler):
return wrapper return wrapper
eventHandler = { eventHandler = {
# TODO: Rename packets and events
# TODO: Host check for multi
packetIDs.client_changeAction: handleEvent(changeActionEvent), packetIDs.client_changeAction: handleEvent(changeActionEvent),
packetIDs.client_logout: handleEvent(logoutEvent), packetIDs.client_logout: handleEvent(logoutEvent),
packetIDs.client_friendAdd: handleEvent(friendAddEvent), packetIDs.client_friendAdd: handleEvent(friendAddEvent),
@@ -124,7 +122,7 @@ class handler(SentryMixin, requestsManager.asyncRequestHandler):
packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent), packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent),
packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent), packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent),
packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent), packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent),
packetIDs.client_channelJoin: handleEvent(channelJoinEvent), packetIDs.client_channelJoin: handleEvent(channelJoinEvent),
packetIDs.client_channelPart: handleEvent(channelPartEvent), packetIDs.client_channelPart: handleEvent(channelPartEvent),
packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent), packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent),
@@ -208,6 +206,9 @@ class handler(SentryMixin, requestsManager.asyncRequestHandler):
userToken.updatePingTime() userToken.updatePingTime()
# Release token lock # Release token lock
userToken.lock.release() userToken.lock.release()
# Delete token if kicked
if userToken.kicked:
glob.tokens.deleteToken(userToken)
if glob.outputRequestTime: if glob.outputRequestTime:
# End time # End time

View File

@@ -12,14 +12,11 @@ def joinChannel(userID = 0, channel = "", token = None, toIRC = True):
""" """
Join a channel Join a channel
userID -- user ID of the user that joins the channel. Optional. :param userID: user ID of the user that joins the channel. Optional. token can be used instead.
token can be used instead. :param token: user token object of user that joins the channel. Optional. userID can be used instead.
token -- user token object of user that joins the channel. Optional. :param channel: channel name
userID can be used instead. :param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Default: True
channel -- name of channe :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
toIRC -- if True, send this channel join event to IRC. Must be true if joining from bancho.
Optional. Defaukt: True
return -- returns 0 if joined or other IRC code in case of error. Needed only on IRC-side
""" """
try: try:
# Get token if not defined # Get token if not defined
@@ -77,15 +74,12 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
""" """
Part a channel Part a channel
userID -- user ID of the user that parts the channel. Optional. :param userID: user ID of the user that parts the channel. Optional. token can be used instead.
token can be used instead. :param token: user token object of user that parts the channel. Optional. userID can be used instead.
token -- user token object of user that parts the channel. Optional. :param channel: channel name
userID can be used instead. :param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Optional. Default: True
channel -- name of channel :param kick: if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False
toIRC -- if True, send this channel join event to IRC. Must be true if joining from bancho. :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
Optional. Defaukt: True
kick -- if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False
return -- returns 0 if joined or other IRC code in case of error. Needed only on IRC-side
""" """
try: try:
# Get token if not defined # Get token if not defined
@@ -151,15 +145,12 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
""" """
Send a message to osu!bancho and IRC server Send a message to osu!bancho and IRC server
fro -- sender username. Optional. :param fro: sender username. Optional. token can be used instead
You can use token instead of this if you wish. :param to: receiver channel (if starts with #) or username
to -- receiver channel (if starts with #) or username :param message: text of the message
message -- text of the message :param token: sender token object. Optional. fro can be used instead
token -- sender token object. :param toIRC: if True, send the message to IRC. If False, send it to Bancho only. Default: True
You can use this instead of fro if you are sending messages from bancho. :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
Optional.
toIRC -- if True, send the message to IRC. If False, send it to Bancho only.
Optional. Default: True
""" """
try: try:
tokenString = "" tokenString = ""
@@ -231,7 +222,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
raise exceptions.channelNoPermissionsException raise exceptions.channelNoPermissionsException
# Everything seems fine, build recipients list and send packet # Everything seems fine, build recipients list and send packet
recipients = glob.channels.channels[to].getConnectedUsers()[:] recipients = glob.channels.channels[to].connectedUsers[:]
for key, value in glob.tokens.tokens.items(): for key, value in glob.tokens.tokens.items():
# Skip our client and irc clients # Skip our client and irc clients
if key == tokenString or value.irc == True: if key == tokenString or value.irc == True:
@@ -256,6 +247,10 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# 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
if recipientToken.awayCheck(userID):
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 == True: if message in messageTemplates.templates and token.admin == True:
sendMessage(fro, to, messageTemplates.templates[message]) sendMessage(fro, to, messageTemplates.templates[message])
@@ -278,7 +273,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
sendMessage("FokaBot", 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("#"): if to.startswith("#") and not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")):
log.chat("{fro} @ {to}: {message}".format(fro=username, to=to, message=str(message.encode("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=username, to=to, message=str(message.encode("utf-8"))[2:-1])) glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))[2:-1]))
return 0 return 0
@@ -308,6 +303,12 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
""" IRC-Bancho Connect/Disconnect/Join/Part interfaces""" """ IRC-Bancho Connect/Disconnect/Join/Part interfaces"""
def fixUsernameForBancho(username): def fixUsernameForBancho(username):
"""
Convert username from IRC format (without spaces) to Bancho format (with spaces)
:param username: username to convert
:return: converted username
"""
# If there are no spaces or underscores in the name # If there are no spaces or underscores in the name
# return it # return it
if " " not in username and "_" not in username: if " " not in username and "_" not in username:
@@ -322,9 +323,22 @@ def fixUsernameForBancho(username):
return username.replace("_", " ") return username.replace("_", " ")
def fixUsernameForIRC(username): def fixUsernameForIRC(username):
"""
Convert an username from Bancho format to IRC format (underscores instead of spaces)
:param username: username to convert
:return: converted username
"""
return username.replace(" ", "_") return username.replace(" ", "_")
def IRCConnect(username): def IRCConnect(username):
"""
Handle IRC login bancho-side.
Add token and broadcast login packet.
:param username: username
:return:
"""
userID = userUtils.getID(username) userID = userUtils.getID(username)
if not userID: if not userID:
log.warning("{} doesn't exist".format(username)) log.warning("{} doesn't exist".format(username))
@@ -335,6 +349,13 @@ def IRCConnect(username):
log.info("{} logged in from IRC".format(username)) log.info("{} logged in from IRC".format(username))
def IRCDisconnect(username): def IRCDisconnect(username):
"""
Handle IRC logout bancho-side.
Remove token and broadcast logout packet.
:param username: username
:return:
"""
token = glob.tokens.getTokenFromUsername(username) token = glob.tokens.getTokenFromUsername(username)
if token is None: if token is None:
log.warning("{} doesn't exist".format(username)) log.warning("{} doesn't exist".format(username))
@@ -343,6 +364,13 @@ def IRCDisconnect(username):
log.info("{} disconnected from IRC".format(username)) log.info("{} disconnected from IRC".format(username))
def IRCJoinChannel(username, channel): def IRCJoinChannel(username, channel):
"""
Handle IRC channel join bancho-side.
:param username: username
:param channel: channel name
:return: IRC return code
"""
userID = userUtils.getID(username) userID = userUtils.getID(username)
if not userID: if not userID:
log.warning("{} doesn't exist".format(username)) log.warning("{} doesn't exist".format(username))
@@ -353,8 +381,30 @@ def IRCJoinChannel(username, channel):
return joinChannel(userID, channel) return joinChannel(userID, channel)
def IRCPartChannel(username, channel): def IRCPartChannel(username, channel):
"""
Handle IRC channel part bancho-side.
:param username: username
:param channel: channel name
:return: IRC return code
"""
userID = userUtils.getID(username) userID = userUtils.getID(username)
if not userID: if not userID:
log.warning("{} doesn't exist".format(username)) log.warning("{} doesn't exist".format(username))
return return
return partChannel(userID, channel) return partChannel(userID, channel)
def IRCAway(username, message):
"""
Handle IRC away command bancho-side.
:param username:
:param message: away message
:return: IRC return code
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
glob.tokens.getTokenFromUserID(userID).awayMessage = message
return 305 if message == "" else 306

View File

@@ -5,9 +5,9 @@ class config:
# Check if config.ini exists and load/generate it # Check if config.ini exists and load/generate it
def __init__(self, file): def __init__(self, file):
""" """
Initialize a config object Initialize a config file object
file -- filename :param file: file name
""" """
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.default = True self.default = True
@@ -25,9 +25,9 @@ class config:
# Check if config.ini has all needed the keys # Check if config.ini has all needed the keys
def checkConfig(self): def checkConfig(self):
""" """
Check if this config has the required keys Check is the config file has all required keys
return -- True if valid, False if not :return: True if valid, False if not valid
""" """
try: try:
# Try to get all the required keys # Try to get all the required keys
@@ -37,14 +37,18 @@ class config:
self.config.get("db","database") self.config.get("db","database")
self.config.get("db","workers") self.config.get("db","workers")
self.config.get("redis","host")
self.config.get("redis","port")
self.config.get("redis","database")
self.config.get("redis","password")
self.config.get("server","port") self.config.get("server","port")
self.config.get("server","threads") self.config.get("server","threads")
self.config.get("server","gzip") self.config.get("server","gzip")
self.config.get("server","gziplevel") self.config.get("server","gziplevel")
self.config.get("server","cikey") self.config.get("server","cikey")
self.config.get("server","cloudflare")
self.config.get("mirror","apiurl") self.config.get("mirror","url")
self.config.get("mirror","apikey") self.config.get("mirror","apikey")
self.config.get("debug","enable") self.config.get("debug","enable")
@@ -75,7 +79,9 @@ class config:
def generateDefaultConfig(self): def generateDefaultConfig(self):
""" """
Open and set default keys for that config file Write a default config file to disk
:return:
""" """
# Open config.ini in write mode # Open config.ini in write mode
f = open(self.fileName, "w") f = open(self.fileName, "w")
@@ -88,16 +94,21 @@ class config:
self.config.set("db", "database", "ripple") self.config.set("db", "database", "ripple")
self.config.set("db", "workers", "4") self.config.set("db", "workers", "4")
self.config.add_section("redis")
self.config.set("redis", "host", "localhost")
self.config.set("redis", "port", "6379")
self.config.set("redis", "database", "0")
self.config.set("redis", "password", "")
self.config.add_section("server") self.config.add_section("server")
self.config.set("server", "port", "5001") self.config.set("server", "port", "5001")
self.config.set("server", "threads", "16") self.config.set("server", "threads", "16")
self.config.set("server", "gzip", "1") self.config.set("server", "gzip", "1")
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.set("server", "cloudflare", "0")
self.config.add_section("mirror") self.config.add_section("mirror")
self.config.set("mirror", "apiurl", "http://storage.ripple.moe") self.config.set("mirror", "url", "http://storage.ripple.moe")
self.config.set("mirror", "apikey", "anotherkey") self.config.set("mirror", "apikey", "anotherkey")
self.config.add_section("debug") self.config.add_section("debug")
@@ -116,9 +127,9 @@ class config:
self.config.set("discord", "devgroup", "") self.config.set("discord", "devgroup", "")
self.config.add_section("datadog") self.config.add_section("datadog")
self.config.set("datadog", "enable") self.config.set("datadog", "enable", "0")
self.config.set("datadog", "apikey") self.config.set("datadog", "apikey", "")
self.config.set("datadog", "appkey") self.config.set("datadog", "appkey", "")
self.config.add_section("irc") self.config.add_section("irc")
self.config.set("irc", "enable", "1") self.config.set("irc", "enable", "1")

View File

@@ -1,11 +1,12 @@
from common.constants import bcolors from common.constants import bcolors
from objects import glob from objects import glob
def printServerStartHeader(asciiArt): def printServerStartHeader(asciiArt=True):
""" """
Print server start header with optional ascii art Print server start message
asciiArt -- if True, will print ascii art too :param asciiArt: print BanchoBoat ascii art. Default: True
:return:
""" """
if asciiArt: if asciiArt:
print("{} _ __".format(bcolors.GREEN)) print("{} _ __".format(bcolors.GREEN))
@@ -32,35 +33,43 @@ def printServerStartHeader(asciiArt):
def printNoNl(string): def printNoNl(string):
""" """
Print string without new line at the end Print a string without \n at the end
string -- string to print :param string: string to print
:return:
""" """
print(string, end="") print(string, end="")
def printColored(string, color): def printColored(string, color):
""" """
Print colored string Print a colored string
string -- string to print :param string: string to print
color -- see bcolors.py :param color: ANSI color code
:return:
""" """
print("{}{}{}".format(color, string, bcolors.ENDC)) print("{}{}{}".format(color, string, bcolors.ENDC))
def printError(): def printError():
""" """
Print error text FOR LOADING Print a red "Error"
:return:
""" """
printColored("Error", bcolors.RED) printColored("Error", bcolors.RED)
def printDone(): def printDone():
""" """
Print error text FOR LOADING Print a green "Done"
:return:
""" """
printColored("Done", bcolors.GREEN) printColored("Done", bcolors.GREEN)
def printWarning(): def printWarning():
""" """
Print error text FOR LOADING Print a yellow "Warning"
:return:
""" """
printColored("Warning", bcolors.YELLOW) printColored("Warning", bcolors.YELLOW)

View File

@@ -1,5 +1,4 @@
"""Contains all country codes with their osu numeric code""" # TODO: Update countries list
countryCodes = { countryCodes = {
"LV": 132, "LV": 132,
"AD": 3, "AD": 3,
@@ -255,12 +254,11 @@ countryCodes = {
def getCountryID(code): def getCountryID(code):
""" """
Get country ID for osu client Get osu country ID from country letters
code -- country name abbreviation (eg: US) :param code: country letters (eg: US)
return -- country code int :return: country osu code
""" """
if code in countryCodes: if code in countryCodes:
return countryCodes[code] return countryCodes[code]
else: else:
@@ -270,10 +268,9 @@ def getCountryLetters(code):
""" """
Get country letters from osu country ID Get country letters from osu country ID
code -- country code int :param code: osu country ID
return -- country name (2 letters) (XX if code not found) :return: country letters (XX if not found)
""" """
for key, value in countryCodes.items(): for key, value in countryCodes.items():
if value == code: if value == code:
return key return key

View File

@@ -7,10 +7,10 @@ from objects import glob
def getCountry(ip): def getCountry(ip):
""" """
Get country from IP address Get country from IP address using geoip api
ip -- IP Address :param ip: IP address
return -- Country code (2 letters) :return: country code. XX if invalid.
""" """
try: try:
# Try to get country from Pikolo Aul's Go-Sanic ip API # Try to get country from Pikolo Aul's Go-Sanic ip API
@@ -22,15 +22,15 @@ def getCountry(ip):
def getLocation(ip): def getLocation(ip):
""" """
Get latitude and longitude from IP address Get latitude and longitude from IP address using geoip api
ip -- IP address :param ip: IP address
return -- [latitude, longitude] :return: (latitude, longitude)
""" """
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

@@ -1,15 +1,15 @@
import struct import struct
from constants import dataTypes from constants import dataTypes
def uleb128Encode(num): cpdef bytearray uleb128Encode(int num):
""" """
Encode int -> uleb128 Encode an int to uleb128
num -- int to encode :param num: int to encode
return -- bytearray with encoded number :return: bytearray with encoded number
""" """
arr = bytearray() cdef bytearray arr = bytearray()
length = 0 cdef int length = 0
if num == 0: if num == 0:
return bytearray(b"\x00") return bytearray(b"\x00")
@@ -23,15 +23,16 @@ def uleb128Encode(num):
return arr return arr
def uleb128Decode(num): cpdef list uleb128Decode(bytes num):
""" """
Decode uleb128 -> int Decode a uleb128 to int
num -- encoded uleb128 :param num: encoded uleb128 int
return -- list. [total, length] :return: (total, length)
""" """
shift = 0 cdef int shift = 0
arr = [0,0] #total, length cdef list arr = [0,0] #total, length
cdef int b
while True: while True:
b = num[arr[1]] b = num[arr[1]]
@@ -43,16 +44,14 @@ def uleb128Decode(num):
return arr return arr
def unpackData(data, dataType): cpdef unpackData(bytes data, int dataType):
""" """
Unpacks data according to dataType Unpacks a single section of a packet.
data -- bytes array to unpack :param data: bytes to unpack
dataType -- data type. See dataTypes.py :param dataType: data type
:return: unpacked bytes
return -- unpacked bytes
""" """
# Get right pack Type # Get right pack Type
if dataType == dataTypes.UINT16: if dataType == dataTypes.UINT16:
unpackType = "<H" unpackType = "<H"
@@ -76,18 +75,17 @@ def unpackData(data, dataType):
# Unpack # Unpack
return struct.unpack(unpackType, bytes(data))[0] return struct.unpack(unpackType, bytes(data))[0]
def packData(__data, dataType): cpdef bytes packData(__data, int dataType):
""" """
Packs data according to dataType Packs a single section of a packet.
data -- bytes to pack :param __data: data to pack
dataType -- data type. See dataTypes.py :param dataType: data type
:return: packed bytes
return -- packed bytes
""" """
cdef bytes data = bytes() # data to return
data = bytes() # data to return cdef bint pack = True # if True, use pack. False only with strings
pack = True # if True, use pack. False only with strings cdef str packType
# Get right pack Type # Get right pack Type
if dataType == dataTypes.BBYTES: if dataType == dataTypes.BBYTES:
@@ -138,23 +136,21 @@ def packData(__data, dataType):
return data return data
def buildPacket(__packet, __packetData=None): cpdef bytes buildPacket(int __packet, list __packetData = []):
""" """
Build a packet Builds a packet
packet -- packet id (int) :param __packet: packet ID
packetData -- list [[data, dataType], [data, dataType], ...] :param __packetData: packet structure [[data, dataType], [data, dataType], ...]
:return: packet bytes
return -- packet bytes
""" """
# Set some variables # Set some variables
if __packetData is None: cdef bytes packetData = bytes()
__packetData = [] cdef int packetLength = 0
packetData = bytes() cdef bytes packetBytes = bytes()
packetLength = 0
packetBytes = bytes()
# Pack packet data # Pack packet data
cdef list i
for i in __packetData: for i in __packetData:
packetData += packData(i[0], i[1]) packetData += packData(i[0], i[1])
@@ -168,42 +164,39 @@ def buildPacket(__packet, __packetData=None):
packetBytes += packetData # packet data packetBytes += packetData # packet data
return packetBytes return packetBytes
def readPacketID(stream): cpdef int readPacketID(bytes stream):
""" """
Read packetID from stream (0-1 bytes) Read packetID (first two bytes) from a packet
stream -- data stream :param stream: packet bytes
return -- packet ID (int) :return: packet ID
""" """
return unpackData(stream[0:2], dataTypes.UINT16) return unpackData(stream[0:2], dataTypes.UINT16)
def readPacketLength(stream): cpdef int readPacketLength(bytes stream):
""" """
Read packet length from stream (3-4-5-6 bytes) Read packet data length (3:7 bytes) from a packet
stream -- data stream :param stream: packet bytes
return -- packet length (int) :return: packet data length
""" """
return unpackData(stream[3:7], dataTypes.UINT32) return unpackData(stream[3:7], dataTypes.UINT32)
def readPacketData(stream, structure=None, 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
stream -- data stream :param structure: packet structure: [[name, dataType], [name, dataType], ...]
structure -- [[name, dataType], [name, dataType], ...] :param hasFirstBytes: if True, `stream` has packetID and length bytes.
hasFirstBytes -- if True, stream has packetID and length bytes. if False, `stream` has only packet data. Default: True
if False, stream has only packetData. :return: {name: unpackedValue, ...}
Optional. Default: True
return -- dictionary. key: name, value: read data
""" """
# Read packet ID (first 2 bytes) # Read packet ID (first 2 bytes)
if structure is None: cdef dict data = {}
structure = []
data = {}
# Skip packet ID and packet length if needed # Skip packet ID and packet length if needed
cdef start, end
if hasFirstBytes: if hasFirstBytes:
end = 7 end = 7
start = 7 start = 7
@@ -212,6 +205,8 @@ def readPacketData(stream, structure=None, hasFirstBytes = True):
start = 0 start = 0
# Read packet # Read packet
cdef list i
cdef bint unpack
for i in structure: for i in structure:
start = end start = end
unpack = True unpack = True
@@ -246,7 +241,10 @@ def readPacketData(stream, structure=None, hasFirstBytes = True):
end = start+length[0]+length[1]+1 end = start+length[0]+length[1]+1
# Read bytes # Read bytes
data[i[0]] = ''.join(chr(j) for j in stream[start+1+length[1]:end]) #data[i[0]] = ''.join(chr(j) for j in stream[start+1+length[1]:end])
data[i[0]] = ""
for j in stream[start+1+length[1]:end]:
data[i[0]] += chr(j)
elif i[1] == dataTypes.BYTE: elif i[1] == dataTypes.BYTE:
end = start+1 end = start+1
elif i[1] == dataTypes.UINT16 or i[1] == dataTypes.SINT16: elif i[1] == dataTypes.UINT16 or i[1] == dataTypes.SINT16:

View File

@@ -17,6 +17,7 @@ from objects import glob
def dispose(): def dispose():
""" """
Perform some clean up. Called on shutdown. Perform some clean up. Called on shutdown.
:return: :return:
""" """
print("> Disposing server... ") print("> Disposing server... ")
@@ -27,7 +28,7 @@ def runningUnderUnix():
""" """
Get if the server is running under UNIX or NT Get if the server is running under UNIX or NT
return --- True if running under UNIX, otherwise False :return: True if running under UNIX, otherwise False
""" """
return True if os.name == "posix" else False return True if os.name == "posix" else False
@@ -35,9 +36,11 @@ def scheduleShutdown(sendRestartTime, restart, message = "", delay=20):
""" """
Schedule a server shutdown/restart Schedule a server shutdown/restart
sendRestartTime -- time (seconds) to wait before sending server restart packets to every client :param sendRestartTime: time (seconds) to wait before sending server restart packets to every client
restart -- if True, server will restart. if False, server will shudown :param restart: if True, server will restart. if False, server will shudown
message -- if set, send that message to every client to warn about the shutdown/restart :param message: if set, send that message to every client to warn about the shutdown/restart
:param delay: additional restart delay in seconds. Default: 20
:return:
""" """
# Console output # Console output
log.info("Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+delay)) log.info("Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+delay))
@@ -61,13 +64,21 @@ def scheduleShutdown(sendRestartTime, restart, message = "", delay=20):
threading.Timer(sendRestartTime+delay, action).start() threading.Timer(sendRestartTime+delay, action).start()
def restartServer(): def restartServer():
"""Restart pep.py script""" """
Restart pep.py
:return:
"""
log.info("Restarting pep.py...") log.info("Restarting pep.py...")
dispose() dispose()
os.execv(sys.executable, [sys.executable] + sys.argv) os.execv(sys.executable, [sys.executable] + sys.argv)
def shutdownServer(): def shutdownServer():
"""Shutdown pep.py""" """
Shutdown pep.py
:return:
"""
log.info("Shutting down pep.py...") log.info("Shutting down pep.py...")
dispose() dispose()
sig = signal.SIGKILL if runningUnderUnix() else signal.CTRL_C_EVENT sig = signal.SIGKILL if runningUnderUnix() else signal.CTRL_C_EVENT
@@ -77,7 +88,7 @@ def getSystemInfo():
""" """
Get a dictionary with some system/server info Get a dictionary with some system/server info
return -- ["unix", "connectedUsers", "webServer", "cpuUsage", "totalMemory", "usedMemory", "loadAverage"] :return: ["unix", "connectedUsers", "webServer", "cpuUsage", "totalMemory", "usedMemory", "loadAverage"]
""" """
data = {"unix": runningUnderUnix(), "connectedUsers": len(glob.tokens.tokens), "matches": len(glob.matches.matches)} data = {"unix": runningUnderUnix(), "connectedUsers": len(glob.tokens.tokens), "matches": len(glob.matches.matches)}

View File

@@ -22,18 +22,15 @@ from objects import glob
class Client: class Client:
"""
IRC Client object
"""
__linesep_regexp = re.compile(r"\r?\n") __linesep_regexp = re.compile(r"\r?\n")
def __init__(self, server, sock): def __init__(self, server, sock):
""" """
Initialize a Client object Initialize a Client object
server -- server object :param server: server object
sock -- socket connection object :param sock: socket connection object
:return:
""" """
self.__timestamp = time.time() self.__timestamp = time.time()
self.__readbuffer = "" self.__readbuffer = ""
@@ -60,7 +57,8 @@ class Client:
Add a message (basic string) to client buffer. Add a message (basic string) to client buffer.
This is the lowest possible level. This is the lowest possible level.
msg -- message to add :param msg: message to add
:return:
""" """
self.__writebuffer += msg + "\r\n" self.__writebuffer += msg + "\r\n"
@@ -69,7 +67,7 @@ class Client:
""" """
Return this client's write buffer size Return this client's write buffer size
return -- write buffer size :return: write buffer size
""" """
return len(self.__writebuffer) return len(self.__writebuffer)
@@ -78,7 +76,8 @@ class Client:
""" """
Add an IRC-like message to client buffer. Add an IRC-like message to client buffer.
msg -- message (without IRC stuff) :param msg: message (without IRC stuff)
:return:
""" """
self.message(":{} {}".format(self.server.host, msg)) self.message(":{} {}".format(self.server.host, msg))
@@ -87,10 +86,11 @@ class Client:
""" """
Add an IRC-like message to client buffer with code Add an IRC-like message to client buffer with code
code -- response code :param code: response code
message -- response message :param message: response message
nickname -- receiver nickname :param nickname: receiver nickname
channel -- optional :param channel: optional
:return:
""" """
if nickname == "": if nickname == "":
nickname = self.IRCUsername nickname = self.IRCUsername
@@ -103,7 +103,8 @@ class Client:
""" """
Add a 403 reply (no such channel) to client buffer. Add a 403 reply (no such channel) to client buffer.
channel -- meh :param channel:
:return:
""" """
self.replyCode(403, "{} :No such channel".format(channel)) self.replyCode(403, "{} :No such channel".format(channel))
@@ -112,7 +113,8 @@ class Client:
""" """
Add a 461 reply (not enough parameters) to client buffer Add a 461 reply (not enough parameters) to client buffer
command -- command that had not enough parameters :param command: name of the command that had not enough parameters
:return:
""" """
self.replyCode(403, "{} :Not enough parameters".format(command)) self.replyCode(403, "{} :Not enough parameters".format(command))
@@ -121,8 +123,9 @@ class Client:
""" """
Disconnects this client from the IRC server Disconnects this client from the IRC server
quitmsg -- IRC quit message. Default: 'Client quit' :param quitmsg: IRC quit message. Default: 'Client quit'
callLogout -- if True, call logoutEvent on bancho :param callLogout: if True, call logoutEvent on bancho
:return:
""" """
# Send error to client and close socket # Send error to client and close socket
self.message("ERROR :{}".format(quitmsg)) self.message("ERROR :{}".format(quitmsg))
@@ -138,7 +141,11 @@ class Client:
def readSocket(self): def readSocket(self):
"""Read data coming from this client socket""" """
Read data coming from this client socket
:return:
"""
try: try:
# Try to read incoming data from socket # Try to read incoming data from socket
data = self.socket.recv(2 ** 10) data = self.socket.recv(2 ** 10)
@@ -161,7 +168,11 @@ class Client:
def parseBuffer(self): def parseBuffer(self):
"""Parse self.__readbuffer, get command, arguments and call its handler""" """
Parse self.__readbuffer, get command, arguments and call its handler
:return:
"""
# Get lines from buffer # Get lines from buffer
lines = self.__linesep_regexp.split(self.__readbuffer) lines = self.__linesep_regexp.split(self.__readbuffer)
self.__readbuffer = lines[-1] self.__readbuffer = lines[-1]
@@ -198,7 +209,11 @@ class Client:
def writeSocket(self): def writeSocket(self):
"""Write buffer to socket""" """
Write buffer to socket
:return:
"""
try: try:
sent = self.socket.send(self.__writebuffer.encode()) sent = self.socket.send(self.__writebuffer.encode())
log.debug("[IRC] [{}:{}] <- {}".format(self.ip, self.port, self.__writebuffer[:sent])) log.debug("[IRC] [{}:{}] <- {}".format(self.ip, self.port, self.__writebuffer[:sent]))
@@ -206,9 +221,13 @@ class Client:
except socket.error as x: except socket.error as x:
self.disconnect(str(x)) self.disconnect(str(x))
def checkAlive(self): def checkAlive(self):
"""Check if this client is still connected""" """
Check if this client is still connected.
If the client is dead, disconnect it.
:return:
"""
now = time.time() now = time.time()
if self.__timestamp + 180 < now: if self.__timestamp + 180 < now:
self.disconnect("ping timeout") self.disconnect("ping timeout")
@@ -224,11 +243,19 @@ class Client:
def sendLusers(self): def sendLusers(self):
"""Send lusers response to this client""" """
Send lusers response to this client
:return:
"""
self.replyCode(251, "There are {} users and 0 services on 1 server".format(len(glob.tokens.tokens))) self.replyCode(251, "There are {} users and 0 services on 1 server".format(len(glob.tokens.tokens)))
def sendMotd(self): def sendMotd(self):
"""Send MOTD to this client""" """
Send MOTD to this client
:return:
"""
self.replyCode(375, "- {} Message of the day - ".format(self.server.host)) self.replyCode(375, "- {} Message of the day - ".format(self.server.host))
if len(self.server.motd) == 0: if len(self.server.motd) == 0:
self.replyCode(422, "MOTD File is missing") self.replyCode(422, "MOTD File is missing")
@@ -296,7 +323,6 @@ class Client:
# Disconnect other IRC clients from the same user # Disconnect other IRC clients from the same user
for _, value in self.server.clients.items(): for _, value in self.server.clients.items():
if value.IRCUsername.lower() == self.IRCUsername.lower() and value != self: if value.IRCUsername.lower() == self.IRCUsername.lower() and value != self:
print("DISCONNECTERINOOOOOOOOOOOOOOOOOOOOO")
value.disconnect(quitmsg="Connected from another client") value.disconnect(quitmsg="Connected from another client")
return return
elif command == "USER": elif command == "USER":
@@ -341,13 +367,13 @@ class Client:
# TODO: Part all channels # TODO: Part all channels
if arguments[0] == "0": if arguments[0] == "0":
return
'''for (channelname, channel) in self.channels.items(): '''for (channelname, channel) in self.channels.items():
self.message_channel(channel, "PART", channelname, True) self.message_channel(channel, "PART", channelname, True)
self.channel_log(channel, "left", meta=True) self.channel_log(channel, "left", meta=True)
server.remove_member_from_channel(self, channelname) server.remove_member_from_channel(self, channelname)
self.channels = {} self.channels = {}
return''' return'''
return
# Get channels to join list # Get channels to join list
channels = arguments[0].split(",") channels = arguments[0].split(",")
@@ -376,7 +402,7 @@ class Client:
self.replyCode(332, description, channel=channel) self.replyCode(332, description, channel=channel)
# Build connected users list # Build connected users list
users = glob.channels.channels[channel].getConnectedUsers()[:] users = glob.channels.channels[channel].connectedUsers[:]
usernames = [] usernames = []
for user in users: for user in users:
token = glob.tokens.getTokenFromUserID(user) token = glob.tokens.getTokenFromUserID(user)
@@ -440,7 +466,6 @@ class Client:
# Send the message to bancho and reply # Send the message to bancho and reply
if not recipientIRC.startswith("#"): if not recipientIRC.startswith("#"):
print("PMPMPM!!!!!!!!!!")
recipientBancho = chat.fixUsernameForBancho(recipientIRC) recipientBancho = chat.fixUsernameForBancho(recipientIRC)
else: else:
recipientBancho = recipientIRC recipientBancho = recipientIRC
@@ -489,10 +514,17 @@ class Client:
"""(fake) PONG command handler""" """(fake) PONG command handler"""
pass pass
def awayHandler(self, command, arguments):
"""AWAY command handler"""
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")
def mainHandler(self, command, arguments): def mainHandler(self, command, arguments):
"""Handler for post-login commands""" """
Handler for post-login commands
"""
handlers = { handlers = {
#"AWAY": away_handler, "AWAY": self.awayHandler,
#"ISON": ison_handler, #"ISON": ison_handler,
"JOIN": self.joinHandler, "JOIN": self.joinHandler,
#"LIST": list_handler, #"LIST": list_handler,
@@ -518,24 +550,20 @@ class Client:
self.replyCode(421, "Unknown command ({})".format(command)) self.replyCode(421, "Unknown command ({})".format(command))
class Server: class Server:
def __init__(self, port): def __init__(self, port):
#self.host = socket.getfqdn("127.0.0.1")[:63]
self.host = glob.conf.config["irc"]["hostname"] self.host = glob.conf.config["irc"]["hostname"]
self.port = port self.port = port
self.clients = {} # Socket --> Client instance. self.clients = {} # Socket - - > Client instance.
self.motd = ["Welcome to pep.py's embedded IRC server!", "This is a VERY simple IRC server and it's still in beta.", "Expect things to crash and not work as expected :("] self.motd = ["Welcome to pep.py's embedded IRC server!", "This is a VERY simple IRC server and it's still in beta.", "Expect things to crash and not work as expected :("]
def forceDisconnection(self, username, isBanchoUsername=True): def forceDisconnection(self, username, isBanchoUsername=True):
""" """
Disconnect someone from IRC if connected Disconnect someone from IRC if connected
username -- victim :param username: victim
:param isBanchoUsername: if True, username is a bancho username, else convert it to a bancho username
:return:
""" """
for _, value in self.clients.items(): for _, value in self.clients.items():
if (value.IRCUsername == username and not isBanchoUsername) or (value.banchoUsername == username and isBanchoUsername): if (value.IRCUsername == username and not isBanchoUsername) or (value.banchoUsername == username and isBanchoUsername):
@@ -546,8 +574,9 @@ class Server:
""" """
Let every IRC client connected to a specific client know that 'username' joined the channel from bancho Let every IRC client connected to a specific client know that 'username' joined the channel from bancho
username -- username of bancho user :param username: username of bancho user
channel -- joined channel name :param channel: joined channel name
:return:
""" """
username = chat.fixUsernameForIRC(username) username = chat.fixUsernameForIRC(username)
for _, value in self.clients.items(): for _, value in self.clients.items():
@@ -558,8 +587,9 @@ class Server:
""" """
Let every IRC client connected to a specific client know that 'username' parted the channel from bancho Let every IRC client connected to a specific client know that 'username' parted the channel from bancho
username -- username of bancho user :param username: username of bancho user
channel -- joined channel name :param channel: joined channel name
:return:
""" """
username = chat.fixUsernameForIRC(username) username = chat.fixUsernameForIRC(username)
for _, value in self.clients.items(): for _, value in self.clients.items():
@@ -570,9 +600,10 @@ class Server:
""" """
Send a message to IRC when someone sends it from bancho Send a message to IRC when someone sends it from bancho
fro -- sender username :param fro: sender username
to -- receiver username :param to: receiver username
message -- text of the message :param message: text of the message
:return:
""" """
fro = chat.fixUsernameForIRC(fro) fro = chat.fixUsernameForIRC(fro)
to = chat.fixUsernameForIRC(to) to = chat.fixUsernameForIRC(to)
@@ -592,14 +623,19 @@ class Server:
""" """
Remove a client from connected clients Remove a client from connected clients
client -- client object :param client: client object
quitmsg -- QUIT argument, useless atm :param quitmsg: QUIT argument, useless atm
:return:
""" """
if client.socket in self.clients: if client.socket in self.clients:
del self.clients[client.socket] del self.clients[client.socket]
def start(self): def start(self):
"""Start IRC server main loop""" """
Start IRC server main loop
:return:
"""
# Sentry # Sentry
if glob.sentry: if glob.sentry:
sentryClient = raven.Client(glob.conf.config["sentry"]["ircdns"]) sentryClient = raven.Client(glob.conf.config["sentry"]["ircdns"])
@@ -622,7 +658,7 @@ class Server:
[x.socket for x in self.clients.values() [x.socket for x in self.clients.values()
if x.writeBufferSize() > 0], if x.writeBufferSize() > 0],
[], [],
2) 1)
# Handle incoming connections # Handle incoming connections
for x in iwtd: for x in iwtd:
@@ -656,5 +692,11 @@ class Server:
sentryClient.captureException() sentryClient.captureException()
def main(port=6667): def main(port=6667):
"""
Create and start an IRC server
:param port: IRC port. Default: 6667
:return:
"""
glob.ircServer = Server(port) glob.ircServer = Server(port)
glob.ircServer.start() glob.ircServer.start()

View File

@@ -1,5 +1,6 @@
# TODO: Rewrite this shit # TODO: Rewrite this shit
from common import generalUtils from common import generalUtils
from constants import serverPackets
from objects import glob from objects import glob
@@ -41,3 +42,20 @@ class banchoConfig:
""" """
self.config["banchoMaintenance"] = maintenance self.config["banchoMaintenance"] = maintenance
glob.db.execute("UPDATE bancho_settings SET value_int = %s WHERE name = 'bancho_maintenance'", [int(maintenance)]) glob.db.execute("UPDATE bancho_settings SET value_int = %s WHERE name = 'bancho_maintenance'", [int(maintenance)])
def reload(self):
# Reload settings from bancho_settings
glob.banchoConf.loadSettings()
# Reload channels too
glob.channels.loadChannels()
# And chat filters
glob.chatFilters.loadFilters()
# Send new channels and new bottom icon to everyone
glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
glob.streams.broadcast("main", serverPackets.channelInfoEnd())
for key, value in glob.channels.channels.items():
if value.publicRead == True and value.hidden == False:
glob.streams.broadcast("main", serverPackets.channelInfo(key))

View File

@@ -1,20 +1,16 @@
from objects import glob from objects import glob
class channel: class channel:
"""
A chat channel
"""
def __init__(self, name, description, publicRead, publicWrite, temp, hidden): def __init__(self, name, description, publicRead, publicWrite, temp, hidden):
""" """
Create a new chat channel object Create a new chat channel object
name -- channel name :param name: channel name
description -- channel description :param description: channel description
publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins :param publicRead: if True, this channel can be read by everyone. If False, it can be read only by mods/admins
publicWrite -- bool, same as public read but relative to write permissions :param publicWrite: same as public read, but regards writing permissions
temp -- if True, channel will be deleted when there's no one in the channel :param temp: if True, this channel will be deleted when there's no one in this channel
hidden -- if True, channel won't be shown in channels list :param hidden: if True, thic channel won't be shown in channels list
""" """
self.name = name self.name = name
self.description = description self.description = description
@@ -36,7 +32,8 @@ class channel:
""" """
Add a user to connected users Add a user to connected users
userID -- user ID that joined the channel :param userID:
:return:
""" """
if userID not in self.connectedUsers: if userID not in self.connectedUsers:
self.connectedUsers.append(userID) self.connectedUsers.append(userID)
@@ -45,7 +42,8 @@ class channel:
""" """
Remove a user from connected users Remove a user from connected users
userID -- user ID that left the channel :param userID:
:return:
""" """
if userID in self.connectedUsers: if userID in self.connectedUsers:
self.connectedUsers.remove(userID) self.connectedUsers.remove(userID)
@@ -53,20 +51,4 @@ class channel:
# Remove temp channels if empty or there's only fokabot connected # Remove temp channels if empty or there's only fokabot connected
l = len(self.connectedUsers) l = len(self.connectedUsers)
if self.temp == True and ((l == 0) or (l == 1 and 999 in self.connectedUsers)): if self.temp == True and ((l == 0) or (l == 1 and 999 in self.connectedUsers)):
glob.channels.removeChannel(self.name) glob.channels.removeChannel(self.name)
def getConnectedUsers(self):
"""
Get connected user IDs list
return -- connectedUsers list
"""
return self.connectedUsers
def getConnectedUsersCount(self):
"""
Count connected users
return -- connected users number
"""
return len(self.connectedUsers)

View File

@@ -4,18 +4,14 @@ from objects import glob
class channelList: class channelList:
""" def __init__(self):
Channel list self.channels = {}
channels -- dictionary. key: channel name, value: channel object
"""
channels = {}
def loadChannels(self): def loadChannels(self):
""" """
Load chat channels from db and add them to channels dictionary Load chat channels from db and add them to channels list
:return:
""" """
# Get channels from DB # Get channels from DB
channels = glob.db.fetchAll("SELECT * FROM bancho_channels") channels = glob.db.fetchAll("SELECT * FROM bancho_channels")
@@ -28,14 +24,15 @@ class channelList:
def addChannel(self, name, description, publicRead, publicWrite, temp = False, hidden = False): def addChannel(self, name, description, publicRead, publicWrite, temp = False, hidden = False):
""" """
Add a channel object to channels dictionary Add a channel to channels list
name -- channel name :param name: channel name
description -- channel description :param description: channel description
publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins :param publicRead: if True, this channel can be read by everyone. If False, it can be read only by mods/admins
publicWrite -- bool, same as public read but relative to write permissions :param publicWrite: same as public read, but regards writing permissions
temp -- if True, channel will be deleted when there's no one in the channel. Optional. Default = False. :param temp: if True, this channel will be deleted when there's no one in this channel
hidden -- if True, channel will be hidden in channels list. Optional. Default = False. :param hidden: if True, thic channel won't be shown in channels list
:return:
""" """
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))
@@ -45,8 +42,8 @@ class channelList:
Add a temporary channel (like #spectator or #multiplayer), gets deleted when there's no one in the channel Add a temporary channel (like #spectator or #multiplayer), gets deleted when there's no one in the channel
and it's hidden in channels list and it's hidden in channels list
name -- channel name :param name: channel name
return -- True if channel was created, False if failed :return: True if the channel was created, otherwise False
""" """
if name in self.channels: if name in self.channels:
return False return False
@@ -57,7 +54,8 @@ class channelList:
""" """
Removes a channel from channels list Removes a channel from channels list
name -- channel name :param name: channel name
:return:
""" """
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))

View File

@@ -1,9 +1,20 @@
class chatFilters: class chatFilters:
def __init__(self, fileName="filters.txt"): def __init__(self, fileName="filters.txt"):
"""
Initialize chat filters
:param fileName: name of the file containing filters. Default: filters.txt
"""
self.filters = {} self.filters = {}
self.loadFilters(fileName) self.loadFilters(fileName)
def loadFilters(self, fileName="filters.txt"): def loadFilters(self, fileName="filters.txt"):
"""
Load filters from a file
:param fileName: name of the file containing filters. Default: filters.txt
:return:
"""
# Reset chat filters # Reset chat filters
self.filters = {} self.filters = {}
@@ -19,6 +30,12 @@ class chatFilters:
self.filters[lineSplit[0].lower()] = lineSplit[1].replace("\n", "") self.filters[lineSplit[0].lower()] = lineSplit[1].replace("\n", "")
def filterMessage(self, message): def filterMessage(self, message):
"""
Replace forbidden words with filtered ones
:param message: normal message
:return: filtered message
"""
# Split words by spaces # Split words by spaces
messageTemp = message.split(" ") messageTemp = message.split(" ")

View File

@@ -12,29 +12,35 @@ from objects import glob
npRegex = re.compile("^https?:\\/\\/osu\\.ppy\\.sh\\/b\\/(\\d*)") npRegex = re.compile("^https?:\\/\\/osu\\.ppy\\.sh\\/b\\/(\\d*)")
def connect(): def connect():
"""Add FokaBot to connected users and send userpanel/stats packet to everyone""" """
Connect FokaBot to Bancho
:return:
"""
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))
glob.streams.broadcast("main", serverPackets.userStats(999)) glob.streams.broadcast("main", serverPackets.userStats(999))
def disconnect(): def disconnect():
"""Remove FokaBot from connected users""" """
Disconnect FokaBot from Bancho
:return:
"""
glob.tokens.deleteToken(glob.tokens.getTokenFromUserID(999)) glob.tokens.deleteToken(glob.tokens.getTokenFromUserID(999))
def fokabotResponse(fro, chan, message): def fokabotResponse(fro, chan, message):
""" """
Check if a message has triggered fokabot (and return its response) Check if a message has triggered FokaBot
fro -- sender username (for permissions stuff with admin commands) :param fro: sender username
chan -- channel name :param chan: channel name (or receiver username)
message -- message :param message: chat mesage
:return: FokaBot's response or False if no response
return -- fokabot's response string or False
""" """
for i in fokabotCommands.commands: for i in fokabotCommands.commands:
# Loop though all commands # Loop though all commands
#if i["trigger"] in message:
if generalUtils.strContains(message, i["trigger"]): if generalUtils.strContains(message, i["trigger"]):
# message has triggered a command # message has triggered a command

View File

@@ -1,7 +1,6 @@
"""Global objects and variables""" """Global objects and variables"""
import time import time
from common.ddog import datadogClient from common.ddog import datadogClient
from common.files import fileBuffer, fileLocks from common.files import fileBuffer, fileLocks
from objects import channelList from objects import channelList
@@ -14,27 +13,25 @@ try:
with open("version") as f: with open("version") as f:
VERSION = f.read() VERSION = f.read()
if VERSION == "": if VERSION == "":
raise raise Exception
except: except:
VERSION = "¯\_(xd)_/¯" VERSION = "Unknown"
DATADOG_PREFIX = "peppy" DATADOG_PREFIX = "peppy"
application = None application = None
db = None db = None
redis = None
conf = None conf = None
banchoConf = None banchoConf = None
tokens = tokenList.tokenList() tokens = tokenList.tokenList()
channels = channelList.channelList() channels = channelList.channelList()
matches = matchList.matchList() matches = matchList.matchList()
restarting = False
fLocks = fileLocks.fileLocks() fLocks = fileLocks.fileLocks()
fileBuffers = fileBuffer.buffersList() fileBuffers = fileBuffer.buffersList()
schiavo = schiavo.schiavo() schiavo = schiavo.schiavo()
dog = datadogClient.datadogClient() dog = datadogClient.datadogClient()
verifiedCache = {} verifiedCache = {}
cloudflare = False
chatFilters = None chatFilters = None
userIDCache = {}
pool = None pool = None
ircServer = None ircServer = None
busyThreads = 0 busyThreads = 0
@@ -46,8 +43,8 @@ gzip = False
localize = False localize = False
sentry = False sentry = False
irc = False irc = False
restarting = False
startTime = int(time.time()) startTime = int(time.time())
streams = streamList.streamList() streams = streamList.streamList()

View File

@@ -13,7 +13,7 @@ from objects import glob
class slot: class slot:
def __init__(self): def __init__(self):
self.status = slotStatuses.free self.status = slotStatuses.FREE
self.team = 0 self.team = 0
self.userID = -1 self.userID = -1
self.user = None self.user = None
@@ -23,19 +23,18 @@ class slot:
self.complete = False self.complete = False
class match: class match:
"""Multiplayer match object"""
def __init__(self, matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID): def __init__(self, matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID):
""" """
Create a new match object Create a new match object
matchID -- match progressive identifier :param matchID: match progressive identifier
matchName -- match name, string :param matchName: match name, string
matchPassword -- match md5 password. Leave empty for no password :param matchPassword: match md5 password. Leave empty for no password
beatmapID -- beatmap ID :param beatmapID: beatmap ID
beatmapName -- beatmap name, string :param beatmapName: beatmap name, string
beatmapMD5 -- beatmap md5 hash, string :param beatmapMD5: beatmap md5 hash, string
gameMode -- game mode ID. See gameModes.py :param gameMode: game mode ID. See gameModes.py
hostUserID -- user id of the host :param hostUserID: user id of the host
""" """
self.matchID = matchID self.matchID = matchID
self.streamName = "multi/{}".format(self.matchID) self.streamName = "multi/{}".format(self.matchID)
@@ -49,9 +48,9 @@ class match:
self.beatmapMD5 = beatmapMD5 self.beatmapMD5 = beatmapMD5
self.hostUserID = hostUserID self.hostUserID = hostUserID
self.gameMode = gameMode self.gameMode = gameMode
self.matchScoringType = matchScoringTypes.score # default values self.matchScoringType = matchScoringTypes.SCORE # default values
self.matchTeamType = matchTeamTypes.headToHead # default value self.matchTeamType = matchTeamTypes.HEAD_TO_HEAD # default value
self.matchModMode = matchModModes.normal # default value self.matchModMode = matchModModes.NORMAL # default value
self.seed = 0 self.seed = 0
self.matchDataCache = bytes() self.matchDataCache = bytes()
@@ -70,6 +69,8 @@ class match:
def getMatchData(self): def getMatchData(self):
""" """
Return binary match data structure for packetHelper Return binary match data structure for packetHelper
: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
@@ -109,7 +110,7 @@ class match:
]) ])
# Slot mods if free mod is enabled # Slot mods if free mod is enabled
if safeMatch.matchModMode == matchModModes.freeMod: if safeMatch.matchModMode == matchModModes.FREE_MOD:
for i in range(0,16): for i in range(0,16):
struct.append([safeMatch.slots[i].mods, dataTypes.UINT32]) struct.append([safeMatch.slots[i].mods, dataTypes.UINT32])
@@ -123,7 +124,8 @@ class match:
""" """
Set room host to newHost and send him host packet Set room host to newHost and send him host packet
newHost -- new host userID :param newHost: new host userID
:return:
""" """
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:
@@ -135,7 +137,21 @@ class match:
log.info("MPROOM{}: {} is now the host".format(self.matchID, token.username)) log.info("MPROOM{}: {} is now the host".format(self.matchID, token.username))
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):
#self.setSlot(i, slotStatuses.notReady, 0, user, 0) """
Set data for a specific slot.
All fields but slotID are optional.
Skipped fields won't be edited.
:param slotID: slot ID
:param status: new status
:param team: new team
:param user: new user id
:param mods: new mods
:param loaded: new loaded status
:param skip: new skip value
:param complete: new completed value
:return:
"""
if status is not None: if status is not None:
self.slots[slotID].status = status self.slots[slotID].status = status
@@ -161,8 +177,9 @@ class match:
""" """
Set slotID mods. Same as calling setSlot and then sendUpdate Set slotID mods. Same as calling setSlot and then sendUpdate
slotID -- slot number :param slotID: slot number
mods -- new mods :param mods: new mods
:return:
""" """
# Set new slot data and send update # Set new slot data and send update
self.setSlot(slotID, mods=mods) self.setSlot(slotID, mods=mods)
@@ -174,14 +191,15 @@ class match:
Switch slotID ready/not ready status Switch slotID ready/not ready status
Same as calling setSlot and then sendUpdate Same as calling setSlot and then sendUpdate
slotID -- slot number :param slotID: slot number
:return:
""" """
# Update ready status and setnd update # Update ready status and setnd update
oldStatus = self.slots[slotID].status oldStatus = self.slots[slotID].status
if oldStatus == slotStatuses.ready: if oldStatus == slotStatuses.READY:
newStatus = slotStatuses.notReady newStatus = slotStatuses.NOT_READY
else: else:
newStatus = slotStatuses.ready newStatus = slotStatuses.READY
self.setSlot(slotID, newStatus) self.setSlot(slotID, newStatus)
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))
@@ -191,13 +209,14 @@ class match:
Lock a slot Lock a slot
Same as calling setSlot and then sendUpdate Same as calling setSlot and then sendUpdate
slotID -- slot number :param slotID: slot number
:return:
""" """
# Check if slot is already locked # Check if slot is already locked
if self.slots[slotID].status == slotStatuses.locked: if self.slots[slotID].status == slotStatuses.LOCKED:
newStatus = slotStatuses.free newStatus = slotStatuses.FREE
else: else:
newStatus = slotStatuses.locked newStatus = slotStatuses.LOCKED
# Send updated settings to kicked user, so he returns to 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:
@@ -208,13 +227,14 @@ class match:
# Send updates to everyone else # Send updates to everyone else
self.sendUpdates() self.sendUpdates()
log.info("MPROOM{}: Slot{} {}".format(self.matchID, slotID, "locked" if newStatus == slotStatuses.locked else "unlocked")) log.info("MPROOM{}: Slot{} {}".format(self.matchID, slotID, "locked" if newStatus == slotStatuses.LOCKED else "unlocked"))
def playerLoaded(self, userID): def playerLoaded(self, userID):
""" """
Set a player loaded status to True Set a player loaded status to True
userID -- ID of user :param userID: ID of user
:return:
""" """
slotID = self.getUserSlotID(userID) slotID = self.getUserSlotID(userID)
if slotID is None: if slotID is None:
@@ -228,7 +248,7 @@ class match:
total = 0 total = 0
loaded = 0 loaded = 0
for i in range(0,16): for i in range(0,16):
if self.slots[i].status == slotStatuses.playing: if self.slots[i].status == slotStatuses.PLAYING:
total+=1 total+=1
if self.slots[i].loaded: if self.slots[i].loaded:
loaded+=1 loaded+=1
@@ -237,7 +257,11 @@ class match:
self.allPlayersLoaded() self.allPlayersLoaded()
def allPlayersLoaded(self): def allPlayersLoaded(self):
"""Send allPlayersLoaded packet to every playing usr in match""" """
Send allPlayersLoaded packet to every playing usr in match
:return:
"""
glob.streams.broadcast(self.playingStreamName, serverPackets.allPlayersLoaded()) glob.streams.broadcast(self.playingStreamName, serverPackets.allPlayersLoaded())
log.info("MPROOM{}: All players loaded! Match starting...".format(self.matchID)) log.info("MPROOM{}: All players loaded! Match starting...".format(self.matchID))
@@ -245,7 +269,8 @@ class match:
""" """
Set a player skip status to True Set a player skip status to True
userID -- ID of user :param userID: ID of user
:return:
""" """
slotID = self.getUserSlotID(userID) slotID = self.getUserSlotID(userID)
if slotID is None: if slotID is None:
@@ -263,7 +288,7 @@ class match:
total = 0 total = 0
skipped = 0 skipped = 0
for i in range(0,16): for i in range(0,16):
if self.slots[i].status == slotStatuses.playing: if self.slots[i].status == slotStatuses.PLAYING:
total+=1 total+=1
if self.slots[i].skip: if self.slots[i].skip:
skipped+=1 skipped+=1
@@ -272,7 +297,11 @@ class match:
self.allPlayersSkipped() self.allPlayersSkipped()
def allPlayersSkipped(self): def allPlayersSkipped(self):
"""Send allPlayersSkipped packet to every playing usr in match""" """
Send allPlayersSkipped packet to every playing usr in match
:return:
"""
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))
@@ -280,7 +309,7 @@ class match:
""" """
Set userID's slot completed to True Set userID's slot completed to True
userID -- ID of user :param userID: ID of user
""" """
slotID = self.getUserSlotID(userID) slotID = self.getUserSlotID(userID)
if slotID is None: if slotID is None:
@@ -294,7 +323,7 @@ class match:
total = 0 total = 0
completed = 0 completed = 0
for i in range(0,16): for i in range(0,16):
if self.slots[i].status == slotStatuses.playing: if self.slots[i].status == slotStatuses.PLAYING:
total+=1 total+=1
if self.slots[i].complete: if self.slots[i].complete:
completed+=1 completed+=1
@@ -303,15 +332,18 @@ class match:
self.allPlayersCompleted() self.allPlayersCompleted()
def allPlayersCompleted(self): def allPlayersCompleted(self):
"""Cleanup match stuff and send match end packet to everyone""" """
Cleanup match stuff and send match end packet to everyone
:return:
"""
# Reset inProgress # Reset inProgress
self.inProgress = False self.inProgress = False
# Reset slots # Reset slots
for i in range(0,16): for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.playing: if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
self.slots[i].status = slotStatuses.notReady self.slots[i].status = slotStatuses.NOT_READY
self.slots[i].loaded = False self.slots[i].loaded = False
self.slots[i].skip = False self.slots[i].skip = False
self.slots[i].complete = False self.slots[i].complete = False
@@ -332,7 +364,7 @@ class match:
""" """
Get slot ID occupied by userID Get slot ID occupied by userID
return -- slot id if found, None if user is not in room :return: slot id if found, None if user is not in room
""" """
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 and glob.tokens.tokens[self.slots[i].user].userID == userID: if self.slots[i].user is not None and self.slots[i].user in glob.tokens.tokens and glob.tokens.tokens[self.slots[i].user].userID == userID:
@@ -343,21 +375,20 @@ class match:
""" """
Add someone to users in match Add someone to users in match
userID -- user id 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
for i in range(0,16): for i in range(0,16):
if self.slots[i].user == user.token: if self.slots[i].user == user.token:
# Set bugged slot to free # Set bugged slot to free
self.setSlot(i, slotStatuses.free, 0, None, 0) self.setSlot(i, slotStatuses.FREE, 0, None, 0)
# Find first free slot # Find first free slot
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
self.setSlot(i, slotStatuses.notReady, 0, user.token, 0) self.setSlot(i, slotStatuses.NOT_READY, 0, user.token, 0)
# Send updated match data # Send updated match data
self.sendUpdates() self.sendUpdates()
@@ -372,7 +403,8 @@ class match:
""" """
Remove someone from users in match Remove someone from users in match
userID -- user if of the user :param userID: user if of the user
:return:
""" """
# Make sure the user is in room # Make sure the user is in room
slotID = self.getUserSlotID(user.userID) slotID = self.getUserSlotID(user.userID)
@@ -380,7 +412,7 @@ class match:
return return
# Set that slot to free # Set that slot to free
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: if self.countUsers() == 0:
@@ -407,8 +439,9 @@ class match:
""" """
Change userID slot to newSlotID Change userID slot to newSlotID
userID -- user that changed slot :param userID: user that changed slot
newSlotID -- slot id of new slot :param newSlotID: slot id of new slot
:return:
""" """
# Make sure the user is in room # Make sure the user is in room
oldSlotID = self.getUserSlotID(userID) oldSlotID = self.getUserSlotID(userID)
@@ -416,7 +449,7 @@ class match:
return 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 and self.slots[newSlotID].status != slotStatuses.free: if self.slots[newSlotID].user is not None and self.slots[newSlotID].status != slotStatuses.FREE:
return return
# Get old slot data # Get old slot data
@@ -424,7 +457,7 @@ class match:
oldData = copy.deepcopy(self.slots[oldSlotID]) oldData = copy.deepcopy(self.slots[oldSlotID])
# Free old slot # Free old slot
self.setSlot(oldSlotID, slotStatuses.free, 0, None, 0, False, False, False) self.setSlot(oldSlotID, slotStatuses.FREE, 0, None, 0, False, False, False)
# Occupy new slot # Occupy new slot
self.setSlot(newSlotID, oldData.status, oldData.team, oldData.user, oldData.mods) self.setSlot(newSlotID, oldData.status, oldData.team, oldData.user, oldData.mods)
@@ -439,13 +472,10 @@ class match:
""" """
Change match password to newPassword Change match password to newPassword
newPassword -- new password string :param newPassword: new password string
:return:
""" """
self.matchPassword = newPassword self.matchPassword = newPassword
#if newPassword != "":
# self.matchPassword = generalUtils.stringMd5(newPassword)
#else:
# self.matchPassword = ""
# Send password change to every user in match # Send password change to every user in match
glob.streams.broadcast(self.streamName, serverPackets.changeMatchPassword(self.matchPassword)) glob.streams.broadcast(self.streamName, serverPackets.changeMatchPassword(self.matchPassword))
@@ -460,7 +490,8 @@ class match:
""" """
Set match global mods Set match global mods
mods -- mods bitwise int thing :param mods: mods bitwise int thing
:return:
""" """
# Set new mods and send update # Set new mods and send update
self.mods = mods self.mods = mods
@@ -471,8 +502,9 @@ class match:
""" """
Set no beatmap status for userID Set no beatmap status for userID
userID -- ID of user :param userID: ID of user
has -- True if has beatmap, false if not :param has: True if has beatmap, false if not
:return:
""" """
# Make sure the user is in room # Make sure the user is in room
slotID = self.getUserSlotID(userID) slotID = self.getUserSlotID(userID)
@@ -480,7 +512,7 @@ class match:
return return
# Set slot # Set slot
self.setSlot(slotID, slotStatuses.noMap if not has else slotStatuses.notReady) self.setSlot(slotID, slotStatuses.NO_MAP if not has else slotStatuses.NOT_READY)
# Send updates # Send updates
self.sendUpdates() self.sendUpdates()
@@ -489,7 +521,8 @@ class match:
""" """
Transfer host to slotID Transfer host to slotID
slotID -- ID of slot :param slotID: ID of slot
:return:
""" """
# Make sure there is someone in that slot # Make sure there is someone in that slot
if self.slots[slotID].user is None or self.slots[slotID].user not in glob.tokens.tokens: if self.slots[slotID].user is None or self.slots[slotID].user not in glob.tokens.tokens:
@@ -505,7 +538,8 @@ class match:
""" """
Send userID's failed packet to everyone in match Send userID's failed packet to everyone in match
userID -- ID of user :param userID: ID of user
:return:
""" """
# Make sure the user is in room # Make sure the user is in room
slotID = self.getUserSlotID(userID) slotID = self.getUserSlotID(userID)
@@ -522,10 +556,10 @@ class match:
""" """
Fro invites to in this match. Fro invites to in this match.
fro -- sender userID :param fro: sender userID
to -- receiver userID :param to: receiver userID
:return:
""" """
# Get tokens # Get tokens
froToken = glob.tokens.getTokenFromUserID(fro) froToken = glob.tokens.getTokenFromUserID(fro)
toToken = glob.tokens.getTokenFromUserID(to) toToken = glob.tokens.getTokenFromUserID(to)
@@ -544,7 +578,7 @@ class match:
""" """
Return how many players are in that match Return how many players are in that match
return -- number of users :return: number of users
""" """
c = 0 c = 0
for i in range(0,16): for i in range(0,16):
@@ -556,7 +590,8 @@ class match:
""" """
Change userID's team Change userID's team
userID -- id of user :param userID: id of user
:return:
""" """
# Make sure the user is in room # Make sure the user is in room
slotID = self.getUserSlotID(userID) slotID = self.getUserSlotID(userID)
@@ -564,11 +599,16 @@ class match:
return return
# Update slot and send update # Update slot and send update
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()
def sendUpdates(self): def sendUpdates(self):
"""
Send match updates packet to everyone in lobby and room streams
:return:
"""
self.matchDataCache = serverPackets.updateMatch(self.matchID) self.matchDataCache = serverPackets.updateMatch(self.matchID)
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)
@@ -580,16 +620,17 @@ class match:
""" """
Check if match teams are valid Check if match teams are valid
return -- True if valid, False if invalid :return: True if valid, False if invalid
:return:
""" """
if self.matchTeamType != matchTeamTypes.teamVs or self.matchTeamType != matchTeamTypes.tagTeamVs: 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
# We have teams, check if they are valid # We have teams, check if they are valid
firstTeam = -1 firstTeam = -1
for i in range(0,16): for i in range(0,16):
if self.slots[i].user is not None and (self.slots[i].status & slotStatuses.noMap) == 0: if self.slots[i].user is not None and (self.slots[i].status & slotStatuses.NO_MAP) == 0:
if firstTeam == -1: if firstTeam == -1:
firstTeam = self.slots[i].team firstTeam = self.slots[i].team
elif firstTeam != self.slots[i].team: elif firstTeam != self.slots[i].team:
@@ -600,6 +641,11 @@ class match:
return False return False
def start(self): def start(self):
"""
Start the match
:return:
"""
# 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 return
@@ -613,8 +659,8 @@ class match:
# 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].status & slotStatuses.ready) > 0 and 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
self.slots[i].complete = False self.slots[i].complete = False

View File

@@ -13,14 +13,14 @@ class matchList:
""" """
Add a new match to matches list Add a new match to matches list
matchName -- match name, string :param matchName: match name, string
matchPassword -- match md5 password. Leave empty for no password :param matchPassword: match md5 password. Leave empty for no password
beatmapID -- beatmap ID :param beatmapID: beatmap ID
beatmapName -- beatmap name, string :param beatmapName: beatmap name, string
beatmapMD5 -- beatmap md5 hash, string :param beatmapMD5: beatmap md5 hash, string
gameMode -- game mode ID. See gameModes.py :param gameMode: game mode ID. See gameModes.py
hostUserID -- user id of who created the match :param hostUserID: user id of who created the match
return -- match ID :return: match ID
""" """
# 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
@@ -32,7 +32,8 @@ class matchList:
""" """
Destroy match object with id = matchID Destroy match object with id = matchID
matchID -- ID of match to dispose :param matchID: ID of match to dispose
:return:
""" """
# Make sure the match exists # Make sure the match exists
if matchID not in self.matches: if matchID not in self.matches:

View File

@@ -12,24 +12,26 @@ from objects import glob
class token: class token:
def __init__(self, userID, token_ = None, ip ="", irc = False, timeOffset = 0, tournament = False): def __init__(self, userID, token_ = None, ip ="", irc = False, timeOffset = 0, tournament = False):
""" """
Create a token object and set userID and token Create a token object and set userID and token
userID -- user associated to this token :param userID: user associated to this token
token -- if passed, set token to that value :param token_: if passed, set token to that value
if not passed, token will be generated if not passed, token will be generated
ip -- client ip. optional. :param ip: client ip. optional.
irc -- if True, set this token as IRC client. optional. :param irc: if True, set this token as IRC client. Default: False.
timeOffset -- the time offset from UTC for this user. optional. :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.
""" """
# Set stuff # Set stuff
self.userID = userID self.userID = userID
self.username = userUtils.getUsername(self.userID) self.username = userUtils.getUsername(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") or userUtils.isInPrivilegeGroup(self.userID, "community manager") self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer") or userUtils.isInPrivilegeGroup(self.userID, "community manager")
self.irc = irc self.irc = irc
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
@@ -51,6 +53,7 @@ class token:
self.country = 0 self.country = 0
self.location = [0,0] self.location = [0,0]
self.awayMessage = "" self.awayMessage = ""
self.sentAway = []
self.matchID = -1 self.matchID = -1
self.tillerino = [0,0,-1.0] # beatmap, mods, acc self.tillerino = [0,0,-1.0] # beatmap, mods, acc
self.silenceEndTime = 0 self.silenceEndTime = 0
@@ -93,25 +96,23 @@ class token:
""" """
Add bytes (packets) to queue Add bytes (packets) to queue
bytes -- (packet) bytes to enqueue :param bytes: (packet) bytes to enqueue
""" """
if not self.irc: # TODO: reduce max queue size
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))
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"""
self.queue = bytes() self.queue = bytes()
def joinChannel(self, channel): def joinChannel(self, channel):
""" """
Add channel to joined channels list Add channel to joined channels list
channel -- channel name :param channel: channel name
""" """
if channel not in self.joinedChannels: if channel not in self.joinedChannels:
self.joinedChannels.append(channel) self.joinedChannels.append(channel)
@@ -120,24 +121,24 @@ class token:
""" """
Remove channel from joined channels list Remove channel from joined channels list
channel -- channel name :param channel: channel name
""" """
if channel in self.joinedChannels: if channel in self.joinedChannels:
self.joinedChannels.remove(channel) self.joinedChannels.remove(channel)
def setLocation(self, location): def setLocation(self, latitude, longitude):
""" """
Set location (latitude and longitude) Set client location
location -- [latitude, longitude] :param location: [latitude, longitude]
""" """
self.location = location self.location = (latitude, longitude)
def getLatitude(self): def getLatitude(self):
""" """
Get latitude Get latitude
return -- latitude :return: latitude
""" """
return self.location[0] return self.location[0]
@@ -145,15 +146,16 @@ class token:
""" """
Get longitude Get longitude
return -- longitude :return: longitude
""" """
return self.location[1] return self.location[1]
def startSpectating(self, host): def startSpectating(self, host):
""" """
Set the spectating user to userID Set the spectating user to userID, join spectator stream and chat channel
and send required packets to host
user -- user object :param host: host osuToken object
""" """
# Stop spectating old client # Stop spectating old client
self.stopSpectating() self.stopSpectating()
@@ -193,6 +195,12 @@ class token:
log.info("{} is spectating {}".format(self.username, host.username)) log.info("{} is spectating {}".format(self.username, host.username))
def stopSpectating(self): def stopSpectating(self):
"""
Stop spectating, leave spectator stream and channel
and send required packets to host
:return:
"""
# Remove our userID from host's spectators # Remove our userID from host's spectators
if self.spectating is None: if self.spectating is None:
return return
@@ -231,36 +239,20 @@ class token:
self.spectating = None self.spectating = None
self.spectatingUserID = 0 self.spectatingUserID = 0
def setCountry(self, countryID):
"""
Set country to countryID
countryID -- numeric country ID. See countryHelper.py
"""
self.country = countryID
def getCountry(self):
"""
Get numeric country ID
return -- numeric country ID. See countryHelper.py
"""
return self.country
def updatePingTime(self): def updatePingTime(self):
"""Update latest ping time""" """
Update latest ping time to current time
:return:
"""
self.pingTime = int(time.time()) self.pingTime = int(time.time())
def setAwayMessage(self, __awayMessage):
"""Set a new away message"""
self.awayMessage = __awayMessage
def joinMatch(self, matchID): def joinMatch(self, matchID):
""" """
Set match to matchID, join match stream and channel Set match to matchID, join match stream and channel
matchID -- new match ID :param matchID: new match ID
:return:
""" """
# Make sure the match exists # Make sure the match exists
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
@@ -320,7 +312,10 @@ class token:
""" """
Kick this user from the server Kick this user from the server
message -- Notification message to send to this user. Optional. :param message: Notification message to send to this user.
Default: "You have been kicked from the server. Please login again."
:param reason: Kick reason, used in logs. Default: "kick"
:return:
""" """
# Send packet to target # Send packet to target
log.info("{} has been disconnected. ({})".format(self.username, reason)) log.info("{} has been disconnected. ({})".format(self.username, reason))
@@ -329,21 +324,28 @@ class token:
self.enqueue(serverPackets.loginFailed()) self.enqueue(serverPackets.loginFailed())
# Logout event # Logout event
logoutEvent.handle(self, None) logoutEvent.handle(self, deleteToken=False)
def silence(self, seconds, reason, author = 999): def silence(self, seconds = None, reason = "", author = 999):
""" """
Silences this user (db, packet and token) Silences this user (db, packet and token)
seconds -- silence length in seconds :param seconds: silence length in seconds. If None, get it from db. Default: None
reason -- silence reason :param reason: silence reason. Default: empty string
author -- userID of who has silenced the target. Optional. Default: 999 (fokabot) :param author: userID of who has silenced the user. Default: 999 (FokaBot)
:return:
""" """
# Silence in db and token if seconds is None:
self.silenceEndTime = int(time.time())+seconds # Get silence expire from db if needed
userUtils.silence(self.userID, seconds, reason, author) seconds = max(0, userUtils.getSilenceEnd(self.userID) - int(time.time()))
else:
# Silence in db and token
userUtils.silence(self.userID, seconds, reason, author)
# Send silence packet to target # Silence token
self.silenceEndTime = int(time.time()) + seconds
# Send silence packet to user
self.enqueue(serverPackets.silenceEndTime(seconds)) self.enqueue(serverPackets.silenceEndTime(seconds))
# Send silenced packet to everyone else # Send silenced packet to everyone else
@@ -353,7 +355,8 @@ class token:
""" """
Silences the user if is spamming. Silences the user if is spamming.
increaseSpamRate -- pass True if the user has sent a new message. Optional. Default: True :param increaseSpamRate: set to True if the user has sent a new message. Default: True
:return:
""" """
# Increase the spam rate if needed # Increase the spam rate if needed
if increaseSpamRate: if increaseSpamRate:
@@ -367,7 +370,7 @@ class token:
""" """
Returns True if this user is silenced, otherwise False Returns True if this user is silenced, otherwise False
return -- True/False :return: True if this user is silenced, otherwise False
""" """
return self.silenceEndTime-int(time.time()) > 0 return self.silenceEndTime-int(time.time()) > 0
@@ -376,12 +379,16 @@ class token:
Returns the seconds left for this user's silence Returns the seconds left for this user's silence
(0 if user is not silenced) (0 if user is not silenced)
return -- silence seconds left :return: silence seconds left (or 0)
""" """
return max(0, self.silenceEndTime-int(time.time())) return max(0, self.silenceEndTime-int(time.time()))
def updateCachedStats(self): def updateCachedStats(self):
"""Update all cached stats for this token""" """
Update all cached stats for this token
:return:
"""
stats = userUtils.getUserStats(self.userID, self.gameMode) stats = userUtils.getUserStats(self.userID, self.gameMode)
log.debug(str(stats)) log.debug(str(stats))
if stats is None: if stats is None:
@@ -394,36 +401,89 @@ class token:
self.gameRank = stats["gameRank"] self.gameRank = stats["gameRank"]
self.pp = stats["pp"] self.pp = stats["pp"]
def checkRestricted(self, force=False): def checkRestricted(self):
""" """
Check if this token is restricted. If so, send fokabot message Check if this token is restricted. If so, send fokabot message
force -- If True, get restricted value from db. :return:
If false, get the cached one. Optional. Default: False
""" """
if force: oldRestricted = self.restricted
self.restricted = userUtils.isRestricted(self.userID) self.restricted = userUtils.isRestricted(self.userID)
if self.restricted: if self.restricted:
self.setRestricted() self.setRestricted()
elif not self.restricted and oldRestricted != self.restricted:
self.resetRestricted()
def checkBanned(self):
"""
Check if this user is banned. If so, disconnect it.
:return:
"""
if userUtils.isBanned(self.userID):
self.enqueue(serverPackets.loginBanned())
logoutEvent.handle(self, deleteToken=False)
def setRestricted(self): def setRestricted(self):
""" """
Set this token as restricted, send FokaBot message to user Set this token as restricted, send FokaBot message to user
and send offline packet to everyone and send offline packet to everyone
:return:
""" """
self.restricted = True self.restricted = True
chat.sendMessage("FokaBot",self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.") chat.sendMessage("FokaBot", self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.")
def resetRestricted(self):
"""
Send FokaBot message to alert the user that he has been unrestricted
and he has to log in again.
:return:
"""
chat.sendMessage("FokaBot", self.username, "Your account has been unrestricted! Please log in again.")
def joinStream(self, name): def joinStream(self, name):
"""
Join a packet stream, or create it if the stream doesn't exist.
:param name: stream name
:return:
"""
glob.streams.join(name, token=self.token) glob.streams.join(name, token=self.token)
if name not in self.streams: if name not in self.streams:
self.streams.append(name) self.streams.append(name)
def leaveStream(self, name): def leaveStream(self, name):
"""
Leave a packets stream
:param name: stream name
:return:
"""
glob.streams.leave(name, token=self.token) glob.streams.leave(name, token=self.token)
if name in self.streams: if name in self.streams:
self.streams.remove(name) self.streams.remove(name)
def leaveAllStreams(self): def leaveAllStreams(self):
"""
Leave all joined packet streams
:return:
"""
for i in self.streams: for i in self.streams:
self.leaveStream(i) self.leaveStream(i)
def awayCheck(self, userID):
"""
Returns True if userID doesn't know that we are away
Returns False if we are not away or if userID already knows we are away
:param userID: original sender userID
:return:
"""
if self.awayMessage == "" or userID in self.sentAway:
return False
self.sentAway.append(userID)
return True

View File

@@ -8,52 +8,44 @@ from events import logoutEvent
from objects import glob from objects import glob
from objects import osuToken from objects import osuToken
class tokenList: class tokenList:
"""
List of connected osu tokens
tokens -- dictionary. key: token string, value: token object
"""
def __init__(self): def __init__(self):
"""
Initialize a tokens list
"""
self.tokens = {} self.tokens = {}
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
userID -- user id associated to that token :param userID: user id associated to that token
irc -- if True, set this token as IRC client :param irc: if True, set this token as IRC client
return -- token object :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.
:return: token object
""" """
newToken = osuToken.token(userID, ip=ip, irc=irc, timeOffset=timeOffset, tournament=tournament) newToken = osuToken.token(userID, ip=ip, irc=irc, timeOffset=timeOffset, tournament=tournament)
self.tokens[newToken.token] = newToken self.tokens[newToken.token] = newToken
glob.redis.incr("ripple:online_users")
return newToken return newToken
def deleteToken(self, token): def deleteToken(self, token):
""" """
Delete a token from token list if it exists Delete a token from token list if it exists
token -- token string :param token: token string
:return:
""" """
if token in self.tokens: if token in self.tokens:
# Delete session from DB
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)
# Pop token from list
self.tokens.pop(token) self.tokens.pop(token)
glob.redis.decr("ripple:online_users")
def getUserIDFromToken(self, token): def getUserIDFromToken(self, token):
""" """
Get user ID from a token Get user ID from a token
token -- token to find :param token: token to find
return -- false if not found, userID if found :return: False if not found, userID if found
""" """
# Make sure the token exists # Make sure the token exists
if token not in self.tokens: if token not in self.tokens:
@@ -66,8 +58,9 @@ class tokenList:
""" """
Get token from a user ID Get token from a user ID
userID -- user ID to find :param userID: user ID to find
return -- False if not found, token object if found :param ignoreIRC: if True, consider bancho clients only and skip IRC clients
:return: False if not found, token object if found
""" """
# Make sure the token exists # Make sure the token exists
for _, value in self.tokens.items(): for _, value in self.tokens.items():
@@ -79,19 +72,22 @@ class tokenList:
# Return none if not found # Return none if not found
return None return None
def getTokenFromUsername(self, username, ignoreIRC=False): def getTokenFromUsername(self, username, ignoreIRC=False, safe=False):
""" """
Get token from a username Get an osuToken object from an username
username -- username to find :param username: normal username or safe username
return -- False if not found, token object if found :param ignoreIRC: if True, consider bancho clients only and skip IRC clients
:param safe: if True, username is a safe username,
compare it with token's safe username rather than normal username
:return: osuToken object or None
""" """
# lowercase # lowercase
who = username.lower() who = username.lower() if not safe else username
# Make sure the token exists # Make sure the token exists
for _, value in self.tokens.items(): for _, value in self.tokens.items():
if value.username.lower() == 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
return value return value
@@ -103,22 +99,28 @@ class tokenList:
""" """
Delete old userID's tokens if found Delete old userID's tokens if found
userID -- tokens associated to this user will be deleted :param userID: tokens associated to this user will be deleted
:return:
""" """
# Delete older tokens # Delete older tokens
delete = []
for key, value in list(self.tokens.items()): for key, value in list(self.tokens.items()):
if value.userID == userID: if value.userID == userID:
# Delete this token from the dictionary # Delete this token from the dictionary
self.tokens[key].kick("You have logged in from somewhere else. You can't connect to Bancho/IRC from more than one device at the same time.", "kicked, multiple clients") #self.tokens[key].kick("You have logged in from somewhere else. You can't connect to Bancho/IRC from more than one device at the same time.", "kicked, multiple clients")
delete.append(self.tokens[key])
for i in delete:
logoutEvent.handle(i)
def multipleEnqueue(self, packet, who, but = False): def multipleEnqueue(self, packet, who, but = False):
""" """
Enqueue a packet to multiple users Enqueue a packet to multiple users
packet -- packet bytes to enqueue :param packet: packet bytes to enqueue
who -- userIDs array :param who: userIDs array
but -- if True, enqueue to everyone but users in who array :param but: if True, enqueue to everyone but users in `who` array
:return:
""" """
for _, value in self.tokens.items(): for _, value in self.tokens.items():
shouldEnqueue = False shouldEnqueue = False
@@ -134,19 +136,21 @@ class tokenList:
""" """
Enqueue packet(s) to every connected user Enqueue packet(s) to every connected user
packet -- packet bytes to enqueue :param packet: packet bytes to enqueue
:return:
""" """
for _, value in self.tokens.items(): for _, value in self.tokens.items():
value.enqueue(packet) value.enqueue(packet)
def usersTimeoutCheckLoop(self, timeoutTime = 100, checkTime = 100): def usersTimeoutCheckLoop(self, timeoutTime = 100, checkTime = 100):
""" """
Deletes all timed out users. Start timed out users disconnect loop.
If called once, will recall after 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!
timeoutTime - seconds of inactivity required to disconnect someone (Default: 100) :param timeoutTime: seconds of inactivity required to disconnect someone. Default: 100
checkTime - seconds between loops (Default: 100) :param checkTime: seconds between loops. Default: 100
:return:
""" """
log.debug("Checking timed out clients") log.debug("Checking timed out clients")
timedOutTokens = [] # timed out users timedOutTokens = [] # timed out users
@@ -170,8 +174,11 @@ class tokenList:
def spamProtectionResetLoop(self): def spamProtectionResetLoop(self):
""" """
Reset spam rate every 10 seconds. Start spam protection reset loop.
Called every 10 seconds.
CALL THIS FUNCTION ONLY ONCE! CALL THIS FUNCTION ONLY ONCE!
:return:
""" """
# Reset spamRate for every token # Reset spamRate for every token
for _, value in self.tokens.items(): for _, value in self.tokens.items():
@@ -182,22 +189,28 @@ class tokenList:
def deleteBanchoSessions(self): def deleteBanchoSessions(self):
""" """
Truncate bancho_sessions table. Remove all `peppy:sessions:*` redis keys.
Call at bancho startup to delete old cached sessions Call at bancho startup to delete old cached sessions
:return:
""" """
glob.db.execute("TRUNCATE TABLE bancho_sessions") try:
# TODO: Make function or some redis meme
glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:sessions:*")
except:
pass
def tokenExists(self, username = "", userID = -1): def tokenExists(self, username = "", userID = -1):
""" """
Check if a token exists (aka check if someone is connected) Check if a token exists
username -- Optional.
userID -- Optional.
return -- True if it exists, otherwise False
Use username or userid, not both at the same time. Use username or userid, not both at the same time.
:param username: Optional.
:param userID: Optional.
:return: True if it exists, otherwise False
""" """
if userID > -1: if userID > -1:
return True if self.getTokenFromUserID(userID) is not None else False return True if self.getTokenFromUserID(userID) is not None else False
else: else:
return True if self.getTokenFromUsername(username) is not None else False return True if self.getTokenFromUsername(username) is not None else False

65
pep.py
View File

@@ -9,13 +9,14 @@ import tornado.httpserver
import tornado.ioloop import tornado.ioloop
import tornado.web import tornado.web
from raven.contrib.tornado import AsyncSentryClient from raven.contrib.tornado import AsyncSentryClient
import redis
from common import generalUtils from common import generalUtils
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
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils from common.redis import pubSub
from common.web import schiavo from common.web import schiavo
from handlers import apiFokabotMessageHandler from handlers import apiFokabotMessageHandler
from handlers import apiIsOnlineHandler from handlers import apiIsOnlineHandler
@@ -33,6 +34,12 @@ from objects import banchoConfig
from objects import chatFilters from objects import chatFilters
from objects import fokabot from objects import fokabot
from objects import glob from objects import glob
from pubSubHandlers import changeUsernameHandler
from pubSubHandlers import disconnectHandler
from pubSubHandlers import banHandler
from pubSubHandlers import updateSilenceHandler
from pubSubHandlers import updateStatsHandler
def make_app(): def make_app():
@@ -82,7 +89,7 @@ if __name__ == "__main__":
# Connect to db # Connect to db
try: try:
consoleHelper.printNoNl("> Connecting to MySQL database...") consoleHelper.printNoNl("> Connecting to MySQL database... ")
glob.db = dbConnector.db(glob.conf.config["db"]["host"], glob.conf.config["db"]["username"], glob.conf.config["db"]["password"], glob.conf.config["db"]["database"], int(glob.conf.config["db"]["workers"])) glob.db = dbConnector.db(glob.conf.config["db"]["host"], glob.conf.config["db"]["username"], glob.conf.config["db"]["password"], glob.conf.config["db"]["database"], int(glob.conf.config["db"]["workers"]))
consoleHelper.printNoNl(" ") consoleHelper.printNoNl(" ")
consoleHelper.printDone() consoleHelper.printDone()
@@ -92,6 +99,31 @@ if __name__ == "__main__":
consoleHelper.printColored("[!] Error while connection to database. Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Error while connection to database. Please check your config.ini and run the server again", bcolors.RED)
raise raise
# Connect to redis
try:
consoleHelper.printNoNl("> Connecting to redis... ")
glob.redis = redis.Redis(glob.conf.config["redis"]["host"], glob.conf.config["redis"]["port"], glob.conf.config["redis"]["database"], glob.conf.config["redis"]["password"])
glob.redis.ping()
consoleHelper.printNoNl(" ")
consoleHelper.printDone()
except:
# Exception while connecting to db
consoleHelper.printError()
consoleHelper.printColored("[!] Error while connection to redis. Please check your config.ini and run the server again", bcolors.RED)
raise
# Empty redis cache
try:
# TODO: Make function or some redis meme
glob.redis.set("ripple:online_users", 0)
glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:*")
except redis.exceptions.ResponseError:
# Script returns error if there are no keys starting with peppy:*
pass
# Save peppy version in redis
glob.redis.set("peppy:version", glob.VERSION)
# Load bancho_settings # Load bancho_settings
try: try:
consoleHelper.printNoNl("> Loading bancho settings from DB... ") consoleHelper.printNoNl("> Loading bancho settings from DB... ")
@@ -151,11 +183,6 @@ if __name__ == "__main__":
glob.tokens.spamProtectionResetLoop() glob.tokens.spamProtectionResetLoop()
consoleHelper.printDone() consoleHelper.printDone()
# Cache user ids
consoleHelper.printNoNl("> Caching user IDs... ")
userUtils.cacheUserIDs()
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:
@@ -202,15 +229,23 @@ if __name__ == "__main__":
[ [
datadogClient.periodicCheck("online_users", lambda: len(glob.tokens.tokens)), datadogClient.periodicCheck("online_users", lambda: len(glob.tokens.tokens)),
datadogClient.periodicCheck("multiplayer_matches", lambda: len(glob.matches.matches)), datadogClient.periodicCheck("multiplayer_matches", lambda: len(glob.matches.matches)),
#datadogClient.periodicCheck("ram_clients", lambda: generalUtils.getTotalSize(glob.tokens)),
#datadogClient.periodicCheck("ram_matches", lambda: generalUtils.getTotalSize(glob.matches)),
#datadogClient.periodicCheck("ram_channels", lambda: generalUtils.getTotalSize(glob.channels)),
#datadogClient.periodicCheck("ram_file_buffers", lambda: generalUtils.getTotalSize(glob.fileBuffers)),
#datadogClient.periodicCheck("ram_file_locks", lambda: generalUtils.getTotalSize(glob.fLocks)),
#datadogClient.periodicCheck("ram_datadog", lambda: generalUtils.getTotalSize(glob.datadogClient)),
#datadogClient.periodicCheck("ram_verified_cache", lambda: generalUtils.getTotalSize(glob.verifiedCache)),
#datadogClient.periodicCheck("ram_irc", lambda: generalUtils.getTotalSize(glob.ircServer)),
#datadogClient.periodicCheck("ram_tornado", lambda: generalUtils.getTotalSize(glob.application)),
#datadogClient.periodicCheck("ram_db", lambda: generalUtils.getTotalSize(glob.db)),
]) ])
else: else:
consoleHelper.printColored("[!] Warning! Datadog stats tracking is disabled!", bcolors.YELLOW) consoleHelper.printColored("[!] Warning! Datadog stats tracking is disabled!", bcolors.YELLOW)
except: except:
consoleHelper.printColored("[!] Error while starting Datadog client! Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Error while starting Datadog client! Please check your config.ini and run the server again", bcolors.RED)
# Cloudflare memes
glob.cloudflare = generalUtils.stringToBool(glob.conf.config["server"]["cloudflare"])
# IRC start message and console output # IRC start message and console output
glob.irc = generalUtils.stringToBool(glob.conf.config["irc"]["enable"]) glob.irc = generalUtils.stringToBool(glob.conf.config["irc"]["enable"])
if glob.irc: if glob.irc:
@@ -235,6 +270,16 @@ if __name__ == "__main__":
log.logMessage("**pep.py** 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
pubSub.listener(glob.redis, {
"peppy:disconnect": disconnectHandler.handler(),
"peppy:change_username": changeUsernameHandler.handler(),
"peppy:reload_settings": lambda x: x == b"reload" and glob.banchoConf.reload(),
"peppy:update_cached_stats": updateStatsHandler.handler(),
"peppy:silence": updateSilenceHandler.handler(),
"peppy:ban": banHandler.handler(),
}).start()
# Start tornado # Start tornado
glob.application.listen(serverPort) glob.application.listen(serverPort)
tornado.ioloop.IOLoop.instance().start() tornado.ioloop.IOLoop.instance().start()

View File

View File

@@ -0,0 +1,18 @@
from common.redis import generalPubSubHandler
from common.ripple import userUtils
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.privileges = userUtils.getPrivileges(userID)
targetToken.checkBanned()
targetToken.checkRestricted()

View File

@@ -0,0 +1,49 @@
from common.redis import generalPubSubHandler
from common.ripple import userUtils
from common.log import logUtils as log
from common.constants import actions
from objects import glob
def handleUsernameChange(userID, newUsername, targetToken=None):
try:
userUtils.changeUsername(userID, newUsername=newUsername)
if targetToken is not None:
targetToken.kick("Your username has been changed to {}. Please log in again.".format(newUsername), "username_change")
except userUtils.usernameAlreadyInUseError:
log.rap(999, "Username change: {} is already in use!", through="Bancho")
if targetToken is not None:
targetToken.kick("There was a critical error while trying to change your username. Please contact a developer.", "username_change_fail")
except userUtils.invalidUsernameError:
log.rap(999, "Username change: {} is not a valid username!", through="Bancho")
if targetToken is not None:
targetToken.kick("There was a critical error while trying to change your username. Please contact a developer.", "username_change_fail")
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"newUsername": ""
}
def handle(self, data):
data = super().parseData(data)
if data is None:
return
# Get the user's token
targetToken = glob.tokens.getTokenFromUserID(data["userID"])
if targetToken is None:
# If the user is offline change username immediately
handleUsernameChange(data["userID"], data["newUsername"])
else:
if targetToken.irc or (targetToken.actionID != actions.PLAYING and targetToken.actionID != actions.MULTIPLAYING):
# If the user is online and he's connected through IRC or he's not playing,
# change username and kick the user immediately
handleUsernameChange(data["userID"], data["newUsername"], targetToken)
else:
# If the user is playing, delay the username change until he submits the score
# On submit modular, lets will send the username change request again
# through redis once the score has been submitted
# The check is performed on bancho logout too, so if the user disconnects
# without submitting a score, the username gets changed on bancho logout
glob.redis.set("ripple:change_username_pending:{}".format(data["userID"]), data["newUsername"])

View File

@@ -0,0 +1,18 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"reason": ""
}
def handle(self, data):
data = super().parseData(data)
if data is None:
return
targetToken = glob.tokens.getTokenFromUserID(data["userID"])
if targetToken is not None:
targetToken.kick(data["reason"], "pubsub_kick")

View File

@@ -0,0 +1,15 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.silence()

View File

@@ -0,0 +1,15 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.updateCachedStats()

View File

@@ -3,4 +3,7 @@ tornado
mysqlclient mysqlclient
psutil psutil
raven raven
bcrypt bcrypt>=3.1.1
dill
redis
cython

17
setup.py Normal file
View File

@@ -0,0 +1,17 @@
"""Cython build file"""
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import os
cythonExt = []
for root, dirs, files in os.walk(os.getcwd()):
for file in files:
if file.endswith(".pyx"):
filePath = os.path.relpath(os.path.join(root, file))
cythonExt.append(Extension(filePath.replace("/", ".")[:-4], [filePath]))
setup(
name = "pep.pyx modules",
ext_modules = cythonize(cythonExt, nthreads = 4),
)

View File

@@ -1 +1 @@
1.9.0 1.11.0