124 Commits

Author SHA1 Message Date
Sunpy
4291a69d54 Added raw packet builder command 2018-04-09 07:56:13 +02:00
Sunpy
2cd69a9a63 Merged with upstream 2018-04-08 21:20:26 +02:00
Giuseppe Guerra
ea4e2bd4fd Pin cython to 0.27.3 2018-04-03 12:40:45 +02:00
Giuseppe Guerra
508f6b507a Add .pyenv to .gitignore 2018-04-03 12:38:32 +02:00
Giuseppe Guerra
e89fbe7604 Pin requirements 2018-04-03 12:37:29 +02:00
Giuseppe Guerra
05694c5d87 Handle invalidUserException when calling fokabot commands 2018-04-02 18:19:47 +02:00
Giuseppe Guerra
198bdb9997 * 2018-04-02 12:15:22 +02:00
Giuseppe Guerra
473a2e1f2d 🔼 1.13.3 🔼 2018-03-23 21:38:24 +01:00
Giuseppe Guerra
a809008e55 Improvements to fokabot commands 2018-03-23 21:37:47 +01:00
Giuseppe Guerra
201593ea02 Handle empty arguments in fokabot !mp commands 2018-03-23 21:28:24 +01:00
Giuseppe Guerra
3275d31861 Handle matches with no name 2018-03-23 21:28:13 +01:00
Giuseppe Guerra
05eead0e1a Handle empty chat messages 2018-03-23 21:27:48 +01:00
Giuseppe Guerra
8bdc56faf6 🔼 1.13.2 🔼 2018-03-23 21:05:06 +01:00
Giuseppe Guerra
745a833aab Fix !mp close not working 2018-03-23 21:03:27 +01:00
Giuseppe Guerra
dcad5c5736 git.zxq.co -> zxq.co 2018-03-23 20:59:44 +01:00
Giuseppe Guerra
c1f8ca8ed3 Removed timeoutTime and checkTime args from usersTimeoutCheckLoop 2018-03-23 20:59:30 +01:00
Giuseppe Guerra
e0d54f49d1 Add empty match cleanup job 2018-03-23 20:59:04 +01:00
Sunpy
c2af1b9772 Updated submodules 2018-02-16 20:12:52 +01:00
Sunpy
a99bf0c74b Unhardcoded hardcoded code :) 2018-02-14 20:03:36 +01:00
Sunpy
f0fa00b181 Additional configuration 2018-02-14 19:59:43 +01:00
Sunpy
956aa8161f Updated submodules 2018-02-14 18:31:28 +01:00
Sunpy
08b367812f Use constant values in commands 2018-02-14 18:16:09 +01:00
Sunpy
0722c91018 Fixed mpClose function return message 2018-02-14 17:53:14 +01:00
Sunpy
f0e8223b5c Updated submodules 2018-02-14 17:52:27 +01:00
Sunpy
27a5f9c000 Use whatever account that is in id 999 as bot 2018-02-14 17:44:37 +01:00
Sunpy
29db61fd12 Updated submodules 2018-02-12 21:16:33 +01:00
Giuseppe Guerra
e63a85e4a4 I knew a threaded bancho server was going to be a bad idea... 2017-12-21 18:58:56 +01:00
Giuseppe Guerra
050c1d5fe8 Fix AiAe.exe willing to break stuff 2017-12-08 13:08:54 +01:00
Giuseppe Guerra
90cd4634fd Le melanzane fritte sono poco digeribili 2017-12-05 21:17:09 +01:00
Giuseppe Guerra
e3f1bc05e9 Changed !rtx privileges to ADMIN_KICK_USERS 2017-12-04 23:34:04 +01:00
Giuseppe Guerra
0939ec972c Merge remote-tracking branch 'origin/master' 2017-10-31 13:11:21 +01:00
Giuseppe Guerra
2ef89daf4c Force ASCII encoding in /api/v1/fokabotMessage 2017-10-31 13:11:01 +01:00
Morgan Bazalgette
bb8ccf8c85 make chat mods red, too 2017-10-09 15:24:34 +02:00
Giuseppe Guerra
dc90f506bd Fix np bug again 2017-09-22 22:57:21 +02:00
Giuseppe Guerra
2f9179362c Fix !last not returning pp for taiko scores 2017-09-22 22:46:40 +02:00
Giuseppe Guerra
3dfb1228ee Merge branch 'tourney-mp-room-changes' of ripple/pep.py into master 2017-09-09 12:48:30 +02:00
Morgan Bazalgette
c4123eb636 fix positional argument exception meme 2017-09-09 12:42:49 +02:00
Morgan Bazalgette
e3e46a34ec Notify the chat when the match has been completed. 2017-09-09 12:30:25 +02:00
Morgan Bazalgette
3ed837dc96 In tourney rooms, send a message in the chat when the ready status changes. 2017-09-09 12:25:51 +02:00
Nyo
48534bb551 Fix typo 🤔 2017-08-26 22:28:05 +02:00
goeo_
36d701eac1 Make the irc gateway not receive the last empty line on multiline messages. 2017-08-20 18:01:18 +03:00
goeo_
60fe2bc56e Make !mp close close the match even if the user is only in the match's channel and not in the match 2017-08-20 17:22:47 +03:00
goeo_
b8baddf694 Make the irc gateway compatible with multi-line messages 2017-08-20 17:10:15 +03:00
Giuseppe Guerra at an airport
018da5c0a2 Removed chat filters because goeo is a dickhead 2017-08-15 08:43:33 +02:00
Giuseppe Guerra at an airport
0a2ca07198 Bump version 2017-08-12 21:36:16 +02:00
Giuseppe Guerra at an airport
34e7a332e6 Add spect lock back 2017-08-12 21:29:27 +02:00
Giuseppe Guerra
8d97227965 Fix race condition while iterating over clients 2017-08-12 19:07:28 +02:00
Giuseppe Guerra at an airport
176775f8f3 Remove extra locks 2017-08-12 18:56:31 +02:00
Giuseppe Guerra at an airport
0329847477 Don't start spectating unexisting users if user id is negative 2017-08-12 08:56:39 +02:00
Giuseppe Guerra at an airport
b24b4ee88d Remove _streamLock 2017-08-12 08:49:55 +02:00
Giuseppe Guerra
e40acd335b Fix some commands not being triggered 2017-08-11 23:17:33 +02:00
Giuseppe Guerra
ce75f5ee99 Remove _chatLock and _internalLock 2017-08-11 22:19:39 +02:00
Giuseppe Guerra
29359ad4fd Fix various bugs with multiplayer teams and mp commands 2017-08-11 22:04:25 +02:00
Giuseppe Guerra
def4891008 Fix wrong commands being triggered if they have the same letters at the beginning 2017-08-11 01:36:25 +02:00
Giuseppe Guerra
5d73682a71 Fix username with spaces breaking commands 2017-08-11 01:33:14 +02:00
Giuseppe Guerra
0229fc4e65 Mp commands now require USER_TOURNAMENT_STAFF privilege 2017-08-11 01:19:30 +02:00
Giuseppe Guerra
05c4c89002 Merge branch 'master' of git.zxq.co:ripple/pep.py 2017-08-11 01:14:45 +02:00
Giuseppe Guerra
02b266f229 Add !mp settings 2017-08-11 01:14:21 +02:00
Giuseppe Guerra
612e808702 Add !mp start force, start game even for non-ready players, check if everyone is ready in !mp start 2017-08-11 00:57:58 +02:00
Giuseppe Guerra
466004f239 Add various locks in osuToken object 2017-08-11 00:45:44 +02:00
goeo_
5f279da6cf Send the api fail info and teams too when a mp match finishes. 2017-08-10 15:53:18 -04:00
Giuseppe Guerra
aa1887e2c4 Fix match teams not being changed 2017-08-08 00:56:39 +02:00
Giuseppe Guerra
df2a9bb13d Bump version to 1.13.0 2017-08-07 23:53:21 +02:00
Giuseppe Guerra
e2149d7d61 Fix wrong variable names 2017-08-07 23:53:09 +02:00
Giuseppe Guerra
ffc84448a2 Fix non-working packet 93, allow tournament-only packets only from tourney clients 2017-08-07 23:39:13 +02:00
Giuseppe Guerra
933c92e5f2 Allow tourney clients to send messages in chat, send match info to lobby after creating it with !mp make 2017-08-07 23:35:33 +02:00
Giuseppe Guerra
3bb1029832 Add !mp team 2017-08-07 23:21:49 +02:00
Giuseppe Guerra
ce889e608a Add !mp mods 2017-08-07 22:30:44 +02:00
Giuseppe Guerra
aeecccdd13 Add !mp password and !mp randompassword 2017-08-07 22:14:44 +02:00
Giuseppe Guerra
9425043b1e Add !mp kick 2017-08-07 21:48:09 +02:00
Giuseppe Guerra
f660a20f60 Changed !mp start countdown message 2017-08-07 21:41:48 +02:00
Giuseppe Guerra
5adc7f4261 Add !mp abort, fix match not being set as in progress when starting it 2017-08-07 21:38:18 +02:00
Giuseppe Guerra
3653447761 Add !mp set 2017-08-07 20:51:16 +02:00
Giuseppe Guerra
17aab9551e Add !mp map 2017-08-06 10:09:05 +02:00
Giuseppe Guerra
87d6186993 Add !mp invite 2017-08-06 09:45:39 +02:00
Giuseppe Guerra
5975e84f52 Add !mp start 2017-08-04 01:04:26 +02:00
Giuseppe Guerra
3309f2f8fd Add !mp host and !mp clearhost 2017-08-04 00:24:12 +02:00
Giuseppe Guerra
baa8ae4cc2 Changes !mp clear to !mp close 2017-08-04 00:10:26 +02:00
Giuseppe Guerra
837df03f05 Fix typo 2017-08-04 00:09:50 +02:00
Giuseppe Guerra
c2a2d9c97b Change minimum !mp size value from 1 to 2 2017-08-03 23:56:04 +02:00
Giuseppe Guerra
fc989a2705 Fix tourney chat channels get deleted when there's nobody in the channel 2017-08-03 23:55:26 +02:00
Giuseppe Guerra
310bc1d3b3 Add !mp size 2017-08-03 23:47:34 +02:00
Giuseppe Guerra
9f647d5f9e Add !switchserver command 2017-08-02 01:36:37 +02:00
Giuseppe Guerra
1e6ee91685 Add !mp lock and !mp unlock 2017-08-02 00:39:16 +02:00
Giuseppe Guerra
86995feb34 Alert users when entering tournament matches, make impossible to join tournament matches from ui 2017-08-02 00:29:12 +02:00
Giuseppe Guerra
af554c94d5 Add !mp make and !mp join 2017-08-02 00:22:57 +02:00
Giuseppe Guerra
3373bc9581 Fix foka responding to commands even if the command is not at the beginning of the message 2017-08-02 00:02:46 +02:00
goeo_
2cf1cdf1fd Send the api some data when a multiplayer match ends 2017-07-31 17:54:50 -04:00
Giuseppe Guerra
8c3fc6842d Release locks properly 2017-07-28 22:46:29 +02:00
Giuseppe Guerra
eab8bee828 Merge branch 'master' of git.zxq.co:ripple/pep.py 2017-07-28 22:36:19 +02:00
Giuseppe Guerra
66061d5fb2 Lock client buffers while writing 2017-07-28 22:32:30 +02:00
goeo_
401dd5ecdb Fix the multiplayer password bug while not breaking anything else, especially not multiplayer as a whole. (tested code) 2017-07-23 14:37:12 -04:00
goeo_
d439490029 I should get a proper development environment 2017-07-23 12:21:38 +02:00
goeo_
f4d0a2424d .HIDE. I suck at coding
does the hide tag even work lol
2017-07-23 12:17:57 +02:00
goeo_
a177e65fcf Silence check is already done in bancho; made the ban check look better
weirdly, the original bug report said silenced people could talk using irc
2017-07-23 12:16:29 +02:00
goeo_
c14c86fe0d Fixed The Underscore Bug (i think) 2017-07-23 03:40:51 +02:00
goeo_
25df2228e3 Finally tested the code and it works now 2017-07-23 02:58:47 +02:00
goeo_
941cf81877 Parenthesis
haha coding on windows sucks because the easiest way to test the code is to push the code then pull it from my dev server
2017-07-23 02:38:37 +02:00
goeo_
76bb15f9f3 Banned/Restricted people can't use the IRC gateway. Silenced people can't send messages on it. 2017-07-23 02:35:29 +02:00
goeo_
8043d686c5 Fix all multi passwords being sent to each user in lobby 2017-07-22 22:41:42 +02:00
Nyo
cae82bd107 Delete timed out tokens 2017-07-04 23:16:10 +02:00
goeo_
2c2c85b382 Trying to fix the user metadata problem. Will this kill performance? 2017-04-16 06:41:25 -04:00
Giuseppe Guerra
a0fdc6c292 1.12.1 2017-04-13 17:22:55 +02:00
Giuseppe Guerra
487f583083 Update submodules 2017-04-13 17:22:35 +02:00
Giuseppe Guerra
530d0c3b74 Update submodules 2017-04-12 21:11:05 +02:00
Giuseppe Guerra
46ec4f3704 Merge branch 'master' of zxq.co:ripple/pep.py 2017-04-11 18:18:19 +02:00
Giuseppe Guerra
0aa0ab0475 Update submodules, moved global leaderboards to redis 2017-04-11 18:18:07 +02:00
Giuseppe Guerra
1c2a29a88e Merge branch 'master' of AiAe/pep.py into master 2017-04-10 20:02:47 +02:00
Daniel
92e57aff28 Update 'filters.txt' 2017-04-10 19:21:41 +02:00
Giuseppe Guerra
7e535d7ed5 Remove peppy->nyo filter 2017-04-08 19:21:49 +02:00
Giuseppe Guerra
0464f713f7 Merge branch 'master' of AiAe/pep.py into master 2017-04-08 19:21:04 +02:00
Daniel
66776c60e0 ¯\_(ツ)_/¯ 2017-04-08 19:11:05 +02:00
Morgan Bazalgette
4ef98b5fc0 Howl porcoddio l'indentazione 2017-02-16 22:11:15 +01:00
Morgan Bazalgette
96add06b44 Add some more FAQs as requested by AiAe 2017-02-16 22:10:03 +01:00
Nyo
31971d4a8b .HIDE. Update submodules 2017-02-03 21:46:31 +01:00
Nyo
9175f9e7f2 .BANCHO. .FIX. Fix utf-8 characters not being displayed correctly in chatlogs 2017-01-17 19:24:12 +01:00
Nyo
784c4a11f1 .BANCHO. Switch from mirrorapi to cheesegull for beatmap update requests 2017-01-16 21:24:15 +01:00
Nyo
2407ecc3bf .HIDE. Change submodule url to new git server and use HTTPS instead of SSH 2017-01-16 19:39:54 +01:00
Nyo
9880c5004d .BANCHO. .FIX. Chat mods can now write in moderated channel and access #admin channel 2017-01-16 19:37:41 +01:00
Nyo
d2f111fd7b .HIDE. Update submodules 2017-01-10 19:55:43 +01:00
Nyo
e489221e39 .HIDE. Update submodules 2017-01-06 12:56:32 +01:00
Nyo
84f1fb566c .BANCHO. Removed dashes in when tracking notes for CM 2017-01-06 12:55:50 +01:00
Nyo
fc3736eba8 .BANCHO. Remove double prefix on bancho start schiavo message 2017-01-06 12:23:45 +01:00
Nyo
768913da59 .BANCHO. Allow !last only in PM 2017-01-06 12:01:07 +01:00
53 changed files with 1306 additions and 530 deletions

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ filters.txt
.idea .idea
redistest.py redistest.py
*.c *.c
*.so *.so
.pyenv

