569 lines
16 KiB
Python
569 lines
16 KiB
Python
import threading
|
|
import time
|
|
import uuid
|
|
|
|
from common.constants import gameModes, actions
|
|
from common.log import logUtils as log
|
|
from common.ripple import userUtils
|
|
from constants import exceptions
|
|
from constants import serverPackets
|
|
from events import logoutEvent
|
|
from helpers import chatHelper as chat
|
|
from objects import glob
|
|
|
|
|
|
class token:
|
|
def __init__(self, userID, token_ = None, ip ="", irc = False, timeOffset = 0, tournament = False):
|
|
"""
|
|
Create a token object and set userID and token
|
|
|
|
:param userID: user associated to this token
|
|
:param token_: if passed, set token to that value
|
|
if not passed, token will be generated
|
|
:param ip: client ip. optional.
|
|
:param irc: if True, set this token as IRC client. Default: False.
|
|
:param timeOffset: the time offset from UTC for this user. Default: 0.
|
|
:param tournament: if True, flag this client as a tournement client. Default: True.
|
|
"""
|
|
# Set stuff
|
|
self.userID = userID
|
|
self.username = userUtils.getUsername(self.userID)
|
|
self.safeUsername = userUtils.getSafeUsername(self.userID)
|
|
self.privileges = userUtils.getPrivileges(self.userID)
|
|
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.kicked = False
|
|
self.restricted = userUtils.isRestricted(self.userID)
|
|
self.loginTime = int(time.time())
|
|
self.pingTime = self.loginTime
|
|
self.timeOffset = timeOffset
|
|
self.streams = []
|
|
self.tournament = tournament
|
|
self.messagesBuffer = []
|
|
|
|
# Default variables
|
|
self.spectators = []
|
|
|
|
# TODO: Move those two vars to a class
|
|
self.spectating = None
|
|
self.spectatingUserID = 0 # we need this in case we the host gets DCed
|
|
|
|
self.location = [0,0]
|
|
self.joinedChannels = []
|
|
self.ip = ip
|
|
self.country = 0
|
|
self.location = [0,0]
|
|
self.awayMessage = ""
|
|
self.sentAway = []
|
|
self.matchID = -1
|
|
self.tillerino = [0,0,-1.0] # beatmap, mods, acc
|
|
self.silenceEndTime = 0
|
|
self.queue = bytes()
|
|
|
|
# Spam protection
|
|
self.spamRate = 0
|
|
|
|
# Stats cache
|
|
self.actionID = actions.IDLE
|
|
self.actionText = ""
|
|
self.actionMd5 = ""
|
|
self.actionMods = 0
|
|
self.gameMode = gameModes.STD
|
|
self.beatmapID = 0
|
|
self.rankedScore = 0
|
|
self.accuracy = 0.0
|
|
self.playcount = 0
|
|
self.totalScore = 0
|
|
self.gameRank = 0
|
|
self.pp = 0
|
|
|
|
# Generate/set token
|
|
if token_ is not None:
|
|
self.token = token_
|
|
else:
|
|
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._spectatorLock = threading.Lock() # Acquired while starting/stopping spectating
|
|
self._multiplayerLock = threading.Lock()# Acquired while joining/leaving streams
|
|
|
|
# Set stats
|
|
self.updateCachedStats()
|
|
|
|
# If we have a valid ip, save bancho session in DB so we can cache LETS logins
|
|
if ip != "":
|
|
userUtils.saveBanchoSession(self.userID, self.ip)
|
|
|
|
# Join main stream
|
|
self.joinStream("main")
|
|
|
|
def enqueue(self, bytes_):
|
|
"""
|
|
Add bytes (packets) to queue
|
|
|
|
:param bytes_: (packet) bytes to enqueue
|
|
"""
|
|
try:
|
|
# Acquire the buffer lock
|
|
self._bufferLock.acquire()
|
|
|
|
# Never enqueue for IRC clients or Foka
|
|
if self.irc or self.userID < 999:
|
|
return
|
|
|
|
# Avoid memory leaks
|
|
if len(bytes_) < 10 * 10 ** 6:
|
|
self.queue += bytes_
|
|
else:
|
|
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):
|
|
"""Resets the queue. Call when enqueued packets have been sent"""
|
|
try:
|
|
self._bufferLock.acquire()
|
|
self.queue = bytes()
|
|
finally:
|
|
self._bufferLock.release()
|
|
|
|
def joinChannel(self, channelObject):
|
|
"""
|
|
Join a channel
|
|
|
|
:param channelObject: channel object
|
|
:raises: exceptions.userAlreadyInChannelException()
|
|
exceptions.channelNoPermissionsException()
|
|
"""
|
|
if channelObject.name in self.joinedChannels:
|
|
raise exceptions.userAlreadyInChannelException()
|
|
if channelObject.publicRead == False and self.admin == False:
|
|
raise exceptions.channelNoPermissionsException()
|
|
self.joinedChannels.append(channelObject.name)
|
|
self.joinStream("chat/{}".format(channelObject.name))
|
|
self.enqueue(serverPackets.channelJoinSuccess(self.userID, channelObject.clientName))
|
|
|
|
def partChannel(self, channelObject):
|
|
"""
|
|
Remove channel from joined channels list
|
|
|
|
:param channelObject: channel object
|
|
"""
|
|
self.joinedChannels.remove(channelObject.name)
|
|
self.leaveStream("chat/{}".format(channelObject.name))
|
|
|
|
def setLocation(self, latitude, longitude):
|
|
"""
|
|
Set client location
|
|
|
|
:param latitude: latitude
|
|
:param longitude: longitude
|
|
"""
|
|
self.location = (latitude, longitude)
|
|
|
|
def getLatitude(self):
|
|
"""
|
|
Get latitude
|
|
|
|
:return: latitude
|
|
"""
|
|
return self.location[0]
|
|
|
|
def getLongitude(self):
|
|
"""
|
|
Get longitude
|
|
|
|
:return: longitude
|
|
"""
|
|
return self.location[1]
|
|
|
|
def startSpectating(self, host):
|
|
"""
|
|
Set the spectating user to userID, join spectator stream and chat channel
|
|
and send required packets to host
|
|
|
|
:param host: host osuToken object
|
|
"""
|
|
try:
|
|
# Stop spectating old client
|
|
self.stopSpectating()
|
|
|
|
# Acquire token's lock
|
|
self._spectatorLock.acquire()
|
|
|
|
# Set new spectator host
|
|
self.spectating = host.token
|
|
self.spectatingUserID = host.userID
|
|
|
|
# Add us to host's spectator list
|
|
host.spectators.append(self.token)
|
|
|
|
# Create and join spectator stream
|
|
streamName = "spect/{}".format(host.userID)
|
|
glob.streams.add(streamName)
|
|
self.joinStream(streamName)
|
|
host.joinStream(streamName)
|
|
|
|
# Send spectator join packet to host
|
|
host.enqueue(serverPackets.addSpectator(self.userID))
|
|
|
|
# Create and join #spectator (#spect_userid) channel
|
|
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))
|
|
|
|
# Send fellow spectator join to all clients
|
|
glob.streams.broadcast(streamName, serverPackets.fellowSpectatorJoined(self.userID))
|
|
|
|
# Get current spectators list
|
|
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._spectatorLock.release()
|
|
|
|
def stopSpectating(self):
|
|
"""
|
|
Stop spectating, leave spectator stream and channel
|
|
and send required packets to host
|
|
|
|
:return:
|
|
"""
|
|
try:
|
|
# Acquire token lock
|
|
self._spectatorLock.acquire()
|
|
|
|
# Remove our userID from host's spectators
|
|
if self.spectating is None:
|
|
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,
|
|
# leave spectator stream
|
|
# and end the spectator left packet to host
|
|
self.leaveStream(streamName)
|
|
if hostToken is not None:
|
|
hostToken.spectators.remove(self.token)
|
|
hostToken.enqueue(serverPackets.removeSpectator(self.userID))
|
|
|
|
# and to all other spectators
|
|
for i in hostToken.spectators:
|
|
if i in glob.tokens.tokens:
|
|
glob.tokens.tokens[i].enqueue(serverPackets.fellowSpectatorLeft(self.userID))
|
|
|
|
# If nobody is spectating the host anymore, close #spectator channel
|
|
# 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)
|
|
|
|
# Console output
|
|
log.info("{} is no longer spectating {}. Current spectators: {}".format(self.username, self.spectatingUserID, hostToken.spectators))
|
|
|
|
# Part #spectator channel
|
|
chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True)
|
|
|
|
# Set our spectating user to 0
|
|
self.spectating = None
|
|
self.spectatingUserID = 0
|
|
finally:
|
|
self._spectatorLock.release()
|
|
|
|
def updatePingTime(self):
|
|
"""
|
|
Update latest ping time to current time
|
|
|
|
:return:
|
|
"""
|
|
self.pingTime = int(time.time())
|
|
|
|
def joinMatch(self, matchID):
|
|
"""
|
|
Set match to matchID, join match stream and channel
|
|
|
|
:param matchID: new match ID
|
|
:return:
|
|
"""
|
|
try:
|
|
self._multiplayerLock.acquire()
|
|
|
|
# Make sure the match exists
|
|
if matchID not in glob.matches.matches:
|
|
return
|
|
|
|
# Match exists, get object
|
|
match = glob.matches.matches[matchID]
|
|
|
|
# Stop spectating
|
|
self.stopSpectating()
|
|
|
|
# Leave other matches
|
|
if self.matchID > -1 and self.matchID != matchID:
|
|
self.leaveMatch()
|
|
|
|
# Try to join match
|
|
joined = match.userJoin(self)
|
|
if not joined:
|
|
self.enqueue(serverPackets.matchJoinFail())
|
|
return
|
|
|
|
# Set matchID, join stream, channel and send packet
|
|
self.matchID = matchID
|
|
self.joinStream(match.streamName)
|
|
chat.joinChannel(token=self, channel="#multi_{}".format(self.matchID))
|
|
self.enqueue(serverPackets.matchJoinSuccess(matchID))
|
|
|
|
# Alert the user if we have just joined a tourney match
|
|
if match.isTourney:
|
|
self.enqueue(serverPackets.notification("You are now in a tournament match."))
|
|
finally:
|
|
self._multiplayerLock.release()
|
|
|
|
def leaveMatch(self):
|
|
"""
|
|
Leave joined match, match stream and match channel
|
|
|
|
:return:
|
|
"""
|
|
try:
|
|
self._multiplayerLock.acquire()
|
|
|
|
# Make sure we are in a match
|
|
if self.matchID == -1:
|
|
return
|
|
|
|
# Part #multiplayer channel and streams (/ and /playing)
|
|
chat.partChannel(token=self, channel="#multi_{}".format(self.matchID), kick=True)
|
|
self.leaveStream("multi/{}".format(self.matchID))
|
|
self.leaveStream("multi/{}/playing".format(self.matchID)) # optional
|
|
|
|
# Set usertoken match to -1
|
|
leavingMatchID = self.matchID
|
|
self.matchID = -1
|
|
|
|
# Make sure the match exists
|
|
if leavingMatchID not in glob.matches.matches:
|
|
return
|
|
|
|
# The match exists, get object
|
|
match = glob.matches.matches[leavingMatchID]
|
|
|
|
# Set slot to free
|
|
match.userLeft(self)
|
|
finally:
|
|
self._multiplayerLock.release()
|
|
|
|
def kick(self, message="You have been kicked from the server. Please login again.", reason="kick"):
|
|
"""
|
|
Kick this user from the server
|
|
|
|
:param message: Notification message to send to this user.
|
|
Default: "You have been kicked from the server. Please login again."
|
|
:param reason: Kick reason, used in logs. Default: "kick"
|
|
:return:
|
|
"""
|
|
# Send packet to target
|
|
log.info("{} has been disconnected. ({})".format(self.username, reason))
|
|
if message != "":
|
|
self.enqueue(serverPackets.notification(message))
|
|
self.enqueue(serverPackets.loginFailed())
|
|
|
|
# Logout event
|
|
logoutEvent.handle(self, deleteToken=self.irc)
|
|
|
|
def silence(self, seconds = None, reason = "", author = 999):
|
|
"""
|
|
Silences this user (db, packet and token)
|
|
|
|
:param seconds: silence length in seconds. If None, get it from db. Default: None
|
|
:param reason: silence reason. Default: empty string
|
|
:param author: userID of who has silenced the user. Default: 999 (FokaBot)
|
|
:return:
|
|
"""
|
|
if seconds is None:
|
|
# Get silence expire from db if needed
|
|
seconds = max(0, userUtils.getSilenceEnd(self.userID) - int(time.time()))
|
|
else:
|
|
# Silence in db and token
|
|
userUtils.silence(self.userID, seconds, reason, author)
|
|
|
|
# Silence token
|
|
self.silenceEndTime = int(time.time()) + seconds
|
|
|
|
# Send silence packet to user
|
|
self.enqueue(serverPackets.silenceEndTime(seconds))
|
|
|
|
# Send silenced packet to everyone else
|
|
glob.streams.broadcast("main", serverPackets.userSilenced(self.userID))
|
|
|
|
def spamProtection(self, increaseSpamRate = True):
|
|
"""
|
|
Silences the user if is spamming.
|
|
|
|
:param increaseSpamRate: set to True if the user has sent a new message. Default: True
|
|
:return:
|
|
"""
|
|
# Increase the spam rate if needed
|
|
if increaseSpamRate:
|
|
self.spamRate += 1
|
|
|
|
# Silence the user if needed
|
|
if self.spamRate > 10:
|
|
self.silence(1800, "Spamming (auto spam protection)")
|
|
|
|
def isSilenced(self):
|
|
"""
|
|
Returns True if this user is silenced, otherwise False
|
|
|
|
:return: True if this user is silenced, otherwise False
|
|
"""
|
|
return self.silenceEndTime-int(time.time()) > 0
|
|
|
|
def getSilenceSecondsLeft(self):
|
|
"""
|
|
Returns the seconds left for this user's silence
|
|
(0 if user is not silenced)
|
|
|
|
:return: silence seconds left (or 0)
|
|
"""
|
|
return max(0, self.silenceEndTime-int(time.time()))
|
|
|
|
def updateCachedStats(self):
|
|
"""
|
|
Update all cached stats for this token
|
|
|
|
:return:
|
|
"""
|
|
stats = userUtils.getUserStats(self.userID, self.gameMode)
|
|
log.debug(str(stats))
|
|
if stats is None:
|
|
log.warning("Stats query returned None")
|
|
return
|
|
self.rankedScore = stats["rankedScore"]
|
|
self.accuracy = stats["accuracy"]/100
|
|
self.playcount = stats["playcount"]
|
|
self.totalScore = stats["totalScore"]
|
|
self.gameRank = stats["gameRank"]
|
|
self.pp = stats["pp"]
|
|
|
|
def checkRestricted(self):
|
|
"""
|
|
Check if this token is restricted. If so, send fokabot message
|
|
|
|
:return:
|
|
"""
|
|
oldRestricted = self.restricted
|
|
self.restricted = userUtils.isRestricted(self.userID)
|
|
if self.restricted:
|
|
self.setRestricted()
|
|
elif not self.restricted and oldRestricted != self.restricted:
|
|
self.resetRestricted()
|
|
|
|
def checkBanned(self):
|
|
"""
|
|
Check if this user is banned. If so, disconnect it.
|
|
|
|
:return:
|
|
"""
|
|
if userUtils.isBanned(self.userID):
|
|
self.enqueue(serverPackets.loginBanned())
|
|
logoutEvent.handle(self, deleteToken=False)
|
|
|
|
|
|
def setRestricted(self):
|
|
"""
|
|
Set this token as restricted, send FokaBot message to user
|
|
and send offline packet to everyone
|
|
|
|
:return:
|
|
"""
|
|
self.restricted = True
|
|
chat.sendMessage("FokaBot", self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.")
|
|
|
|
def resetRestricted(self):
|
|
"""
|
|
Send FokaBot message to alert the user that he has been unrestricted
|
|
and he has to log in again.
|
|
|
|
:return:
|
|
"""
|
|
chat.sendMessage("FokaBot", self.username, "Your account has been unrestricted! Please log in again.")
|
|
|
|
def joinStream(self, name):
|
|
"""
|
|
Join a packet stream, or create it if the stream doesn't exist.
|
|
|
|
:param name: stream name
|
|
:return:
|
|
"""
|
|
glob.streams.join(name, token=self.token)
|
|
if name not in self.streams:
|
|
self.streams.append(name)
|
|
|
|
def leaveStream(self, name):
|
|
"""
|
|
Leave a packets stream
|
|
|
|
:param name: stream name
|
|
:return:
|
|
"""
|
|
glob.streams.leave(name, token=self.token)
|
|
if name in self.streams:
|
|
self.streams.remove(name)
|
|
|
|
def leaveAllStreams(self):
|
|
"""
|
|
Leave all joined packet streams
|
|
|
|
:return:
|
|
"""
|
|
for i in self.streams:
|
|
self.leaveStream(i)
|
|
|
|
def awayCheck(self, userID):
|
|
"""
|
|
Returns True if userID doesn't know that we are away
|
|
Returns False if we are not away or if userID already knows we are away
|
|
|
|
:param userID: original sender userID
|
|
:return:
|
|
"""
|
|
if self.awayMessage == "" or userID in self.sentAway:
|
|
return False
|
|
self.sentAway.append(userID)
|
|
return True
|
|
|
|
def addMessageInBuffer(self, chan, message):
|
|
"""
|
|
Add a message in messages buffer (10 messages, truncated at 50 chars).
|
|
Used as proof when the user gets reported.
|
|
|
|
:param chan: channel
|
|
:param message: message content
|
|
:return:
|
|
"""
|
|
if len(self.messagesBuffer) > 9:
|
|
self.messagesBuffer = self.messagesBuffer[1:]
|
|
self.messagesBuffer.append("{time} - {user}@{channel}: {message}".format(time=time.strftime("%H:%M", time.localtime()), user=self.username, channel=chan, message=message[:50]))
|
|
|
|
def getMessagesBufferString(self):
|
|
"""
|
|
Get the content of the messages buffer as a string
|
|
|
|
:return: messages buffer content as a string
|
|
"""
|
|
return "\n".join(x for x in self.messagesBuffer) |