pep.py/objects/osuToken.py

564 lines
16 KiB
Python
Raw Normal View History

2016-10-02 20:48:14 +00:00
import threading
import time
import uuid
from common.constants import gameModes, actions
from common.log import logUtils as log
from common.ripple import userUtils
2016-12-11 10:07:35 +00:00
from constants import exceptions
2016-05-18 17:12:46 +00:00
from constants import serverPackets
from events import logoutEvent
from helpers import chatHelper as chat
2016-10-02 20:48:14 +00:00
from objects import glob
2016-04-19 17:40:59 +00:00
2016-09-02 15:45:10 +00:00
class token:
def __init__(self, userID, token_ = None, ip ="", irc = False, timeOffset = 0, tournament = False):
2016-04-19 17:40:59 +00:00
"""
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.
2016-04-19 17:40:59 +00:00
"""
# Set stuff
self.userID = userID
2016-10-02 20:48:14 +00:00
self.username = userUtils.getUsername(self.userID)
self.safeUsername = userUtils.getSafeUsername(self.userID)
2016-10-02 20:48:14 +00:00
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
2016-10-02 20:48:14 +00:00
self.restricted = userUtils.isRestricted(self.userID)
2016-04-19 17:40:59 +00:00
self.loginTime = int(time.time())
self.pingTime = self.loginTime
2016-08-01 18:38:26 +00:00
self.timeOffset = timeOffset
self.streams = []
self.tournament = tournament
2016-12-11 22:12:06 +00:00
self.messagesBuffer = []
2016-04-19 17:40:59 +00:00
# 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
2016-04-19 17:40:59 +00:00
self.location = [0,0]
self.joinedChannels = []
2016-06-10 11:15:42 +00:00
self.ip = ip
self.country = 0
self.location = [0,0]
2016-04-19 17:40:59 +00:00
self.awayMessage = ""
self.sentAway = []
2016-04-19 17:40:59 +00:00
self.matchID = -1
self.tillerino = [0,0,-1.0] # beatmap, mods, acc
self.silenceEndTime = 0
self.queue = bytes()
2016-04-19 17:40:59 +00:00
# Spam protection
self.spamRate = 0
# Stats cache
2016-09-02 15:16:22 +00:00
self.actionID = actions.IDLE
self.actionText = ""
self.actionMd5 = ""
self.actionMods = 0
2016-10-02 20:48:14 +00:00
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
2016-04-19 17:40:59 +00:00
# Generate/set token
2016-09-02 15:45:10 +00:00
if token_ is not None:
self.token = token_
2016-04-19 17:40:59 +00:00
else:
self.token = str(uuid.uuid4())
2017-08-10 22:45:44 +00:00
# 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
2017-08-12 19:29:24 +00:00
self._spectLock = threading.RLock()
2017-08-10 22:45:44 +00:00
# Set stats
self.updateCachedStats()
2016-06-10 11:15:42 +00:00
# If we have a valid ip, save bancho session in DB so we can cache LETS logins
if ip != "":
2016-10-02 20:48:14 +00:00
userUtils.saveBanchoSession(self.userID, self.ip)
2016-06-10 11:15:42 +00:00
# Join main stream
self.joinStream("main")
2016-09-02 15:45:10 +00:00
def enqueue(self, bytes_):
2016-04-19 17:40:59 +00:00
"""
Add bytes (packets) to queue
2016-12-26 09:33:05 +00:00
:param bytes_: (packet) bytes to enqueue
2016-04-19 17:40:59 +00:00
"""
2017-07-28 20:46:24 +00:00
try:
# Acquire the buffer lock
2017-08-10 22:45:44 +00:00
self._bufferLock.acquire()
2017-07-28 20:46:24 +00:00
# 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
2017-08-10 22:45:44 +00:00
self._bufferLock.release()
2017-07-28 20:32:30 +00:00
2016-04-19 17:40:59 +00:00
def resetQueue(self):
"""Resets the queue. Call when enqueued packets have been sent"""
2017-07-28 20:46:24 +00:00
try:
2017-08-10 22:45:44 +00:00
self._bufferLock.acquire()
2017-07-28 20:46:24 +00:00
self.queue = bytes()
finally:
2017-08-10 22:45:44 +00:00
self._bufferLock.release()
2016-04-19 17:40:59 +00:00
2016-12-11 10:07:35 +00:00
def joinChannel(self, channelObject):
"""
2016-12-11 10:07:35 +00:00
Join a channel
2016-04-19 17:40:59 +00:00
2016-12-11 10:07:35 +00:00
:param channelObject: channel object
:raises: exceptions.userAlreadyInChannelException()
exceptions.channelNoPermissionsException()
"""
2017-08-11 20:19:39 +00:00
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))
2016-12-11 10:07:35 +00:00
def partChannel(self, channelObject):
"""
Remove channel from joined channels list
2016-04-19 17:40:59 +00:00
2016-12-11 10:07:35 +00:00
:param channelObject: channel object
"""
2017-08-11 20:19:39 +00:00
self.joinedChannels.remove(channelObject.name)
self.leaveStream("chat/{}".format(channelObject.name))
2016-04-19 17:40:59 +00:00
def setLocation(self, latitude, longitude):
"""
Set client location
2016-04-19 17:40:59 +00:00
2016-12-26 09:33:05 +00:00
:param latitude: latitude
:param longitude: longitude
"""
self.location = (latitude, longitude)
2016-04-19 17:40:59 +00:00
def getLatitude(self):
"""
Get latitude
2016-04-19 17:40:59 +00:00
:return: latitude
"""
2016-04-19 17:40:59 +00:00
return self.location[0]
def getLongitude(self):
"""
Get longitude
2016-04-19 17:40:59 +00:00
:return: longitude
"""
2016-04-19 17:40:59 +00:00
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
2016-04-19 17:40:59 +00:00
:param host: host osuToken object
"""
2017-08-12 19:29:24 +00:00
try:
self._spectLock.acquire()
# Stop spectating old client
self.stopSpectating()
2017-08-12 19:29:24 +00:00
# Set new spectator host
self.spectating = host.token
self.spectatingUserID = host.userID
2017-08-12 19:29:24 +00:00
# Add us to host's spectator list
host.spectators.append(self.token)
2017-08-12 19:29:24 +00:00
# Create and join spectator stream
streamName = "spect/{}".format(host.userID)
glob.streams.add(streamName)
self.joinStream(streamName)
host.joinStream(streamName)
2017-08-12 19:29:24 +00:00
# Send spectator join packet to host
host.enqueue(serverPackets.addSpectator(self.userID))
2017-08-12 19:29:24 +00:00
# Create and join #spectator (#spect_userid) channel
glob.channels.addTempChannel("#spect_{}".format(host.userID))
chat.joinChannel(token=self, channel="#spect_{}".format(host.userID), force=True)
2017-08-12 19:29:24 +00:00
if len(host.spectators) == 1:
# First spectator, send #spectator join to host too
chat.joinChannel(token=host, channel="#spect_{}".format(host.userID), force=True)
2017-08-12 19:29:24 +00:00
# Send fellow spectator join to all clients
glob.streams.broadcast(streamName, serverPackets.fellowSpectatorJoined(self.userID))
2017-08-12 19:29:24 +00:00
# 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))
2017-08-10 22:45:44 +00:00
2017-08-12 19:29:24 +00:00
# Log
log.info("{} is spectating {}".format(self.username, host.username))
finally:
self._spectLock.release()
2016-04-19 17:40:59 +00:00
def stopSpectating(self):
"""
Stop spectating, leave spectator stream and channel
and send required packets to host
:return:
"""
2017-08-12 19:29:24 +00:00
try:
self._spectLock.acquire()
# Remove our userID from host's spectators
if self.spectating is None or self.spectatingUserID <= 0:
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, force=True)
2017-08-12 19:29:24 +00:00
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, force=True)
2017-08-12 19:29:24 +00:00
# Set our spectating user to 0
self.spectating = None
self.spectatingUserID = 0
finally:
self._spectLock.release()
2016-04-19 17:40:59 +00:00
def updatePingTime(self):
"""
Update latest ping time to current time
2016-04-19 17:40:59 +00:00
:return:
"""
2016-04-19 17:40:59 +00:00
self.pingTime = int(time.time())
def joinMatch(self, matchID):
2016-04-19 17:40:59 +00:00
"""
Set match to matchID, join match stream and channel
2016-04-19 17:40:59 +00:00
:param matchID: new match ID
:return:
2016-04-19 17:40:59 +00:00
"""
2017-08-12 16:56:31 +00:00
# Make sure the match exists
if matchID not in glob.matches.matches:
return
2017-08-12 16:56:31 +00:00
# Match exists, get object
match = glob.matches.matches[matchID]
2017-08-12 16:56:31 +00:00
# Stop spectating
self.stopSpectating()
2017-08-12 16:56:31 +00:00
# Leave other matches
if self.matchID > -1 and self.matchID != matchID:
self.leaveMatch()
2017-08-12 16:56:31 +00:00
# Try to join match
joined = match.userJoin(self)
if not joined:
self.enqueue(serverPackets.matchJoinFail())
return
2017-08-12 16:56:31 +00:00
# Set matchID, join stream, channel and send packet
self.matchID = matchID
self.joinStream(match.streamName)
chat.joinChannel(token=self, channel="#multi_{}".format(self.matchID), force=True)
2017-08-12 16:56:31 +00:00
self.enqueue(serverPackets.matchJoinSuccess(matchID))
2017-08-12 16:56:31 +00:00
if match.isTourney:
# Alert the user if we have just joined a tourney match
2017-08-12 16:56:31 +00:00
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.
2017-09-09 10:42:49 +00:00
match.sendReadyStatus()
def leaveMatch(self):
"""
Leave joined match, match stream and match channel
:return:
"""
2017-08-12 16:56:31 +00:00
# Make sure we are in a match
if self.matchID == -1:
return
2017-08-12 16:56:31 +00:00
# Part #multiplayer channel and streams (/ and /playing)
chat.partChannel(token=self, channel="#multi_{}".format(self.matchID), kick=True, force=True)
2017-08-12 16:56:31 +00:00
self.leaveStream("multi/{}".format(self.matchID))
self.leaveStream("multi/{}/playing".format(self.matchID)) # optional
2017-08-12 16:56:31 +00:00
# Set usertoken match to -1
leavingMatchID = self.matchID
self.matchID = -1
2017-08-12 16:56:31 +00:00
# Make sure the match exists
if leavingMatchID not in glob.matches.matches:
return
2017-08-12 16:56:31 +00:00
# The match exists, get object
match = glob.matches.matches[leavingMatchID]
2017-08-10 22:45:44 +00:00
2017-08-12 16:56:31 +00:00
# Set slot to free
match.userLeft(self)
if match.isTourney:
# 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
2017-09-09 10:42:49 +00:00
match.sendReadyStatus()
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:
"""
2017-08-11 20:19:39 +00:00
# Send packet to target
log.info("{} has been disconnected. ({})".format(self.username, reason))
if message != "":
self.enqueue(serverPackets.notification(message))
self.enqueue(serverPackets.loginFailed())
2017-08-10 22:45:44 +00:00
2017-08-11 20:19:39 +00:00
# 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:
"""
2017-08-11 20:19:39 +00:00
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)
2017-08-11 20:19:39 +00:00
# Silence token
self.silenceEndTime = int(time.time()) + seconds
2017-08-11 20:19:39 +00:00
# Send silence packet to user
self.enqueue(serverPackets.silenceEndTime(seconds))
2017-08-10 22:45:44 +00:00
2017-08-11 20:19:39 +00:00
# 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:
"""
2017-08-11 20:19:39 +00:00
# Increase the spam rate if needed
if increaseSpamRate:
self.spamRate += 1
2017-08-10 22:45:44 +00:00
2017-08-11 20:19:39 +00:00
# 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:
"""
2017-08-11 20:19:39 +00:00
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)
2016-09-02 15:45:10 +00:00
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:
"""
2017-08-12 06:49:55 +00:00
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:
"""
2017-08-12 06:49:55 +00:00
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)
2016-12-11 22:12:06 +00:00
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]))
2016-12-11 22:12:06 +00:00
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)