2
.gitmodules vendored
View File

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

2
common

Submodule common updated: 6329b9ac2d...94c7474e39

18
config.json Normal file
View File

@@ -0,0 +1,18 @@
{
"support-email": "support@ripple.moe",
"faq":
{
"rules": "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23].",
"swearing": "Please don't abuse swearing",
"spam": "Please don't spam",
"offend": "Please don't offend other players",
"github": "(Ripple's Github page!)[https://github.com/osuripple/ripple]",
"discord": "(Join Ripple's Discord!)[https://discord.gg/0rJcZruIsA6rXuIx]",
"blog": "You can find the latest Ripple news on the (blog)[https://ripple.moe/blog/]!",
"changelog": "Check the (changelog)[https://ripple.moe/index.php?p=17] !",
"status": "Check the server status (here!)[https://ripple.moe/index.php?p=27]",
"english": "Please keep this channel in english.",
"topic": "Can you please drop the topic and talk about something else?",
"lines": "Please try to keep your sentences on a single line to avoid getting silenced."
}
}

View File

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

View File

@@ -101,4 +101,7 @@ class missingReportInfoException(Exception):
pass pass
class invalidUserException(Exception): class invalidUserException(Exception):
pass
class wrongChannelException(Exception):
pass pass

View File

@@ -1,6 +1,7 @@
import json import json
import random import random
import re import re
import threading
import requests import requests
import time import time
@@ -9,7 +10,7 @@ from common import generalUtils
from common.constants import mods from common.constants import mods
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils from common.ripple import userUtils
from constants import exceptions from constants import exceptions, slotStatuses, matchModModes, matchTeams, matchTeamTypes
from common.constants import gameModes from common.constants import gameModes
from common.constants import privileges from common.constants import privileges
from constants import serverPackets from constants import serverPackets
@@ -17,6 +18,8 @@ from helpers import systemHelper
from objects import fokabot from objects import fokabot
from objects import glob from objects import glob
from helpers import chatHelper as chat from helpers import chatHelper as chat
from helpers import packetHelper
from common.web import cheesegull
""" """
Commands callbacks Commands callbacks
@@ -34,33 +37,14 @@ TODO: Change False to None, because False doesn't make any sense
""" """
def instantRestart(fro, chan, message): def instantRestart(fro, chan, message):
glob.streams.broadcast("main", serverPackets.notification("We are restarting Bancho. Be right back!")) glob.streams.broadcast("main", serverPackets.notification("We are restarting Bancho. Be right back!"))
systemHelper.scheduleShutdown(0, True, delay=1) systemHelper.scheduleShutdown(0, True, delay=5)
return False return False
def faq(fro, chan, message): def faq(fro, chan, message):
# TODO: Unhardcode this key = message[0].lower()
if message[0] == "rules": if key not in glob.conf.extra["faq"]:
return "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23]."
elif message[0] == "swearing":
return "Please don't abuse swearing"
elif message[0] == "spam":
return "Please don't spam"
elif message[0] == "offend":
return "Please don't offend other players"
elif message[0] == "github":
return "(Ripple's Github page!)[https://github.com/osuripple/ripple]"
elif message[0] == "discord":
return "(Join Ripple's Discord!)[https://discord.gg/0rJcZruIsA6rXuIx]"
elif message[0] == "blog":
return "You can find the latest Ripple news on the (blog)[https://ripple.moe/blog/]!"
elif message[0] == "changelog":
return "Check the (changelog)[https://ripple.moe/index.php?p=17] !"
elif message[0] == "status":
return "Check the server status (here!)[https://ripple.moe/index.php?p=27]"
elif message[0] == "english":
return "Please keep this channel in english."
else:
return False return False
return glob.conf.extra["faq"][key]
def roll(fro, chan, message): def roll(fro, chan, message):
maxPoints = 100 maxPoints = 100
@@ -75,15 +59,20 @@ def roll(fro, chan, message):
# return random.choice(["yes", "no", "maybe"]) # return random.choice(["yes", "no", "maybe"])
def alert(fro, chan, message): def alert(fro, chan, message):
glob.streams.broadcast("main", serverPackets.notification(' '.join(message[:]))) msg = ' '.join(message[:]).strip()
if not msg:
return False
glob.streams.broadcast("main", serverPackets.notification(msg))
return False return False
def alertUser(fro, chan, message): def alertUser(fro, chan, message):
target = message[0].replace("_", " ") target = message[0].lower()
targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
targetToken = glob.tokens.getTokenFromUsername(target)
if targetToken is not None: if targetToken is not None:
targetToken.enqueue(serverPackets.notification(' '.join(message[1:]))) msg = ' '.join(message[1:]).strip()
if not msg:
return False
targetToken.enqueue(serverPackets.notification(msg))
return False return False
else: else:
return "User offline." return "User offline."
@@ -109,9 +98,10 @@ def moderated(fro, chan, message):
def kickAll(fro, chan, message): def kickAll(fro, chan, message):
# Kick everyone but mods/admins # Kick everyone but mods/admins
toKick = [] toKick = []
for key, value in glob.tokens.tokens.items(): with glob.tokens:
if not value.admin: for key, value in glob.tokens.tokens.items():
toKick.append(key) if not value.admin:
toKick.append(key)
# Loop though users to kick (we can't change dictionary size while iterating) # Loop though users to kick (we can't change dictionary size while iterating)
for i in toKick: for i in toKick:
@@ -122,12 +112,12 @@ def kickAll(fro, chan, message):
def kick(fro, chan, message): def kick(fro, chan, message):
# Get parameters # Get parameters
target = message[0].lower().replace("_", " ") target = message[0].lower()
if target == "fokabot": if target == glob.BOT_NAME.lower():
return "Nope." return "Nope."
# Get target token and make sure is connected # Get target token and make sure is connected
tokens = glob.tokens.getTokenFromUsername(target, _all=True) tokens = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True, _all=True)
if len(tokens) == 0: if len(tokens) == 0:
return "{} is not online".format(target) return "{} is not online".format(target)
@@ -141,22 +131,25 @@ def kick(fro, chan, message):
def fokabotReconnect(fro, chan, message): def fokabotReconnect(fro, chan, message):
# Check if fokabot is already connected # Check if fokabot is already connected
if glob.tokens.getTokenFromUserID(999) is not None: if glob.tokens.getTokenFromUserID(999) is not None:
return "Fokabot is already connected to Bancho" return "{} is already connected to Bancho".format(glob.BOT_NAME)
# Fokabot is not connected, connect it # Fokabot is not connected, connect it
fokabot.connect() fokabot.connect()
return False return False
def silence(fro, chan, message): def silence(fro, chan, message):
for i in message: message = [x.lower() for x in message]
i = i.lower() target = message[0]
target = message[0].replace("_", " ")
amount = message[1] amount = message[1]
unit = message[2] unit = message[2]
reason = ' '.join(message[3:]) reason = ' '.join(message[3:]).strip()
if not reason:
return "Please provide a valid reason."
if not amount.isdigit():
return "The amount must be a number."
# Get target user ID # Get target user ID
targetUserID = userUtils.getID(target) targetUserID = userUtils.getIDSafe(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
# Make sure the user exists # Make sure the user exists
@@ -180,7 +173,7 @@ def silence(fro, chan, message):
return "Invalid silence time. Max silence time is 7 days." return "Invalid silence time. Max silence time is 7 days."
# Send silence packet to target if he's connected # Send silence packet to target if he's connected
targetToken = glob.tokens.getTokenFromUsername(target) targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
if targetToken is not None: if targetToken is not None:
# user online, silence both in db and with packet # user online, silence both in db and with packet
targetToken.silence(silenceTime, reason, userID) targetToken.silence(silenceTime, reason, userID)
@@ -196,16 +189,16 @@ def removeSilence(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0].replace("_", " ") target = message[0]
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getID(target) targetUserID = userUtils.getIDSafe(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
# Send new silence end packet to user if he's online # Send new silence end packet to user if he's online
targetToken = glob.tokens.getTokenFromUsername(target) targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
if targetToken is not None: if targetToken is not None:
# User online, remove silence both in db and with packet # User online, remove silence both in db and with packet
targetToken.silence(0, "", userID) targetToken.silence(0, "", userID)
@@ -219,10 +212,10 @@ def ban(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0].replace("_", " ") target = message[0]
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getID(target) targetUserID = userUtils.getIDSafe(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
@@ -231,7 +224,7 @@ def ban(fro, chan, message):
userUtils.ban(targetUserID) userUtils.ban(targetUserID)
# Send ban packet to the user if he's online # Send ban packet to the user if he's online
targetToken = glob.tokens.getTokenFromUsername(target) targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
if targetToken is not None: if targetToken is not None:
targetToken.enqueue(serverPackets.loginBanned()) targetToken.enqueue(serverPackets.loginBanned())
@@ -242,10 +235,10 @@ def unban(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0].replace("_", " ") target = message[0]
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getID(target) targetUserID = userUtils.getIDSafe(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
@@ -260,10 +253,10 @@ def restrict(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0].replace("_", " ") target = message[0]
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getID(target) targetUserID = userUtils.getIDSafe(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
@@ -272,7 +265,7 @@ def restrict(fro, chan, message):
userUtils.restrict(targetUserID) userUtils.restrict(targetUserID)
# Send restricted mode packet to this user if he's online # Send restricted mode packet to this user if he's online
targetToken = glob.tokens.getTokenFromUsername(target) targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
if targetToken is not None: if targetToken is not None:
targetToken.setRestricted() targetToken.setRestricted()
@@ -283,10 +276,10 @@ def unrestrict(fro, chan, message):
# Get parameters # Get parameters
for i in message: for i in message:
i = i.lower() i = i.lower()
target = message[0].replace("_", " ") target = message[0]
# Make sure the user exists # Make sure the user exists
targetUserID = userUtils.getID(target) targetUserID = userUtils.getIDSafe(target)
userID = userUtils.getID(fro) userID = userUtils.getID(fro)
if not targetUserID: if not targetUserID:
return "{}: user not found".format(target) return "{}: user not found".format(target)
@@ -331,9 +324,10 @@ def systemMaintenance(fro, chan, message):
who = [] who = []
# Disconnect everyone but mod/admins # Disconnect everyone but mod/admins
for _, value in glob.tokens.tokens.items(): with glob.tokens:
if not value.admin: for _, value in glob.tokens.tokens.items():
who.append(value.userID) if not value.admin:
who.append(value.userID)
glob.streams.broadcast("main", serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.")) glob.streams.broadcast("main", serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later."))
glob.tokens.multipleEnqueue(serverPackets.loginError(), who) glob.tokens.multipleEnqueue(serverPackets.loginError(), who)
@@ -396,7 +390,7 @@ def getPPMessage(userID, just_data = False):
# Make sure status is 200 # Make sure status is 200
if data["status"] != 200: if data["status"] != 200:
if "message" in data: if "message" in data:
return "Error in LETS API call ({}). Please tell this to a dev.".format(data["message"]) return "Error in LETS API call ({}).".format(data["message"])
else: else:
raise exceptions.apiException raise exceptions.apiException
@@ -432,7 +426,7 @@ def getPPMessage(userID, just_data = False):
return "API Timeout. Please try again in a few seconds." return "API Timeout. Please try again in a few seconds."
except exceptions.apiException: except exceptions.apiException:
# API error # API error
return "Unknown error in LETS API call. Please tell this to a dev." return "Unknown error in LETS API call."
#except: #except:
# Unknown exception # Unknown exception
# TODO: print exception # TODO: print exception
@@ -570,6 +564,10 @@ def tillerinoAcc(fro, chan, message):
def tillerinoLast(fro, chan, message): def tillerinoLast(fro, chan, message):
try: try:
# Run the command in PM only
if chan.startswith("#"):
return False
data = glob.db.fetch("""SELECT beatmaps.song_name as sn, scores.*, data = glob.db.fetch("""SELECT beatmaps.song_name as sn, scores.*,
beatmaps.beatmap_id as bid, beatmaps.difficulty_std, beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania, beatmaps.max_combo as fc beatmaps.beatmap_id as bid, beatmaps.difficulty_std, beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania, beatmaps.max_combo as fc
FROM scores FROM scores
@@ -585,11 +583,11 @@ def tillerinoLast(fro, chan, message):
rank = generalUtils.getRank(data["play_mode"], data["mods"], data["accuracy"], rank = generalUtils.getRank(data["play_mode"], data["mods"], data["accuracy"],
data["300_count"], data["100_count"], data["50_count"], data["misses_count"]) data["300_count"], data["100_count"], data["50_count"], data["misses_count"])
ifPlayer = "{0} | ".format(fro) if chan != "FokaBot" else "" ifPlayer = "{0} | ".format(fro) if chan != glob.BOT_NAME else ""
ifFc = " (FC)" if data["max_combo"] == data["fc"] else " {0}x/{1}x".format(data["max_combo"], data["fc"]) ifFc = " (FC)" if data["max_combo"] == data["fc"] else " {0}x/{1}x".format(data["max_combo"], data["fc"])
beatmapLink = "[http://osu.ppy.sh/b/{1} {0}]".format(data["sn"], data["bid"]) beatmapLink = "[http://osu.ppy.sh/b/{1} {0}]".format(data["sn"], data["bid"])
hasPP = data["play_mode"] == gameModes.STD or data["play_mode"] == gameModes.MANIA hasPP = data["play_mode"] != gameModes.CTB
msg = ifPlayer msg = ifPlayer
msg += beatmapLink msg += beatmapLink
@@ -677,23 +675,12 @@ def updateBeatmap(fro, chan, message):
if token.tillerino[0] == 0: if token.tillerino[0] == 0:
return "Please give me a beatmap first with /np command." return "Please give me a beatmap first with /np command."
# Send request # Send the request to cheesegull
beatmapData = glob.db.fetch("SELECT beatmapset_id, song_name FROM beatmaps WHERE beatmap_id = %s LIMIT 1", [token.tillerino[0]]) ok, message = cheesegull.updateBeatmap(token.tillerino[0])
if beatmapData is None: if ok:
return "Couldn't find beatmap data in database. Please load the beatmap's leaderboard and try again." return "An update request for that beatmap has been queued. Check back in a few minutes and the beatmap should be updated!"
response = requests.post("{}/api/v1/update_beatmap".format(glob.conf.config["mirror"]["url"]), {
"beatmap_set_id": beatmapData["beatmapset_id"],
"beatmap_name": beatmapData["song_name"],
"username": token.username,
"key": glob.conf.config["mirror"]["apikey"]
})
if response.status_code == 200:
return "An update request for that beatmap has been queued. You'll receive a message once the beatmap has been updated on our mirror!"
elif response.status_code == 429:
return "You are sending too many beatmaps update requests. Wait a bit and retry later."
else: else:
return "Error in beatmap mirror API request. Tell this to a dev: {}".format(response.text) return "Error in beatmap mirror API request: {}".format(message)
except: except:
return False return False
@@ -714,7 +701,7 @@ def report(fro, chan, message):
target = chat.fixUsernameForBancho(target) target = chat.fixUsernameForBancho(target)
# Make sure the target is not foka # Make sure the target is not foka
if target.lower() == "fokabot": if target.lower() == glob.BOT_NAME.lower():
raise exceptions.invalidUserException() raise exceptions.invalidUserException()
# Make sure the user exists # Make sure the user exists
@@ -728,7 +715,7 @@ def report(fro, chan, message):
# Get the token if possible # Get the token if possible
chatlog = "" chatlog = ""
token = glob.tokens.getTokenFromUsername(target) token = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
if token is not None: if token is not None:
chatlog = token.getMessagesBufferString() chatlog = token.getMessagesBufferString()
@@ -738,10 +725,10 @@ def report(fro, chan, message):
adminMsg = "{user} has reported {target} for {reason} ({info})".format(user=fro, target=target, reason=reason, info=additionalInfo) adminMsg = "{user} has reported {target} for {reason} ({info})".format(user=fro, target=target, reason=reason, info=additionalInfo)
# Log report in #admin and on discord # Log report in #admin and on discord
chat.sendMessage("FokaBot", "#admin", adminMsg) chat.sendMessage(glob.BOT_NAME, "#admin", adminMsg)
log.warning(adminMsg, discord="cm") log.warning(adminMsg, discord="cm")
except exceptions.invalidUserException: except exceptions.invalidUserException:
msg = "Hello, FokaBot here! You can't report me. I won't forget what you've tried to do. Watch out." msg = "Hello, {} here! You can't report me. I won't forget what you've tried to do. Watch out.".format(glob.BOT_NAME)
except exceptions.invalidArgumentsException: except exceptions.invalidArgumentsException:
msg = "Invalid report command syntax. To report an user, click on it and select 'Report user'." msg = "Invalid report command syntax. To report an user, click on it and select 'Report user'."
except exceptions.userNotFoundException: except exceptions.userNotFoundException:
@@ -755,11 +742,411 @@ def report(fro, chan, message):
token = glob.tokens.getTokenFromUsername(fro) token = glob.tokens.getTokenFromUsername(fro)
if token is not None: if token is not None:
if token.irc: if token.irc:
chat.sendMessage("FokaBot", fro, msg) chat.sendMessage(glob.BOT_NAME, fro, msg)
else: else:
token.enqueue(serverPackets.notification(msg)) token.enqueue(serverPackets.notification(msg))
return False return False
def multiplayer(fro, chan, message):
def getMatchIDFromChannel(chan):
if not chan.lower().startswith("#multi_"):
raise exceptions.wrongChannelException()
parts = chan.lower().split("_")
if len(parts) < 2 or not parts[1].isdigit():
raise exceptions.wrongChannelException()
matchID = int(parts[1])
if matchID not in glob.matches.matches:
raise exceptions.matchNotFoundException()
return matchID
def mpMake():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp make <name>")
matchName = " ".join(message[1:]).strip()
if not matchName:
raise exceptions.invalidArgumentsException("Match name must not be empty!")
matchID = glob.matches.createMatch(matchName, generalUtils.stringMd5(generalUtils.randomString(32)), 0, "Tournament", "", 0, -1, isTourney=True)
glob.matches.matches[matchID].sendUpdates()
return "Tourney match #{} created!".format(matchID)
def mpJoin():
if len(message) < 2 or not message[1].isdigit():
raise exceptions.invalidArgumentsException("Wrong syntax: !mp join <id>")
matchID = int(message[1])
userToken = glob.tokens.getTokenFromUsername(fro, ignoreIRC=True)
userToken.joinMatch(matchID)
return "Attempting to join match #{}!".format(matchID)
def mpClose():
matchID = getMatchIDFromChannel(chan)
glob.matches.disposeMatch(matchID)
return "Multiplayer match #{} disposed successfully".format(matchID)
def mpLock():
matchID = getMatchIDFromChannel(chan)
glob.matches.matches[matchID].isLocked = True
return "This match has been locked"
def mpUnlock():
matchID = getMatchIDFromChannel(chan)
glob.matches.matches[matchID].isLocked = False
return "This match has been unlocked"
def mpSize():
if len(message) < 2 or not message[1].isdigit() or int(message[1]) < 2 or int(message[1]) > 16:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp size <slots(2-16)>")
matchSize = int(message[1])
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.forceSize(matchSize)
return "Match size changed to {}".format(matchSize)
def mpMove():
if len(message) < 3 or not message[2].isdigit() or int(message[2]) < 0 or int(message[2]) > 16:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp move <username> <slot>")
username = message[1]
newSlotID = int(message[2])
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
success = _match.userChangeSlot(userID, newSlotID)
if success:
result = "Player {} moved to slot {}".format(username, newSlotID)
else:
result = "You can't use that slot: it's either already occupied by someone else or locked"
return result
def mpHost():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp host <username>")
username = message[1].strip()
if not username:
raise exceptions.invalidArgumentsException("Please provide a username")
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
success = _match.setHost(userID)
return "{} is now the host".format(username) if success else "Couldn't give host to {}".format(username)
def mpClearHost():
matchID = getMatchIDFromChannel(chan)
glob.matches.matches[matchID].removeHost()
return "Host has been removed from this match"
def mpStart():
def _start():
matchID = getMatchIDFromChannel(chan)
success = glob.matches.matches[matchID].start()
if not success:
chat.sendMessage(glob.BOT_NAME, chan, "Couldn't start match. Make sure there are enough players and "
"teams are valid. The match has been unlocked.")
else:
chat.sendMessage(glob.BOT_NAME, chan, "Have fun!")
def _decreaseTimer(t):
if t <= 0:
_start()
else:
if t % 10 == 0 or t <= 5:
chat.sendMessage(glob.BOT_NAME, chan, "Match starts in {} seconds.".format(t))
threading.Timer(1.00, _decreaseTimer, [t - 1]).start()
if len(message) < 2 or not message[1].isdigit():
startTime = 0
else:
startTime = int(message[1])
force = False if len(message) < 3 else message[2].lower() == "force"
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
# Force everyone to ready
someoneNotReady = False
for i, slot in enumerate(_match.slots):
if slot.status != slotStatuses.READY and slot.user is not None:
someoneNotReady = True
if force:
_match.toggleSlotReady(i)
if someoneNotReady and not force:
return "Some users aren't ready yet. Use '!mp start force' if you want to start the match, " \
"even with non-ready players."
if startTime == 0:
_start()
return "Starting match"
else:
_match.isStarting = True
threading.Timer(1.00, _decreaseTimer, [startTime - 1]).start()
return "Match starts in {} seconds. The match has been locked. " \
"Please don't leave the match during the countdown " \
"or you might receive a penalty.".format(startTime)
def mpInvite():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp invite <username>")
username = message[1].strip()
if not username:
raise exceptions.invalidArgumentsException("Please provide a username")
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
token = glob.tokens.getTokenFromUserID(userID, ignoreIRC=True)
if token is None:
raise exceptions.invalidUserException("That user is not connected to bancho right now.")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.invite(999, userID)
token.enqueue(serverPackets.notification("Please accept the invite you've just received from {} to "
"enter your tourney match.".format(glob.BOT_NAME)))
return "An invite to this match has been sent to {}".format(username)
def mpMap():
if len(message) < 2 or not message[1].isdigit() or (len(message) == 3 and not message[2].isdigit()):
raise exceptions.invalidArgumentsException("Wrong syntax: !mp map <beatmapid> [<gamemode>]")
beatmapID = int(message[1])
gameMode = int(message[2]) if len(message) == 3 else 0
if gameMode < 0 or gameMode > 3:
raise exceptions.invalidArgumentsException("Gamemode must be 0, 1, 2 or 3")
beatmapData = glob.db.fetch("SELECT * FROM beatmaps WHERE beatmap_id = %s LIMIT 1", [beatmapID])
if beatmapData is None:
raise exceptions.invalidArgumentsException("The beatmap you've selected couldn't be found in the database."
"If the beatmap id is valid, please load the scoreboard first in "
"order to cache it, then try again.")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.beatmapID = beatmapID
_match.beatmapName = beatmapData["song_name"]
_match.beatmapMD5 = beatmapData["beatmap_md5"]
_match.gameMode = gameMode
_match.resetReady()
_match.sendUpdates()
return "Match map has been updated"
def mpSet():
if len(message) < 2 or not message[1].isdigit() or \
(len(message) >= 3 and not message[2].isdigit()) or \
(len(message) >= 4 and not message[3].isdigit()):
raise exceptions.invalidArgumentsException("Wrong syntax: !mp set <teammode> [<scoremode>] [<size>]")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
matchTeamType = int(message[1])
matchScoringType = int(message[2]) if len(message) >= 3 else _match.matchScoringType
if not 0 <= matchTeamType <= 3:
raise exceptions.invalidArgumentsException("Match team type must be between 0 and 3")
if not 0 <= matchScoringType <= 3:
raise exceptions.invalidArgumentsException("Match scoring type must be between 0 and 3")
oldMatchTeamType = _match.matchTeamType
_match.matchTeamType = matchTeamType
_match.matchScoringType = matchScoringType
if len(message) >= 4:
_match.forceSize(int(message[3]))
if _match.matchTeamType != oldMatchTeamType:
_match.initializeTeams()
if _match.matchTeamType == matchTeamTypes.TAG_COOP or _match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
_match.matchModMode = matchModModes.NORMAL
_match.sendUpdates()
return "Match settings have been updated!"
def mpAbort():
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.abort()
return "Match aborted!"
def mpKick():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp kick <username>")
username = message[1].strip()
if not username:
raise exceptions.invalidArgumentsException("Please provide a username")
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
slotID = _match.getUserSlotID(userID)
if slotID is None:
raise exceptions.userNotFoundException("The specified user is not in this match")
for i in range(0, 2):
_match.toggleSlotLocked(slotID)
return "{} has been kicked from the match.".format(username)
def mpPassword():
password = "" if len(message) < 2 or not message[1].strip() else message[1]
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.changePassword(password)
return "Match password has been changed!"
def mpRandomPassword():
password = generalUtils.stringMd5(generalUtils.randomString(32))
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.changePassword(password)
return "Match password has been changed to a random one"
def mpMods():
if len(message) < 2:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp <mod1> [<mod2>] ...")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
newMods = 0
freeMod = False
for _mod in message[1:]:
if _mod.lower().strip() == "hd":
newMods |= mods.HIDDEN
elif _mod.lower().strip() == "hr":
newMods |= mods.HARDROCK
elif _mod.lower().strip() == "dt":
newMods |= mods.DOUBLETIME
elif _mod.lower().strip() == "fl":
newMods |= mods.FLASHLIGHT
elif _mod.lower().strip() == "fi":
newMods |= mods.FADEIN
if _mod.lower().strip() == "none":
newMods = 0
if _mod.lower().strip() == "freemod":
freeMod = True
_match.matchModMode = matchModModes.FREE_MOD if freeMod else matchModModes.NORMAL
_match.resetReady()
if _match.matchModMode == matchModModes.FREE_MOD:
_match.resetMods()
_match.changeMods(newMods)
return "Match mods have been updated!"
def mpTeam():
if len(message) < 3:
raise exceptions.invalidArgumentsException("Wrong syntax: !mp team <username> <colour>")
username = message[1].strip()
if not username:
raise exceptions.invalidArgumentsException("Please provide a username")
colour = message[2].lower().strip()
if colour not in ["red", "blue"]:
raise exceptions.invalidArgumentsException("Team colour must be red or blue")
userID = userUtils.getIDSafe(username)
if userID is None:
raise exceptions.userNotFoundException("No such user")
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
_match.changeTeam(userID, matchTeams.BLUE if colour == "blue" else matchTeams.RED)
return "{} is now in {} team".format(username, colour)
def mpSettings():
_match = glob.matches.matches[getMatchIDFromChannel(chan)]
msg = "PLAYERS IN THIS MATCH:\n"
empty = True
for slot in _match.slots:
if slot.user is None:
continue
readableStatuses = {
slotStatuses.READY: "ready",
slotStatuses.NOT_READY: "not ready",
slotStatuses.NO_MAP: "no map",
slotStatuses.PLAYING: "playing",
}
if slot.status not in readableStatuses:
readableStatus = "???"
else:
readableStatus = readableStatuses[slot.status]
empty = False
msg += "* [{team}] <{status}> ~ {username}{mods}\n".format(
team="red" if slot.team == matchTeams.RED else "blue" if slot.team == matchTeams.BLUE else "!! no team !!",
status=readableStatus,
username=glob.tokens.tokens[slot.user].username,
mods=" (+ {})".format(generalUtils.readableMods(slot.mods)) if slot.mods > 0 else ""
)
if empty:
msg += "Nobody.\n"
return msg
try:
subcommands = {
"make": mpMake,
"close": mpClose,
"join": mpJoin,
"lock": mpLock,
"unlock": mpUnlock,
"size": mpSize,
"move": mpMove,
"host": mpHost,
"clearhost": mpClearHost,
"start": mpStart,
"invite": mpInvite,
"map": mpMap,
"set": mpSet,
"abort": mpAbort,
"kick": mpKick,
"password": mpPassword,
"randompassword": mpRandomPassword,
"mods": mpMods,
"team": mpTeam,
"settings": mpSettings,
}
requestedSubcommand = message[0].lower().strip()
if requestedSubcommand not in subcommands:
raise exceptions.invalidArgumentsException("Invalid subcommand")
return subcommands[requestedSubcommand]()
except (exceptions.invalidArgumentsException, exceptions.userNotFoundException, exceptions.invalidUserException) as e:
return str(e)
except exceptions.wrongChannelException:
return "This command only works in multiplayer chat channels"
except exceptions.matchNotFoundException:
return "Match not found"
except:
raise
def switchServer(fro, chan, message):
# Get target user ID
target = message[0]
newServer = message[1].strip()
if not newServer:
return "Invalid server IP"
targetUserID = userUtils.getIDSafe(target)
userID = userUtils.getID(fro)
# Make sure the user exists
if not targetUserID:
return "{}: user not found".format(target)
# Connect the user to the end server
userToken = glob.tokens.getTokenFromUserID(userID, ignoreIRC=True, _all=False)
userToken.enqueue(serverPackets.switchServer(newServer))
# Disconnect the user from the origin server
# userToken.kick()
return "{} has been connected to {}".format(target, newServer)
def rtx(fro, chan, message):
target = message[0]
message = " ".join(message[1:]).strip()
if not message:
return "Invalid message"
targetUserID = userUtils.getIDSafe(target)
if not targetUserID:
return "{}: user not found".format(target)
userToken = glob.tokens.getTokenFromUserID(targetUserID, ignoreIRC=True, _all=False)
userToken.enqueue(serverPackets.rtx(message))
return ":ok_hand:"
def rawPacket(fro, chan, message):
target = message[0]
message = " ".join(message[1:]).strip()
if not message:
return "Invalid message"
targetUserID = userUtils.getIDSafe(target)
if not targetUserID:
return "{}: user not found".format(target)
userToken = glob.tokens.getTokenFromUserID(targetUserID, ignoreIRC=True, _all=False)
p = message.split(" ", 1)
try:
packetID = int(p[0])
packetData = eval(p[1])
except Exception:
return "Error"
userToken.enqueue(packetHelper.buildPacket(packetID, packetData))
return ":thinking:"
""" """
Commands list Commands list
@@ -782,7 +1169,7 @@ commands = [
"callback": report "callback": report
}, { }, {
"trigger": "!help", "trigger": "!help",
"response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for FokaBot's full command list" "response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for the full command list"
}, #{ }, #{
#"trigger": "!ask", #"trigger": "!ask",
#"syntax": "<question>", #"syntax": "<question>",
@@ -815,7 +1202,7 @@ commands = [
"privileges": privileges.ADMIN_KICK_USERS, "privileges": privileges.ADMIN_KICK_USERS,
"callback": kick "callback": kick
}, { }, {
"trigger": "!fokabot reconnect", "trigger": "!bot reconnect",
"privileges": privileges.ADMIN_MANAGE_SERVERS, "privileges": privileges.ADMIN_MANAGE_SERVERS,
"callback": fokabotReconnect "callback": fokabotReconnect
}, { }, {
@@ -894,6 +1281,26 @@ commands = [
}, { }, {
"trigger": "!update", "trigger": "!update",
"callback": updateBeatmap "callback": updateBeatmap
}, {
"trigger": "!mp",
"privileges": privileges.USER_TOURNAMENT_STAFF,
"syntax": "<subcommand>",
"callback": multiplayer
}, {
"trigger": "!switchserver",
"privileges": privileges.ADMIN_MANAGE_SERVERS,
"syntax": "<username> <server_address>",
"callback": switchServer
}, {
"trigger": "!rtx",
"privileges": privileges.ADMIN_MANAGE_USERS,
"syntax": "<username> <message>",
"callback": rtx
}, {
"trigger": "!raw",
"privileges": privileges.ADMIN_MANAGE_SERVERS,
"syntax": "<username> <byte> <array>",
"callback": rawPacket
} }
# #
# "trigger": "!acc", # "trigger": "!acc",

View File

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

View File

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

View File

@@ -15,29 +15,29 @@ def handle(userToken, packetData):
matchID = userToken.matchID matchID = userToken.matchID
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
match = glob.matches.matches[matchID]
# Set slot or match mods according to modType # Set slot or match mods according to modType
if match.matchModMode == matchModModes.FREE_MOD: with glob.matches.matches[matchID] as match:
# Freemod if match.matchModMode == matchModModes.FREE_MOD:
# Host can set global DT/HT # Freemod
if userID == match.hostUserID: # Host can set global DT/HT
# If host has selected DT/HT and Freemod is enabled, set DT/HT as match mod if userID == match.hostUserID:
if (packetData["mods"] & mods.DOUBLETIME) > 0: # If host has selected DT/HT and Freemod is enabled, set DT/HT as match mod
match.changeMods(mods.DOUBLETIME) if (packetData["mods"] & mods.DOUBLETIME) > 0:
# Nightcore match.changeMods(mods.DOUBLETIME)
if (packetData["mods"] & mods.NIGHTCORE) > 0: # Nightcore
match.changeMods(match.mods + mods.NIGHTCORE) if (packetData["mods"] & mods.NIGHTCORE) > 0:
elif (packetData["mods"] & mods.HALFTIME) > 0: match.changeMods(match.mods + mods.NIGHTCORE)
match.changeMods(mods.HALFTIME) elif (packetData["mods"] & mods.HALFTIME) > 0:
else: match.changeMods(mods.HALFTIME)
# No DT/HT, set global mods to 0 (we are in freemod mode) else:
match.changeMods(0) # No DT/HT, set global mods to 0 (we are in freemod mode)
match.changeMods(0)
# Set slot mods # Set slot mods
slotID = match.getUserSlotID(userID) slotID = match.getUserSlotID(userID)
if slotID is not None: if slotID is not None:
match.setSlotMods(slotID, packetData["mods"]) match.setSlotMods(slotID, packetData["mods"])
else: else:
# Not freemod, set match mods # Not freemod, set match mods
match.changeMods(packetData["mods"]) match.changeMods(packetData["mods"])

View File

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

View File

@@ -21,97 +21,84 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Get match object
match = glob.matches.matches[matchID]
# Host check # Host check
if userToken.userID != match.hostUserID: with glob.matches.matches[matchID] as match:
return if userToken.userID != match.hostUserID:
return
# Some dank memes easter egg # Some dank memes easter egg
memeTitles = [ memeTitles = [
"RWC 2020", "RWC 2020",
"Fokabot is a duck", "Fokabot is a duck",
"Dank memes", "Dank memes",
"1337ms Ping", "1337ms Ping",
"Iscriviti a Xenotoze", "Iscriviti a Xenotoze",
"...e i marò?", "...e i marò?",
"Superman dies", "Superman dies",
"The brace is on fire", "The brace is on fire",
"print_foot()", "print_foot()",
"#FREEZEBARKEZ", "#FREEZEBARKEZ",
"Ripple devs are actually cats", "Ripple devs are actually cats",
"Thank Mr Shaural", "Thank Mr Shaural",
"NEVER GIVE UP", "NEVER GIVE UP",
"T I E D W I T H U N I T E D", "T I E D W I T H U N I T E D",
"HIGHEST HDHR LOBBY OF ALL TIME", "HIGHEST HDHR LOBBY OF ALL TIME",
"This is gasoline and I set myself on fire", "This is gasoline and I set myself on fire",
"Everyone is cheating apparently", "Everyone is cheating apparently",
"Kurwa mac", "Kurwa mac",
"TATOE", "TATOE",
"This is not your drama landfill.", "This is not your drama landfill.",
"I like cheese", "I like cheese",
"NYO IS NOT A CAT HE IS A DO(N)G", "NYO IS NOT A CAT HE IS A DO(N)G",
"Datingu startuato" "Datingu startuato"
] ]
# Set match name # Set match name
match.matchName = packetData["matchName"] if packetData["matchName"] != "meme" else random.choice(memeTitles) match.matchName = packetData["matchName"] if packetData["matchName"] != "meme" else random.choice(memeTitles)
# Update match settings # Update match settings
match.inProgress = packetData["inProgress"] match.inProgress = packetData["inProgress"]
if packetData["matchPassword"] != "": if packetData["matchPassword"] != "":
match.matchPassword = generalUtils.stringMd5(packetData["matchPassword"]) match.matchPassword = generalUtils.stringMd5(packetData["matchPassword"])
else: else:
match.matchPassword = "" match.matchPassword = ""
match.beatmapName = packetData["beatmapName"] match.beatmapName = packetData["beatmapName"]
match.beatmapID = packetData["beatmapID"] match.beatmapID = packetData["beatmapID"]
match.hostUserID = packetData["hostUserID"] match.hostUserID = packetData["hostUserID"]
match.gameMode = packetData["gameMode"] match.gameMode = packetData["gameMode"]
oldBeatmapMD5 = match.beatmapMD5 oldBeatmapMD5 = match.beatmapMD5
oldMods = match.mods oldMods = match.mods
oldMatchTeamType = match.matchTeamType
match.mods = packetData["mods"] match.mods = packetData["mods"]
match.beatmapMD5 = packetData["beatmapMD5"] match.beatmapMD5 = packetData["beatmapMD5"]
match.matchScoringType = packetData["scoringType"] match.matchScoringType = packetData["scoringType"]
match.matchTeamType = packetData["teamType"] match.matchTeamType = packetData["teamType"]
match.matchModMode = packetData["freeMods"] match.matchModMode = packetData["freeMods"]
# 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): match.resetReady()
if match.slots[i].status == slotStatuses.READY:
match.slots[i].status = slotStatuses.NOT_READY
# Reset mods if needed # Reset mods if needed
if match.matchModMode == matchModModes.NORMAL: if match.matchModMode == matchModModes.NORMAL:
# Reset slot mods if not freeMods # Reset slot mods if not freeMods
for i in range(0,16): match.resetMods()
match.slots[i].mods = 0 else:
else: # Reset match mods if freemod
# Reset match mods if freemod match.mods = 0
match.mods = 0
# Set/reset teams # Initialize teams if team type changed
if match.matchTeamType == matchTeamTypes.TEAM_VS or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS: if match.matchTeamType != oldMatchTeamType:
# Set teams match.initializeTeams()
c=0
for i in range(0,16):
if match.slots[i].team == matchTeams.NO_TEAM:
match.slots[i].team = matchTeams.RED if c % 2 == 0 else matchTeams.BLUE
c+=1
else:
# Reset teams
for i in range(0,16):
match.slots[i].team = matchTeams.NO_TEAM
# Force no freemods if tag coop # Force no freemods if tag coop
if match.matchTeamType == matchTeamTypes.TAG_COOP or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS: if match.matchTeamType == matchTeamTypes.TAG_COOP or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
match.matchModMode = matchModModes.NORMAL match.matchModMode = matchModModes.NORMAL
# Send updated settings # Send updated settings
match.sendUpdates() match.sendUpdates()
# Console output # Console output
log.info("MPROOM{}: Updated room settings".format(match.matchID)) log.info("MPROOM{}: Updated room settings".format(match.matchID))

View File

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

View File

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

View File

@@ -17,21 +17,17 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# Match exists, get object
match = glob.matches.matches[matchID]
# Hash password if needed # Hash password if needed
#if password != "": # if password != "":
# password = generalUtils.stringMd5(password) # password = generalUtils.stringMd5(password)
# Check password # Check password
# TODO: Admins can enter every match with glob.matches.matches[matchID] as match:
if match.matchPassword != "": if match.matchPassword != "" and match.matchPassword != password:
if match.matchPassword != password: raise exceptions.matchWrongPasswordException()
raise exceptions.matchWrongPasswordException
# Password is correct, join match # Password is correct, join match
userToken.joinMatch(matchID) userToken.joinMatch(matchID)
except exceptions.matchWrongPasswordException: except exceptions.matchWrongPasswordException:
userToken.enqueue(serverPackets.matchJoinFail()) userToken.enqueue(serverPackets.matchJoinFail())
log.warning("{} has tried to join a mp room, but he typed the wrong password".format(userToken.username)) log.warning("{} has tried to join a mp room, but he typed the wrong password".format(userToken.username))

View File

@@ -183,8 +183,11 @@ def handle(tornadoRequest):
if glob.banchoConf.config["menuIcon"] != "": if glob.banchoConf.config["menuIcon"] != "":
responseToken.enqueue(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) responseToken.enqueue(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
# Send online users IDs array # Send online users' panels
responseToken.enqueue(serverPackets.onlineUsers()) with glob.tokens:
for _, token in glob.tokens.tokens.items():
if not token.restricted:
responseToken.enqueue(serverPackets.userPanel(token.userID))
# Get location and country from ip.zxq.co or database # Get location and country from ip.zxq.co or database
if glob.localize: if glob.localize:
@@ -257,4 +260,4 @@ def handle(tornadoRequest):
log.info("Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP), "bunker") log.info("Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP), "bunker")
# Return token string and data # Return token string and data
return responseTokenString, responseData return responseTokenString, responseData

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,16 +12,16 @@ def handle(userToken, packetData):
matchID = userToken.matchID matchID = userToken.matchID
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
match = glob.matches.matches[matchID]
# Host check with glob.matches.matches[matchID] as match:
if userID != match.hostUserID: # Host check
return if userID != match.hostUserID:
return
# Make sure we aren't locking our slot # Make sure we aren't locking our slot
ourSlot = match.getUserSlotID(userID) ourSlot = match.getUserSlotID(userID)
if packetData["slotID"] == ourSlot: if packetData["slotID"] == ourSlot:
return return
# Lock/Unlock slot # Lock/Unlock slot
match.toggleSlotLock(packetData["slotID"]) match.toggleSlotLocked(packetData["slotID"])

View File

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

View File

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

View File

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

View File

@@ -13,11 +13,9 @@ def handle(userToken, _):
if matchID not in glob.matches.matches: if matchID not in glob.matches.matches:
return return
# The match exists, get object with glob.matches.matches[matchID] as match:
match = glob.matches.matches[matchID] # Host check
if userToken.userID != match.hostUserID:
return
# Host check match.start()
if userToken.userID != match.hostUserID:
return
match.start()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,23 @@
fuck=firetruck fuck=firetruck
shit=shish shit=shish
ass=peach ass=peach
asses=peaches asses=peaches
bitch=fine lady bitch=fine lady
bitches=fine ladies bitches=fine ladies
asshole=donkey asshole=donkey
ass hole=donkey ass hole=donkey
cock=chicken cock=chicken
cocks=chickens cocks=chickens
dick=eggplant dick=eggplant
dicks=eggplants dicks=eggplants
boobs=bob boobs=bob
tits=teeth tits=teeth
cum=yogurt cum=yogurt
cunt=count cunt=count
nigger=flowers
ngger=flowers
niggers=flowers
weed=grass
AQN=meme
theaquila=meme
aquila=meme

View File

@@ -27,7 +27,11 @@ class handler(requestsManager.asyncRequestHandler):
if key is None or key != glob.conf.config["server"]["cikey"]: if key is None or key != glob.conf.config["server"]["cikey"]:
raise exceptions.invalidArgumentsException() raise exceptions.invalidArgumentsException()
chatHelper.sendMessage("FokaBot", self.get_argument("to"), self.get_argument("msg")) chatHelper.sendMessage(
glob.BOT_NAME,
self.get_argument("to").encode().decode("ASCII", "ignore"),
self.get_argument("msg").encode().decode("ASCII", "ignore")
)
# Status code and message # Status code and message
statusCode = 200 statusCode = 200

View File

@@ -93,7 +93,7 @@ class handler(requestsManager.asyncRequestHandler):
# Token exists, get its object and lock it # Token exists, get its object and lock it
userToken = glob.tokens.tokens[requestTokenString] userToken = glob.tokens.tokens[requestTokenString]
userToken.lock.acquire() userToken.processingLock.acquire()
# Keep reading packets until everything has been read # Keep reading packets until everything has been read
while pos < len(requestData): while pos < len(requestData):
@@ -205,8 +205,8 @@ class handler(requestsManager.asyncRequestHandler):
if userToken is not None: if userToken is not None:
# Update ping time for timeout # Update ping time for timeout
userToken.updatePingTime() userToken.updatePingTime()
# Release token lock # Release processing lock
userToken.lock.release() userToken.processingLock.release()
# Delete token if kicked # Delete token if kicked
if userToken.kicked: if userToken.kicked:
glob.tokens.deleteToken(userToken) glob.tokens.deleteToken(userToken)

View File

@@ -79,7 +79,7 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
token = glob.tokens.getTokenFromUserID(userID) token = glob.tokens.getTokenFromUserID(userID)
# Make sure the token exists # Make sure the token exists
if token is None: if token is None:
raise exceptions.userNotFoundException raise exceptions.userNotFoundException()
else: else:
token = token token = token
@@ -157,15 +157,15 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
if token is None: if token is None:
token = glob.tokens.getTokenFromUsername(fro) token = glob.tokens.getTokenFromUsername(fro)
if token is None: if token is None:
raise exceptions.userNotFoundException raise exceptions.userNotFoundException()
else: else:
# token object alredy passed, get its string and its username (fro) # token object alredy passed, get its string and its username (fro)
fro = token.username fro = token.username
#tokenString = token.token #tokenString = token.token
# Make sure this is not a tournament client # Make sure this is not a tournament client
if token.tournament: # if token.tournament:
raise exceptions.userTournamentException() # raise exceptions.userTournamentException()
# Make sure the user is not in restricted mode # Make sure the user is not in restricted mode
if token.restricted: if token.restricted:
@@ -177,7 +177,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# Redirect !report to FokaBot # Redirect !report to FokaBot
if message.startswith("!report"): if message.startswith("!report"):
to = "FokaBot" to = glob.BOT_NAME
# Determine internal name if needed # Determine internal name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels) # (toclient is used clientwise for #multiplayer and #spectator channels)
@@ -194,6 +194,11 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
toClient = "#spectator" toClient = "#spectator"
elif to.startswith("#multi_"): elif to.startswith("#multi_"):
toClient = "#multiplayer" toClient = "#multiplayer"
# Make sure the message is valid
if not message.strip():
raise exceptions.invalidArgumentsException()
# Truncate message if > 2048 characters # Truncate message if > 2048 characters
message = message[:2048]+"..." if len(message) > 2048 else message message = message[:2048]+"..." if len(message) > 2048 else message
@@ -236,7 +241,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# raise exceptions.userTournamentException() # raise exceptions.userTournamentException()
# Make sure the recipient is not restricted or we are FokaBot # Make sure the recipient is not restricted or we are FokaBot
if recipientToken.restricted == True and fro.lower() != "fokabot": if recipientToken.restricted == True and fro.lower() != glob.BOT_NAME.lower():
raise exceptions.userRestrictedException() raise exceptions.userRestrictedException()
# TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend # TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend
@@ -254,22 +259,26 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# Send the message to IRC # Send the message to IRC
if glob.irc == True and toIRC == True: if glob.irc == True and toIRC == True:
glob.ircServer.banchoMessage(fro, to, message) messageSplitInLines = message.encode("latin-1").decode("utf-8").split("\n")
for line in messageSplitInLines:
if line == messageSplitInLines[:1] and line == "":
continue
glob.ircServer.banchoMessage(fro, to, line)
# Spam protection (ignore FokaBot) # Spam protection (ignore FokaBot)
if token.userID > 999: if token.userID > 999:
token.spamProtection() token.spamProtection()
# Fokabot message # Fokabot message
if isChannel == True or to.lower() == "fokabot": if isChannel == True or to.lower() == glob.BOT_NAME.lower():
fokaMessage = fokabot.fokabotResponse(token.username, to, message) fokaMessage = fokabot.fokabotResponse(token.username, to, message)
if fokaMessage: if fokaMessage:
sendMessage("FokaBot", to if isChannel else fro, fokaMessage) sendMessage(glob.BOT_NAME, to if isChannel else fro, fokaMessage)
# File and discord logs (public chat only) # File and discord logs (public chat only)
if to.startswith("#") and not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")): if to.startswith("#") and not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")):
log.chat("{fro} @ {to}: {message}".format(fro=token.username, to=to, message=str(message.encode("utf-8")))) log.chat("{fro} @ {to}: {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=str(message.encode("utf-8"))[2:-1])) glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
return 0 return 0
except exceptions.userSilencedException: except exceptions.userSilencedException:
token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft())) token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft()))
@@ -293,6 +302,9 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
except exceptions.userNotFoundException: except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho") log.warning("User not connected to IRC/Bancho")
return 401 return 401
except exceptions.invalidArgumentsException:
log.warning("{} tried to send an invalid message to {}".format(token.username, to))
return 404
""" IRC-Bancho Connect/Disconnect/Join/Part interfaces""" """ IRC-Bancho Connect/Disconnect/Join/Part interfaces"""

View File

@@ -48,8 +48,8 @@ class config:
self.config.get("server","gziplevel") self.config.get("server","gziplevel")
self.config.get("server","cikey") self.config.get("server","cikey")
self.config.get("mirror","url") self.config.get("cheesegull", "apiurl")
self.config.get("mirror","apikey") self.config.get("cheesegull", "apikey")
self.config.get("debug","enable") self.config.get("debug","enable")
self.config.get("debug","packets") self.config.get("debug","packets")
@@ -107,9 +107,9 @@ class config:
self.config.set("server", "gziplevel", "6") self.config.set("server", "gziplevel", "6")
self.config.set("server", "cikey", "changeme") self.config.set("server", "cikey", "changeme")
self.config.add_section("mirror") self.config.add_section("cheesegull")
self.config.set("mirror", "url", "http://storage.ripple.moe") self.config.set("cheesegull", "apiurl", "http://cheesegu.ll/api")
self.config.set("mirror", "apikey", "anotherkey") self.config.set("cheesegull", "apikey", "")
self.config.add_section("debug") self.config.add_section("debug")
self.config.set("debug", "enable", "0") self.config.set("debug", "enable", "0")

View File

@@ -28,7 +28,7 @@ def printServerStartHeader(asciiArt=True):
printColored("> Welcome to pep.py osu!bancho server v{}".format(glob.VERSION), bcolors.GREEN) printColored("> Welcome to pep.py osu!bancho server v{}".format(glob.VERSION), bcolors.GREEN)
printColored("> Made by the Ripple team", bcolors.GREEN) printColored("> Made by the Ripple team", bcolors.GREEN)
printColored("> {}https://git.zxq.co/ripple/pep.py".format(bcolors.UNDERLINE), bcolors.GREEN) printColored("> {}https://zxq.co/ripple/pep.py".format(bcolors.UNDERLINE), bcolors.GREEN)
printColored("> Press CTRL+C to exit\n", bcolors.GREEN) printColored("> Press CTRL+C to exit\n", bcolors.GREEN)
def printNoNl(string): def printNoNl(string):

View File

@@ -17,6 +17,7 @@ import traceback
import raven import raven
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils
from helpers import chatHelper as chat from helpers import chatHelper as chat
from objects import glob from objects import glob
@@ -44,6 +45,7 @@ class Client:
self.IRCUsername = "" self.IRCUsername = ""
self.banchoUsername = "" self.banchoUsername = ""
self.supposedUsername = "" self.supposedUsername = ""
self.supposedUserID = 0
self.joinedChannels = [] self.joinedChannels = []
def messageChannel(self, channel, command, message, includeSelf=False): def messageChannel(self, channel, command, message, includeSelf=False):
@@ -280,9 +282,10 @@ class Client:
m = hashlib.md5() m = hashlib.md5()
m.update(arguments[0].encode("utf-8")) m.update(arguments[0].encode("utf-8"))
tokenHash = m.hexdigest() tokenHash = m.hexdigest()
supposedUsername = glob.db.fetch("SELECT users.username FROM users LEFT JOIN irc_tokens ON users.id = irc_tokens.userid WHERE irc_tokens.token = %s LIMIT 1", [tokenHash]) supposedUser = glob.db.fetch("SELECT users.username, users.id FROM users LEFT JOIN irc_tokens ON users.id = irc_tokens.userid WHERE irc_tokens.token = %s LIMIT 1", [tokenHash])
if supposedUsername: if supposedUser:
self.supposedUsername = chat.fixUsernameForIRC(supposedUsername["username"]) self.supposedUsername = chat.fixUsernameForIRC(supposedUser["username"])
self.supposedUserID = supposedUser["id"]
self.__handleCommand = self.registerHandler self.__handleCommand = self.registerHandler
else: else:
# Wrong IRC Token # Wrong IRC Token
@@ -310,6 +313,11 @@ class Client:
self.reply("464 :Password incorrect") self.reply("464 :Password incorrect")
return return
# Make sure that the user is not banned/restricted:
if not userUtils.isAllowed(self.supposedUserID):
self.reply("465 :You're banned")
return
# Make sure we are not connected to Bancho # Make sure we are not connected to Bancho
token = glob.tokens.getTokenFromUsername(chat.fixUsernameForBancho(nick), True) token = glob.tokens.getTokenFromUsername(chat.fixUsernameForBancho(nick), True)
if token is not None: if token is not None:

View File

@@ -53,6 +53,20 @@ class channelList:
self.channels[name] = channel.channel(name, "Chat", True, True, True, True) self.channels[name] = channel.channel(name, "Chat", True, True, True, True)
log.info("Created temp channel {}".format(name)) log.info("Created temp channel {}".format(name))
def addHiddenChannel(self, name):
"""
Add a hidden channel. It's like a normal channel and must be deleted manually,
but it's not shown in channels list.
:param name: channel name
:return: True if the channel was created, otherwise False
"""
if name in self.channels:
return False
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, "Chat", True, True, False, True)
log.info("Created hidden channel {}".format(name))
def removeChannel(self, name): def removeChannel(self, name):
""" """
Removes a channel from channels list Removes a channel from channels list

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ except:
VERSION = "Unknown" VERSION = "Unknown"
DATADOG_PREFIX = "peppy" DATADOG_PREFIX = "peppy"
BOT_NAME = "FokaBot"
application = None application = None
db = None db = None
redis = None redis = None

View File

@@ -1,4 +1,9 @@
import copy import copy
import json
import threading
import time
from common.log import logUtils as log from common.log import logUtils as log
from constants import dataTypes from constants import dataTypes
from constants import matchModModes from constants import matchModModes
@@ -14,16 +19,19 @@ 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 = matchTeams.NO_TEAM
self.userID = -1 self.userID = -1
self.user = None self.user = None
self.mods = 0 self.mods = 0
self.loaded = False self.loaded = False
self.skip = False self.skip = False
self.complete = False self.complete = False
self.score = 0
self.failed = False
self.passed = True
class match: class match:
def __init__(self, matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID): def __init__(self, matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID, isTourney=False):
""" """
Create a new match object Create a new match object
@@ -53,6 +61,11 @@ class match:
self.matchModMode = matchModModes.NORMAL # default value self.matchModMode = matchModModes.NORMAL # default value
self.seed = 0 self.seed = 0
self.matchDataCache = bytes() self.matchDataCache = bytes()
self.isTourney = isTourney
self.isLocked = False # if True, users can't change slots/teams. Used in tourney matches
self.isStarting = False
self._lock = threading.Lock()
self.createTime = int(time.time())
# Create all slots and reset them # Create all slots and reset them
self.slots = [] self.slots = []
@@ -64,59 +77,67 @@ class match:
glob.streams.add(self.playingStreamName) glob.streams.add(self.playingStreamName)
# Create #multiplayer channel # Create #multiplayer channel
glob.channels.addTempChannel("#multi_{}".format(self.matchID)) glob.channels.addHiddenChannel("#multi_{}".format(self.matchID))
log.info("MPROOM{}: {} match created!".format(self.matchID, "Tourney" if self.isTourney else "Normal"))
def getMatchData(self): def getMatchData(self, censored = False):
""" """
Return binary match data structure for packetHelper Return binary match data structure for packetHelper
Return binary match data structure for packetHelper
:return: :return:
""" """
# General match info # General match info
# TODO: Test without safe copy, the error might have been caused by outdated python bytecode cache # TODO: Test without safe copy, the error might have been caused by outdated python bytecode cache
safeMatch = copy.deepcopy(self) # safeMatch = copy.deepcopy(self)
struct = [ struct = [
[safeMatch.matchID, dataTypes.UINT16], [self.matchID, dataTypes.UINT16],
[int(safeMatch.inProgress), dataTypes.BYTE], [int(self.inProgress), dataTypes.BYTE],
[0, dataTypes.BYTE], [0, dataTypes.BYTE],
[safeMatch.mods, dataTypes.UINT32], [self.mods, dataTypes.UINT32],
[safeMatch.matchName, dataTypes.STRING], [self.matchName, dataTypes.STRING]
[safeMatch.matchPassword, dataTypes.STRING],
[safeMatch.beatmapName, dataTypes.STRING],
[safeMatch.beatmapID, dataTypes.UINT32],
[safeMatch.beatmapMD5, dataTypes.STRING],
] ]
if censored and self.matchPassword:
struct.append(["redacted", dataTypes.STRING])
else:
struct.append([self.matchPassword, dataTypes.STRING])
struct.extend([
[self.beatmapName, dataTypes.STRING],
[self.beatmapID, dataTypes.UINT32],
[self.beatmapMD5, dataTypes.STRING]
])
# Slots status IDs, always 16 elements # Slots status IDs, always 16 elements
for i in range(0,16): for i in range(0,16):
struct.append([safeMatch.slots[i].status, dataTypes.BYTE]) struct.append([self.slots[i].status, dataTypes.BYTE])
# Slot teams, always 16 elements # Slot teams, always 16 elements
for i in range(0,16): for i in range(0,16):
struct.append([safeMatch.slots[i].team, dataTypes.BYTE]) struct.append([self.slots[i].team, dataTypes.BYTE])
# Slot user ID. Write only if slot is occupied # Slot user ID. Write only if slot is occupied
for i in range(0,16): for i in range(0,16):
if safeMatch.slots[i].user is not None and safeMatch.slots[i].user in glob.tokens.tokens: if self.slots[i].user is not None and self.slots[i].user in glob.tokens.tokens:
struct.append([glob.tokens.tokens[safeMatch.slots[i].user].userID, dataTypes.UINT32]) struct.append([glob.tokens.tokens[self.slots[i].user].userID, dataTypes.UINT32])
# Other match data # Other match data
struct.extend([ struct.extend([
[safeMatch.hostUserID, dataTypes.SINT32], [self.hostUserID, dataTypes.SINT32],
[safeMatch.gameMode, dataTypes.BYTE], [self.gameMode, dataTypes.BYTE],
[safeMatch.matchScoringType, dataTypes.BYTE], [self.matchScoringType, dataTypes.BYTE],
[safeMatch.matchTeamType, dataTypes.BYTE], [self.matchTeamType, dataTypes.BYTE],
[safeMatch.matchModMode, dataTypes.BYTE], [self.matchModMode, dataTypes.BYTE],
]) ])
# Slot mods if free mod is enabled # Slot mods if free mod is enabled
if safeMatch.matchModMode == matchModModes.FREE_MOD: if self.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([self.slots[i].mods, dataTypes.UINT32])
# Seed idk # Seed idk
# TODO: Implement this, it should be used for mania "random" mod # TODO: Implement this, it should be used for mania "random" mod
struct.append([safeMatch.seed, dataTypes.UINT32]) struct.append([self.seed, dataTypes.UINT32])
return struct return struct
@@ -129,12 +150,22 @@ class match:
""" """
slotID = self.getUserSlotID(newHost) slotID = self.getUserSlotID(newHost)
if slotID is None or self.slots[slotID].user not in glob.tokens.tokens: if slotID is None or self.slots[slotID].user not in glob.tokens.tokens:
return return False
token = glob.tokens.tokens[self.slots[slotID].user] token = glob.tokens.tokens[self.slots[slotID].user]
self.hostUserID = newHost self.hostUserID = newHost
token.enqueue(serverPackets.matchTransferHost()) token.enqueue(serverPackets.matchTransferHost())
self.sendUpdates() self.sendUpdates()
log.info("MPROOM{}: {} is now the host".format(self.matchID, token.username)) log.info("MPROOM{}: {} is now the host".format(self.matchID, token.username))
return True
def removeHost(self):
"""
Removes the host (for tourney matches)
:return:
"""
self.hostUserID = -1
self.sendUpdates()
log.info("MPROOM{}: Removed host".format(self.matchID))
def setSlot(self, slotID, status = None, team = None, user = "", mods = None, loaded = None, skip = None, complete = None): def setSlot(self, slotID, status = None, team = None, user = "", mods = None, loaded = None, skip = None, complete = None):
""" """
@@ -195,6 +226,8 @@ class match:
:return: :return:
""" """
# Update ready status and setnd update # Update ready status and setnd update
if self.slots[slotID].user is None or self.isStarting:
return
oldStatus = self.slots[slotID].status oldStatus = self.slots[slotID].status
if oldStatus == slotStatuses.READY: if oldStatus == slotStatuses.READY:
newStatus = slotStatuses.NOT_READY newStatus = slotStatuses.NOT_READY
@@ -204,7 +237,7 @@ class match:
self.sendUpdates() self.sendUpdates()
log.info("MPROOM{}: Slot{} changed ready status to {}".format(self.matchID, slotID, self.slots[slotID].status)) log.info("MPROOM{}: Slot{} changed ready status to {}".format(self.matchID, slotID, self.slots[slotID].status))
def toggleSlotLock(self, slotID): def toggleSlotLocked(self, slotID):
""" """
Lock a slot Lock a slot
Same as calling setSlot and then sendUpdate Same as calling setSlot and then sendUpdate
@@ -305,6 +338,26 @@ class match:
glob.streams.broadcast(self.playingStreamName, serverPackets.allPlayersSkipped()) glob.streams.broadcast(self.playingStreamName, serverPackets.allPlayersSkipped())
log.info("MPROOM{}: All players have skipped!".format(self.matchID)) log.info("MPROOM{}: All players have skipped!".format(self.matchID))
def updateScore(self, slotID, score):
"""
Update score for a slot
:param slotID: the slot that the user that is updating their score is in
:param score: the new score to update
:return:
"""
self.slots[slotID].score = score
def updateHP(self, slotID, hp):
"""
Update HP for a slot
:param slotID: the slot that the user that is updating their hp is in
:param hp: the new hp to update
:return:
"""
self.slots[slotID].failed = True if hp == 254 else False
def playerCompleted(self, userID): def playerCompleted(self, userID):
""" """
Set userID's slot completed to True Set userID's slot completed to True
@@ -337,16 +390,35 @@ class match:
:return: :return:
""" """
# Collect some info about the match that just ended to send to the api
infoToSend = {
"id": self.matchID,
"name": self.matchName,
"beatmap_id": self.beatmapID,
"mods": self.mods,
"game_mode": self.gameMode,
"scores": {}
}
# Add score info for each player
for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
infoToSend["scores"][glob.tokens.tokens[self.slots[i].user].userID] = {
"score": self.slots[i].score,
"mods": self.slots[i].mods,
"failed": self.slots[i].failed,
"pass": self.slots[i].passed,
"team": self.slots[i].team
}
# Send the info to the api
glob.redis.publish("api:mp_complete_match", json.dumps(infoToSend))
# Reset inProgress # Reset inProgress
self.inProgress = False self.inProgress = False
# Reset slots # Reset slots
for i in range(0,16): self.resetSlots()
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
self.slots[i].status = slotStatuses.NOT_READY
self.slots[i].loaded = False
self.slots[i].skip = False
self.slots[i].complete = False
# Send match update # Send match update
self.sendUpdates() self.sendUpdates()
@@ -361,6 +433,23 @@ class match:
# Console output # Console output
log.info("MPROOM{}: Match completed".format(self.matchID)) log.info("MPROOM{}: Match completed".format(self.matchID))
# If this is a tournament match, then we send a notification in the chat
# saying that the match has completed.
chanName = "#multi_{}".format(self.matchID)
if self.isTourney and (chanName in glob.channels.channels):
chat.sendMessage(glob.BOT_NAME, chanName, "Match has just finished.")
def resetSlots(self):
for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
self.slots[i].status = slotStatuses.NOT_READY
self.slots[i].loaded = False
self.slots[i].skip = False
self.slots[i].complete = False
self.slots[i].score = 0
self.slots[i].failed = False
self.slots[i].passed = True
def getUserSlotID(self, userID): def getUserSlotID(self, userID):
""" """
Get slot ID occupied by userID Get slot ID occupied by userID
@@ -389,7 +478,10 @@ class match:
for i in range(0,16): for i in range(0,16):
if self.slots[i].status == slotStatuses.FREE: if self.slots[i].status == slotStatuses.FREE:
# Occupy slot # Occupy slot
self.setSlot(i, slotStatuses.NOT_READY, 0, user.token, 0) team = matchTeams.NO_TEAM
if self.matchTeamType == matchTeamTypes.TEAM_VS or self.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
team = matchTeams.RED if i % 2 == 0 else matchTeams.BLUE
self.setSlot(i, slotStatuses.NOT_READY, team, user.token, 0)
# Send updated match data # Send updated match data
self.sendUpdates() self.sendUpdates()
@@ -400,11 +492,12 @@ class match:
return False return False
def userLeft(self, user): def userLeft(self, user, disposeMatch=True):
""" """
Remove someone from users in match Remove someone from users in match
:param user: user object of the user :param user: user object of the user
:param disposeMatch: if `True`, will try to dispose match if there are no users in the room
:return: :return:
""" """
# Make sure the user is in room # Make sure the user is in room
@@ -416,10 +509,10 @@ class match:
self.setSlot(slotID, slotStatuses.FREE, 0, None, 0) self.setSlot(slotID, slotStatuses.FREE, 0, None, 0)
# Check if everyone left # Check if everyone left
if self.countUsers() == 0: if self.countUsers() == 0 and disposeMatch and not self.isTourney:
# Dispose match # Dispose match
glob.matches.disposeMatch(self.matchID) glob.matches.disposeMatch(self.matchID)
log.info("MPROOM{}: Room disposed".format(self.matchID)) log.info("MPROOM{}: Room disposed because all users left".format(self.matchID))
return return
# Check if host left # Check if host left
@@ -444,14 +537,18 @@ class match:
:param newSlotID: slot id of new slot :param newSlotID: slot id of new slot
:return: :return:
""" """
# Make sure the match is not locked
if self.isLocked or self.isStarting:
return False
# Make sure the user is in room # Make sure the user is in room
oldSlotID = self.getUserSlotID(userID) oldSlotID = self.getUserSlotID(userID)
if oldSlotID is None: if oldSlotID is None:
return return False
# 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 or self.slots[newSlotID].status != slotStatuses.FREE:
return return False
# Get old slot data # Get old slot data
#oldData = dill.copy(self.slots[oldSlotID]) #oldData = dill.copy(self.slots[oldSlotID])
@@ -468,6 +565,7 @@ class match:
# Console output # Console output
log.info("MPROOM{}: {} moved to slot {}".format(self.matchID, userID, newSlotID)) log.info("MPROOM{}: {} moved to slot {}".format(self.matchID, userID, newSlotID))
return True
def changePassword(self, newPassword): def changePassword(self, newPassword):
""" """
@@ -533,7 +631,7 @@ class match:
self.setHost(glob.tokens.tokens[self.slots[slotID].user].userID) self.setHost(glob.tokens.tokens[self.slots[slotID].user].userID)
# Send updates # Send updates
self.sendUpdates() # self.sendUpdates()
def playerFailed(self, userID): def playerFailed(self, userID):
""" """
@@ -547,6 +645,8 @@ class match:
if slotID is None: if slotID is None:
return return
self.slots[slotID].passed = False
# Send packet to everyone # Send packet to everyone
glob.streams.broadcast(self.playingStreamName, serverPackets.playerFailed(slotID)) glob.streams.broadcast(self.playingStreamName, serverPackets.playerFailed(slotID))
@@ -569,7 +669,7 @@ class match:
# FokaBot is too busy # FokaBot is too busy
if to == 999: if to == 999:
chat.sendMessage("FokaBot", froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.") chat.sendMessage(glob.BOT_NAME, froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.")
# Send message # Send message
message = "Come join my multiplayer match: \"[osump://{}/{} {}]\"".format(self.matchID, self.matchPassword.replace(" ", "_"), self.matchName) message = "Come join my multiplayer match: \"[osump://{}/{} {}]\"".format(self.matchID, self.matchPassword.replace(" ", "_"), self.matchName)
@@ -587,20 +687,29 @@ class match:
c+=1 c+=1
return c return c
def changeTeam(self, userID): def changeTeam(self, userID, newTeam=None):
""" """
Change userID's team Change userID's team
:param userID: id of user :param userID: id of user
:return: :return:
""" """
# Make sure this match's mode has teams
if self.matchTeamType != matchTeamTypes.TEAM_VS and self.matchTeamType != matchTeamTypes.TAG_TEAM_VS:
return
# Make sure the match is not locked
if self.isLocked or self.isStarting:
return
# Make sure the user is in room # Make sure the user is in room
slotID = self.getUserSlotID(userID) slotID = self.getUserSlotID(userID)
if slotID is None: if slotID is None:
return return
# Update slot and send update # Update slot and send update
newTeam = matchTeams.BLUE if self.slots[slotID].team == matchTeams.RED else matchTeams.RED if newTeam is None:
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()
@@ -611,9 +720,11 @@ class match:
:return: :return:
""" """
self.matchDataCache = serverPackets.updateMatch(self.matchID) self.matchDataCache = serverPackets.updateMatch(self.matchID)
censoredDataCache = serverPackets.updateMatch(self.matchID, censored=True)
if self.matchDataCache is not None: if self.matchDataCache is not None:
glob.streams.broadcast(self.streamName, self.matchDataCache) glob.streams.broadcast(self.streamName, self.matchDataCache)
glob.streams.broadcast("lobby", self.matchDataCache) if censoredDataCache is not None:
glob.streams.broadcast("lobby", censoredDataCache)
else: else:
log.error("MPROOM{}: Can't send match update packet, match data is None!!!".format(self.matchID)) log.error("MPROOM{}: Can't send match update packet, match data is None!!!".format(self.matchID))
@@ -624,7 +735,7 @@ class match:
:return: True if valid, False if invalid :return: True if valid, False if invalid
:return: :return:
""" """
if self.matchTeamType != matchTeamTypes.TEAM_VS or self.matchTeamType != matchTeamTypes.TAG_TEAM_VS: if self.matchTeamType != matchTeamTypes.TEAM_VS and 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
@@ -647,20 +758,23 @@ class match:
:return: :return:
""" """
# Remove isStarting timer flag thingie
self.isStarting = False
# Make sure we have enough players # Make sure we have enough players
if self.countUsers() < 2 or not self.checkTeams(): if self.countUsers() < 2 or not self.checkTeams():
return return False
# Create playing channel # Create playing channel
glob.streams.add(self.playingStreamName) glob.streams.add(self.playingStreamName)
# Change inProgress value # Change inProgress value
match.inProgress = True self.inProgress = True
# Set playing to ready players and set load, skip and complete to False # Set playing to ready players and set load, skip and complete to False
# Make clients join playing stream # Make clients join playing stream
for i in range(0, 16): for i in range(0, 16):
if (self.slots[i].status & slotStatuses.READY) > 0 and self.slots[i].user in glob.tokens.tokens: if 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
@@ -671,4 +785,86 @@ class match:
glob.streams.broadcast(self.playingStreamName, serverPackets.matchStart(self.matchID)) glob.streams.broadcast(self.playingStreamName, serverPackets.matchStart(self.matchID))
# Send updates # Send updates
self.sendUpdates() self.sendUpdates()
return True
def forceSize(self, matchSize):
for i in range(0, matchSize):
if self.slots[i].status == slotStatuses.LOCKED:
self.toggleSlotLocked(i)
for i in range(matchSize, 16):
if self.slots[i].status != slotStatuses.LOCKED:
self.toggleSlotLocked(i)
def abort(self):
if not self.inProgress:
log.warning("MPROOM{}: Match is not in progress!".format(self.matchID))
return
self.inProgress = False
self.isStarting = False
self.resetSlots()
self.sendUpdates()
glob.streams.broadcast(self.playingStreamName, serverPackets.matchAbort())
glob.streams.dispose(self.playingStreamName)
glob.streams.remove(self.playingStreamName)
log.info("MPROOM{}: Match aborted".format(self.matchID))
def initializeTeams(self):
if self.matchTeamType == matchTeamTypes.TEAM_VS or self.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
# Set teams
for i, _slot in enumerate(self.slots):
_slot.team = matchTeams.RED if i % 2 == 0 else matchTeams.BLUE
else:
# Reset teams
for _slot in self.slots:
_slot.team = matchTeams.NO_TEAM
def resetMods(self):
for _slot in self.slots:
_slot.mods = 0
def resetReady(self):
for _slot in self.slots:
if _slot.status == slotStatuses.READY:
_slot.status = slotStatuses.NOT_READY
def sendReadyStatus(self):
chanName = "#multi_{}".format(self.matchID)
# Make sure match exists before attempting to do anything else
if chanName not in glob.channels.channels:
return
totalUsers = 0
readyUsers = 0
for slot in self.slots:
# Make sure there is a user in this slot
if slot.user is None:
continue
# In this slot there is a user, so we increase the amount of total users
# in this multi room.
totalUsers += 1
if slot.status == slotStatuses.READY:
readyUsers += 1
message = "{} users ready out of {}.".format(readyUsers, totalUsers)
if totalUsers == readyUsers:
message += " All users ready!"
# Check whether there is anyone left in this match.
if totalUsers == 0:
message = "The match is now empty."
chat.sendMessage(glob.BOT_NAME, chanName, message)
def __enter__(self):
# 🌚🌚🌚🌚🌚
self._lock.acquire()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._lock.release()

View File

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

View File

@@ -30,14 +30,15 @@ class token:
self.username = userUtils.getUsername(self.userID) self.username = userUtils.getUsername(self.userID)
self.safeUsername = userUtils.getSafeUsername(self.userID) self.safeUsername = userUtils.getSafeUsername(self.userID)
self.privileges = userUtils.getPrivileges(self.userID) self.privileges = userUtils.getPrivileges(self.userID)
self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer") or userUtils.isInPrivilegeGroup(self.userID, "community manager") self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer")\
or userUtils.isInPrivilegeGroup(self.userID, "community manager")\
or userUtils.isInPrivilegeGroup(self.userID, "chat mod")
self.irc = irc self.irc = irc
self.kicked = False self.kicked = False
self.restricted = userUtils.isRestricted(self.userID) self.restricted = userUtils.isRestricted(self.userID)
self.loginTime = int(time.time()) self.loginTime = int(time.time())
self.pingTime = self.loginTime self.pingTime = self.loginTime
self.timeOffset = timeOffset self.timeOffset = timeOffset
self.lock = threading.Lock() # Sync primitive
self.streams = [] self.streams = []
self.tournament = tournament self.tournament = tournament
self.messagesBuffer = [] self.messagesBuffer = []
@@ -84,6 +85,11 @@ class token:
else: else:
self.token = str(uuid.uuid4()) self.token = str(uuid.uuid4())
# Locks
self.processingLock = threading.Lock() # Acquired while there's an incoming packet from this user
self._bufferLock = threading.Lock() # Acquired while writing to packets buffer
self._spectLock = threading.RLock()
# Set stats # Set stats
self.updateCachedStats() self.updateCachedStats()
@@ -100,20 +106,30 @@ class token:
:param bytes_: (packet) bytes to enqueue :param bytes_: (packet) bytes to enqueue
""" """
try:
# Acquire the buffer lock
self._bufferLock.acquire()
# Never enqueue for IRC clients or Foka # Never enqueue for IRC clients or Foka
if self.irc or self.userID < 999: if self.irc or self.userID < 999:
return return
# Avoid memory leaks # Avoid memory leaks
if len(bytes_) < 10 * 10 ** 6: if len(bytes_) < 10 * 10 ** 6:
self.queue += bytes_ self.queue += bytes_
else: else:
log.warning("{}'s packets buffer is above 10M!! Lost some data!".format(self.username)) log.warning("{}'s packets buffer is above 10M!! Lost some data!".format(self.username))
finally:
# Release the buffer lock
self._bufferLock.release()
def resetQueue(self): def resetQueue(self):
"""Resets the queue. Call when enqueued packets have been sent""" """Resets the queue. Call when enqueued packets have been sent"""
self.queue = bytes() try:
self._bufferLock.acquire()
self.queue = bytes()
finally:
self._bufferLock.release()
def joinChannel(self, channelObject): def joinChannel(self, channelObject):
""" """
@@ -172,42 +188,47 @@ class token:
:param host: host osuToken object :param host: host osuToken object
""" """
# Stop spectating old client try:
self.stopSpectating() self._spectLock.acquire()
# Set new spectator host # Stop spectating old client
self.spectating = host.token self.stopSpectating()
self.spectatingUserID = host.userID
# Add us to host's spectator list # Set new spectator host
host.spectators.append(self.token) self.spectating = host.token
self.spectatingUserID = host.userID
# Create and join spectator stream # Add us to host's spectator list
streamName = "spect/{}".format(host.userID) host.spectators.append(self.token)
glob.streams.add(streamName)
self.joinStream(streamName)
host.joinStream(streamName)
# Send spectator join packet to host # Create and join spectator stream
host.enqueue(serverPackets.addSpectator(self.userID)) streamName = "spect/{}".format(host.userID)
glob.streams.add(streamName)
self.joinStream(streamName)
host.joinStream(streamName)
# Create and join #spectator (#spect_userid) channel # Send spectator join packet to host
glob.channels.addTempChannel("#spect_{}".format(host.userID)) host.enqueue(serverPackets.addSpectator(self.userID))
chat.joinChannel(token=self, channel="#spect_{}".format(host.userID))
if len(host.spectators) == 1:
# First spectator, send #spectator join to host too
chat.joinChannel(token=host, channel="#spect_{}".format(host.userID))
# Send fellow spectator join to all clients # Create and join #spectator (#spect_userid) channel
glob.streams.broadcast(streamName, serverPackets.fellowSpectatorJoined(self.userID)) glob.channels.addTempChannel("#spect_{}".format(host.userID))
chat.joinChannel(token=self, channel="#spect_{}".format(host.userID))
if len(host.spectators) == 1:
# First spectator, send #spectator join to host too
chat.joinChannel(token=host, channel="#spect_{}".format(host.userID))
# Get current spectators list # Send fellow spectator join to all clients
for i in host.spectators: glob.streams.broadcast(streamName, serverPackets.fellowSpectatorJoined(self.userID))
if i != self.token and i in glob.tokens.tokens:
self.enqueue(serverPackets.fellowSpectatorJoined(glob.tokens.tokens[i].userID))
# Log # Get current spectators list
log.info("{} is spectating {}".format(self.username, host.username)) for i in host.spectators:
if i != self.token and i in glob.tokens.tokens:
self.enqueue(serverPackets.fellowSpectatorJoined(glob.tokens.tokens[i].userID))
# Log
log.info("{} is spectating {}".format(self.username, host.username))
finally:
self._spectLock.release()
def stopSpectating(self): def stopSpectating(self):
""" """
@@ -216,43 +237,48 @@ class token:
:return: :return:
""" """
# Remove our userID from host's spectators try:
if self.spectating is None: self._spectLock.acquire()
return
if self.spectating in glob.tokens.tokens:
hostToken = glob.tokens.tokens[self.spectating]
else:
hostToken = None
streamName = "spect/{}".format(self.spectatingUserID)
# Remove us from host's spectators list, # Remove our userID from host's spectators
# leave spectator stream if self.spectating is None or self.spectatingUserID <= 0:
# and end the spectator left packet to host return
self.leaveStream(streamName) if self.spectating in glob.tokens.tokens:
if hostToken is not None: hostToken = glob.tokens.tokens[self.spectating]
hostToken.spectators.remove(self.token) else:
hostToken.enqueue(serverPackets.removeSpectator(self.userID)) hostToken = None
streamName = "spect/{}".format(self.spectatingUserID)
# and to all other spectators # Remove us from host's spectators list,
for i in hostToken.spectators: # leave spectator stream
if i in glob.tokens.tokens: # and end the spectator left packet to host
glob.tokens.tokens[i].enqueue(serverPackets.fellowSpectatorLeft(self.userID)) self.leaveStream(streamName)
if hostToken is not None:
hostToken.spectators.remove(self.token)
hostToken.enqueue(serverPackets.removeSpectator(self.userID))
# If nobody is spectating the host anymore, close #spectator channel # and to all other spectators
# and remove host from spect stream too for i in hostToken.spectators:
if len(hostToken.spectators) == 0: if i in glob.tokens.tokens:
chat.partChannel(token=hostToken, channel="#spect_{}".format(hostToken.userID), kick=True) glob.tokens.tokens[i].enqueue(serverPackets.fellowSpectatorLeft(self.userID))
hostToken.leaveStream(streamName)
# Console output # If nobody is spectating the host anymore, close #spectator channel
log.info("{} is no longer spectating {}. Current spectators: {}".format(self.username, self.spectatingUserID, hostToken.spectators)) # and remove host from spect stream too
if len(hostToken.spectators) == 0:
chat.partChannel(token=hostToken, channel="#spect_{}".format(hostToken.userID), kick=True)
hostToken.leaveStream(streamName)
# Part #spectator channel # Console output
chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True) log.info("{} is no longer spectating {}. Current spectators: {}".format(self.username, self.spectatingUserID, hostToken.spectators))
# Set our spectating user to 0 # Part #spectator channel
self.spectating = None chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True)
self.spectatingUserID = 0
# Set our spectating user to 0
self.spectating = None
self.spectatingUserID = 0
finally:
self._spectLock.release()
def updatePingTime(self): def updatePingTime(self):
""" """
@@ -295,6 +321,13 @@ class token:
chat.joinChannel(token=self, channel="#multi_{}".format(self.matchID)) chat.joinChannel(token=self, channel="#multi_{}".format(self.matchID))
self.enqueue(serverPackets.matchJoinSuccess(matchID)) self.enqueue(serverPackets.matchJoinSuccess(matchID))
if match.isTourney:
# Alert the user if we have just joined a tourney match
self.enqueue(serverPackets.notification("You are now in a tournament match."))
# If an user joins, then the ready status of the match changes and
# maybe not all users are ready.
match.sendReadyStatus()
def leaveMatch(self): def leaveMatch(self):
""" """
Leave joined match, match stream and match channel Leave joined match, match stream and match channel
@@ -310,23 +343,29 @@ class token:
self.leaveStream("multi/{}".format(self.matchID)) self.leaveStream("multi/{}".format(self.matchID))
self.leaveStream("multi/{}/playing".format(self.matchID)) # optional self.leaveStream("multi/{}/playing".format(self.matchID)) # optional
# Set usertoken match to -1
leavingMatchID = self.matchID
self.matchID = -1
# Make sure the match exists # Make sure the match exists
if self.matchID not in glob.matches.matches: if leavingMatchID not in glob.matches.matches:
return return
# The match exists, get object # The match exists, get object
match = glob.matches.matches[self.matchID] match = glob.matches.matches[leavingMatchID]
# Set slot to free # Set slot to free
match.userLeft(self) match.userLeft(self)
# Set usertoken match to -1 if match.isTourney:
self.matchID = -1 # If an user leaves, then the ready status of the match changes and
# maybe all users are ready. Or maybe nobody is in the match anymore
match.sendReadyStatus()
def kick(self, message="You have been kicked from the server. Please login again.", reason="kick"): def kick(self, message="You have been kicked from the server. Please login again.", reason="kick"):
""" """
Kick this user from the server Kick this user from the server
:param message: Notification message to send to this user. :param message: Notification message to send to this user.
Default: "You have been kicked from the server. Please login again." Default: "You have been kicked from the server. Please login again."
:param reason: Kick reason, used in logs. Default: "kick" :param reason: Kick reason, used in logs. Default: "kick"
@@ -448,7 +487,7 @@ class token:
:return: :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(glob.BOT_NAME, self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.")
def resetRestricted(self): def resetRestricted(self):
""" """
@@ -457,7 +496,7 @@ class token:
:return: :return:
""" """
chat.sendMessage("FokaBot", self.username, "Your account has been unrestricted! Please log in again.") chat.sendMessage(glob.BOT_NAME, self.username, "Your account has been unrestricted! Please log in again.")
def joinStream(self, name): def joinStream(self, name):
""" """

View File

@@ -13,6 +13,13 @@ from objects import osuToken
class tokenList: class tokenList:
def __init__(self): def __init__(self):
self.tokens = {} self.tokens = {}
self._lock = threading.Lock()
def __enter__(self):
self._lock.acquire()
def __exit__(self, exc_type, exc_val, exc_tb):
self._lock.release()
def addToken(self, userID, ip = "", irc = False, timeOffset=0, tournament=False): def addToken(self, userID, ip = "", irc = False, timeOffset=0, tournament=False):
""" """
@@ -40,7 +47,8 @@ class tokenList:
if token in self.tokens: if token in self.tokens:
if self.tokens[token].ip != "": if self.tokens[token].ip != "":
userUtils.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip) userUtils.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip)
self.tokens.pop(token) t = self.tokens.pop(token)
del t
glob.redis.decr("ripple:online_users") glob.redis.decr("ripple:online_users")
def getUserIDFromToken(self, token): def getUserIDFromToken(self, token):
@@ -163,19 +171,16 @@ class tokenList:
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):
""" """
Start timed out users disconnect loop. Start timed out users disconnect loop.
This function will be called every `checkTime` seconds and so on, forever. This function will be called every `checkTime` seconds and so on, forever.
CALL THIS FUNCTION ONLY ONCE! CALL THIS FUNCTION ONLY ONCE!
:param timeoutTime: seconds of inactivity required to disconnect someone. Default: 100
:param checkTime: seconds between loops. Default: 100
:return: :return:
""" """
log.debug("Checking timed out clients") log.debug("Checking timed out clients")
timedOutTokens = [] # timed out users timedOutTokens = [] # timed out users
timeoutLimit = int(time.time())-timeoutTime timeoutLimit = int(time.time()) - 100
for key, value in self.tokens.items(): for key, value in self.tokens.items():
# Check timeout (fokabot is ignored) # Check timeout (fokabot is ignored)
if value.pingTime < timeoutLimit and value.userID != 999 and value.irc == False and value.tournament == False: if value.pingTime < timeoutLimit and value.userID != 999 and value.irc == False and value.tournament == False:
@@ -189,9 +194,10 @@ class tokenList:
log.debug("{} timed out!!".format(self.tokens[i].username)) log.debug("{} timed out!!".format(self.tokens[i].username))
self.tokens[i].enqueue(serverPackets.notification("Your connection to the server timed out.")) self.tokens[i].enqueue(serverPackets.notification("Your connection to the server timed out."))
logoutEvent.handle(self.tokens[i], None) logoutEvent.handle(self.tokens[i], None)
del timedOutTokens
# Schedule a new check (endless loop) # Schedule a new check (endless loop)
threading.Timer(checkTime, self.usersTimeoutCheckLoop, [timeoutTime, checkTime]).start() threading.Timer(100, self.usersTimeoutCheckLoop).start()
def spamProtectionResetLoop(self): def spamProtectionResetLoop(self):
""" """

19
pep.py
View File

@@ -1,6 +1,7 @@
import os import os
import sys import sys
import threading import threading
import json
from multiprocessing.pool import ThreadPool from multiprocessing.pool import ThreadPool
import tornado.gen import tornado.gen
import tornado.httpserver import tornado.httpserver
@@ -61,6 +62,11 @@ if __name__ == "__main__":
consoleHelper.printNoNl("> Loading config file... ") consoleHelper.printNoNl("> Loading config file... ")
glob.conf = configHelper.config("config.ini") glob.conf = configHelper.config("config.ini")
# Read additional config file
consoleHelper.printNoNl("> Loading additional config file... ")
with open("config.json", "r") as f:
glob.conf.extra = json.load(f)
if glob.conf.default: if glob.conf.default:
# We have generated a default config.ini, quit server # We have generated a default config.ini, quit server
consoleHelper.printWarning() consoleHelper.printWarning()
@@ -156,7 +162,7 @@ if __name__ == "__main__":
raise raise
# Start fokabot # Start fokabot
consoleHelper.printNoNl("> Connecting FokaBot... ") consoleHelper.printNoNl("> Connecting bot... ")
fokabot.connect() fokabot.connect()
consoleHelper.printDone() consoleHelper.printDone()
@@ -181,6 +187,11 @@ if __name__ == "__main__":
glob.tokens.spamProtectionResetLoop() glob.tokens.spamProtectionResetLoop()
consoleHelper.printDone() consoleHelper.printDone()
# Initialize multiplayer cleanup loop
consoleHelper.printNoNl("> Initializing multiplayer cleanup loop... ")
glob.matches.cleanupLoop()
consoleHelper.printDone()
# Localize warning # Localize warning
glob.localize = generalUtils.stringToBool(glob.conf.config["localize"]["enable"]) glob.localize = generalUtils.stringToBool(glob.conf.config["localize"]["enable"])
if not glob.localize: if not glob.localize:
@@ -212,7 +223,7 @@ if __name__ == "__main__":
try: try:
glob.sentry = generalUtils.stringToBool(glob.conf.config["sentry"]["enable"]) glob.sentry = generalUtils.stringToBool(glob.conf.config["sentry"]["enable"])
if glob.sentry: if glob.sentry:
glob.application.sentry_client = AsyncSentryClient(glob.conf.config["sentry"]["banchodns"], release=glob.VERSION) glob.application.sentry_client = AsyncSentryClient(glob.conf.config["sentry"]["banchodsn"], release=glob.VERSION)
else: else:
consoleHelper.printColored("[!] Warning! Sentry logging is disabled!", bcolors.YELLOW) consoleHelper.printColored("[!] Warning! Sentry logging is disabled!", bcolors.YELLOW)
except: except:
@@ -253,7 +264,7 @@ if __name__ == "__main__":
ircPort = int(glob.conf.config["irc"]["port"]) ircPort = int(glob.conf.config["irc"]["port"])
except ValueError: except ValueError:
consoleHelper.printColored("[!] Invalid IRC port! Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Invalid IRC port! Please check your config.ini and run the server again", bcolors.RED)
log.logMessage("**pep.py** IRC server started!", discord="bunker", of="info.txt", stdout=False) log.logMessage("IRC server started!", discord="bunker", of="info.txt", stdout=False)
consoleHelper.printColored("> IRC server listening on 127.0.0.1:{}...".format(ircPort), bcolors.GREEN) consoleHelper.printColored("> IRC server listening on 127.0.0.1:{}...".format(ircPort), bcolors.GREEN)
threading.Thread(target=lambda: ircserver.main(port=ircPort)).start() threading.Thread(target=lambda: ircserver.main(port=ircPort)).start()
else: else:
@@ -267,7 +278,7 @@ if __name__ == "__main__":
consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED)
# Server start message and console output # Server start message and console output
log.logMessage("**pep.py** Server started!", discord="bunker", of="info.txt", stdout=False) log.logMessage("Server started!", discord="bunker", of="info.txt", stdout=False)
consoleHelper.printColored("> Tornado listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN) consoleHelper.printColored("> Tornado listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN)
# Connect to pubsub channels # Connect to pubsub channels

View File

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

View File

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

View File

@@ -1 +1 @@
1.11.2 1.13.3