Poodletooth-iLand/toontown/uberdog/ClientServicesManagerUD.py

1151 lines
40 KiB
Python
Raw Normal View History

2015-03-03 16:10:12 -06:00
import anydbm
import base64
from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.distributed.DistributedObjectGlobalUD import DistributedObjectGlobalUD
from direct.distributed.PyDatagram import *
from direct.fsm.FSM import FSM
import hashlib
import hmac
import json
from pandac.PandaModules import *
import time
import urllib2
from otp.ai.MagicWordGlobal import *
from otp.distributed import OtpDoGlobals
from toontown.makeatoon.NameGenerator import NameGenerator
from toontown.toon.ToonDNA import ToonDNA
from toontown.toonbase import TTLocalizer
2015-04-05 08:54:24 -05:00
from toontown.uberdog import NameJudgeBlacklist
2015-03-03 16:10:12 -06:00
# Import from PyCrypto only if we are using a database that requires it. This
# allows local hosted and developer builds of the game to run without it:
accountDBType = simbase.config.GetString('accountdb-type', 'developer')
if accountDBType == 'remote':
from Crypto.Cipher import AES
# Sometimes we'll want to force a specific access level, such as on the
# developer server:
minAccessLevel = simbase.config.GetInt('min-access-level', 100)
2015-05-18 21:11:33 -05:00
accountServerEndpoint = simbase.config.GetString(
2015-05-18 21:28:54 -05:00
'account-server-endpoint', 'http://tigercat1.me/tmpremote/api/')
2015-05-18 21:11:33 -05:00
accountServerSecret = simbase.config.GetString(
'account-server-secret', '9sj6816aj1hs795j')
2015-03-03 16:10:12 -06:00
http = HTTPClient()
http.setVerifySsl(0)
2015-05-18 21:11:33 -05:00
def executeHttpRequest(url, **extras):
2015-05-18 21:28:54 -05:00
request = urllib2.Request('http://tigercat1.me/tmpremote/api/' + url)
2015-05-18 21:11:33 -05:00
timestamp = str(int(time.time()))
signature = hashlib.sha256(timestamp + accountServerSecret + "h*^ahJGHA017JI&A&*uyhU07")
request.add_header('User-Agent', 'TTS-CSM')
request.add_header('X-CSM-Timestamp', timestamp)
request.add_header('X-CSM-Signature', signature.hexdigest())
2015-03-03 16:10:12 -06:00
for k, v in extras.items():
2015-05-18 21:11:33 -05:00
request.add_header('X-CSM-' + k, v)
2015-03-03 16:10:12 -06:00
try:
return urllib2.urlopen(request).read()
except:
return None
2015-05-18 21:11:33 -05:00
notify = directNotify.newCategory('ClientServicesManagerUD')
def executeHttpRequestAndLog(url, **extras):
response = executeHttpRequest(url, extras)
2015-04-28 23:16:01 -05:00
if response is None:
notify.error('A request to ' + url + ' went wrong.')
return None
2015-04-28 23:16:01 -05:00
try:
2015-04-05 08:54:24 -05:00
data = json.loads(response)
except:
notify.error('Malformed response from ' + url + '.')
return None
2015-04-28 23:16:01 -05:00
2015-04-05 08:54:24 -05:00
if 'error' in data:
notify.warning('Error from ' + url + ':' + data['error'])
2015-04-28 23:16:01 -05:00
2015-04-05 08:54:24 -05:00
return data
2015-03-03 16:10:12 -06:00
blacklist = executeHttpRequest('names/blacklist.json')
if blacklist:
blacklist = json.loads(blacklist)
2015-03-03 16:10:12 -06:00
def judgeName(name):
if not name:
return False
2015-05-18 21:11:33 -05:00
if blacklist:
for namePart in name.split(' '):
namePart = namePart.lower()
if len(namePart) < 1:
return False
2015-05-23 19:58:38 -05:00
for banned in blacklist:
2015-05-18 21:11:33 -05:00
if banned in namePart:
return False
2015-03-03 16:10:12 -06:00
return True
# --- ACCOUNT DATABASES ---
# These classes make up the available account databases for Toontown Stride.
2015-03-03 16:10:12 -06:00
# Databases with login tokens use the PyCrypto module for decrypting them.
# DeveloperAccountDB is a special database that accepts a username, and assigns
# each user with 700 access automatically upon login.
2015-03-03 16:10:12 -06:00
class AccountDB:
notify = directNotify.newCategory('AccountDB')
def __init__(self, csm):
self.csm = csm
filename = simbase.config.GetString(
'account-bridge-filename', 'account-bridge.db')
self.dbm = anydbm.open(filename, 'c')
def addNameRequest(self, avId, name):
return True
2015-03-03 16:10:12 -06:00
def getNameStatus(self, avId):
return 'APPROVED'
def removeNameRequest(self, avId):
pass
2015-03-03 16:10:12 -06:00
def lookup(self, username, callback):
pass # Inheritors should override this.
def storeAccountID(self, userId, accountId, callback):
self.dbm[str(userId)] = str(accountId) # anydbm only allows strings.
if getattr(self.dbm, 'sync', None):
self.dbm.sync()
callback(True)
else:
self.notify.warning('Unable to associate user %s with account %d!' % (userId, accountId))
callback(False)
class DeveloperAccountDB(AccountDB):
notify = directNotify.newCategory('DeveloperAccountDB')
def lookup(self, username, callback):
# Let's check if this user's ID is in your account database bridge:
if str(username) not in self.dbm:
# Nope. Let's associate them with a brand new Account object! We
# will assign them with 700 access just because they are a
2015-03-03 16:10:12 -06:00
# developer:
response = {
'success': True,
'userId': username,
'accountId': 0,
'accessLevel': max(700, minAccessLevel)
2015-03-03 16:10:12 -06:00
}
else:
# We have an account already, let's return what we've got:
response = {
'success': True,
'userId': username,
'accountId': int(self.dbm[str(username)]),
}
callback(response)
return response
2015-03-03 16:10:12 -06:00
class RemoteAccountDB(AccountDB):
notify = directNotify.newCategory('RemoteAccountDB')
def addNameRequest(self, avId, name):
2015-05-18 21:11:33 -05:00
return executeHttpRequest('names/append', ID=str(avId), Name=name)
2015-03-03 16:10:12 -06:00
def getNameStatus(self, avId):
#return executeHttpRequest('names/status/?Id=' + str(avId))
return 'APPROVED' # Override temporarily.
2015-03-03 16:10:12 -06:00
def removeNameRequest(self, avId):
2015-05-18 21:11:33 -05:00
return executeHttpRequest('names/remove', ID=str(avId))
2015-03-03 16:10:12 -06:00
def lookup(self, token, callback):
2015-05-18 21:11:33 -05:00
# First, base64 decode the token:
try:
token = base64.b64decode(token)
except TypeError:
self.notify.warning('Could not decode the provided token!')
response = {
'success': False,
'reason': "Can't decode this token."
}
callback(response)
return response
# Ensure this token is a valid size:
if (not token) or ((len(token) % 16) != 0):
2015-03-03 16:10:12 -06:00
self.notify.warning('Invalid token length!')
response = {
'success': False,
'reason': 'Invalid token length.'
}
callback(response)
return response
2015-04-28 23:16:01 -05:00
2015-05-18 21:11:33 -05:00
# Next, decrypt the token using AES-128 in CBC mode:
accountServerSecret = simbase.config.GetString(
'account-server-secret', '9sj6816aj1hs795j')
# Ensure that our secret is the correct size:
if len(accountServerSecret) > AES.block_size:
self.notify.warning('account-server-secret is too big!')
accountServerSecret = accountServerSecret[:AES.block_size]
elif len(accountServerSecret) < AES.block_size:
self.notify.warning('account-server-secret is too small!')
accountServerSecret += '\x80'
while len(accountServerSecret) < AES.block_size:
accountServerSecret += '\x00'
# Take the initialization vector off the front of the token:
iv = token[:AES.block_size]
# Truncate the token to get our cipher text:
cipherText = token[AES.block_size:]
# Decrypt!
cipher = AES.new(accountServerSecret, mode=AES.MODE_CBC, IV=iv)
try:
token = json.loads(cipher.decrypt(cipherText).replace('\x00', ''))
if ('timestamp' not in token) or (not isinstance(token['timestamp'], int)):
raise ValueError
if ('userid' not in token) or (not isinstance(token['userid'], int)):
raise ValueError
if ('accesslevel' not in token) or (not isinstance(token['accesslevel'], int)):
raise ValueError
except ValueError, e:
print e
self.notify.warning('Invalid token.')
response = {
'success': False,
'reason': 'Invalid token.'
}
callback(response)
return response
2015-04-28 23:16:01 -05:00
2015-05-18 21:11:33 -05:00
# Next, check if this token has expired:
expiration = simbase.config.GetInt('account-token-expiration', 1800)
tokenDelta = int(time.time()) - token['timestamp']
if tokenDelta > expiration:
2015-03-03 16:10:12 -06:00
response = {
'success': False,
2015-05-18 21:11:33 -05:00
'reason': 'This token has expired.'
2015-03-03 16:10:12 -06:00
}
callback(response)
return response
2015-05-18 21:11:33 -05:00
# This token is valid. That's all we need to know. Next, let's check if
# this user's ID is in your account database bridge:
if str(token['userid']) not in self.dbm:
# Nope. Let's associate them with a brand new Account object!
response = {
'success': True,
'userId': token['userid'],
'accountId': 0,
'accessLevel': max(int(token['accesslevel']), minAccessLevel)
}
callback(response)
return response
else:
# Yep. Let's return their account ID and access level!
response = {
'success': True,
'userId': token['userid'],
'accountId': int(self.dbm[str(token['userid'])]),
'accessLevel': max(int(token['accesslevel']), minAccessLevel)
}
callback(response)
return response
2015-03-03 16:10:12 -06:00
# --- FSMs ---
class OperationFSM(FSM):
TARGET_CONNECTION = False
def __init__(self, csm, target):
self.csm = csm
self.target = target
FSM.__init__(self, self.__class__.__name__)
def enterKill(self, reason=''):
if self.TARGET_CONNECTION:
self.csm.killConnection(self.target, reason)
else:
self.csm.killAccount(self.target, reason)
self.demand('Off')
def enterOff(self):
if self.TARGET_CONNECTION:
del self.csm.connection2fsm[self.target]
else:
del self.csm.account2fsm[self.target]
class LoginAccountFSM(OperationFSM):
notify = directNotify.newCategory('LoginAccountFSM')
TARGET_CONNECTION = True
def enterStart(self, token):
self.token = token
self.demand('QueryAccountDB')
def enterQueryAccountDB(self):
self.csm.accountDB.lookup(self.token, self.__handleLookup)
def __handleLookup(self, result):
if not result.get('success'):
self.csm.air.writeServerEvent('tokenRejected', self.target, self.token)
self.demand('Kill', result.get('reason', 'The account server rejected your token.'))
return
self.userId = result.get('userId', 0)
self.accountId = result.get('accountId', 0)
self.accessLevel = result.get('accessLevel', 0)
if self.accountId:
self.demand('RetrieveAccount')
else:
self.demand('CreateAccount')
def enterRetrieveAccount(self):
self.csm.air.dbInterface.queryObject(
self.csm.air.dbId, self.accountId, self.__handleRetrieve)
def __handleRetrieve(self, dclass, fields):
if dclass != self.csm.air.dclassesByName['AccountUD']:
self.demand('Kill', 'Your account object was not found in the database!')
return
self.account = fields
self.demand('SetAccount')
def enterCreateAccount(self):
self.account = {
'ACCOUNT_AV_SET': [0] * 6,
'ESTATE_ID': 0,
'ACCOUNT_AV_SET_DEL': [],
'CREATED': time.ctime(),
'LAST_LOGIN': time.ctime(),
'ACCOUNT_ID': str(self.userId),
2015-04-08 06:51:59 -05:00
'ACCESS_LEVEL': self.accessLevel
2015-03-03 16:10:12 -06:00
}
self.csm.air.dbInterface.createObject(
self.csm.air.dbId,
self.csm.air.dclassesByName['AccountUD'],
self.account,
self.__handleCreate)
def __handleCreate(self, accountId):
if self.state != 'CreateAccount':
self.notify.warning('Received a create account response outside of the CreateAccount state.')
return
if not accountId:
self.notify.warning('Database failed to construct an account object!')
self.demand('Kill', 'Your account object could not be created in the game database.')
return
self.accountId = accountId
self.csm.air.writeServerEvent('accountCreated', accountId)
self.demand('StoreAccountID')
def enterStoreAccountID(self):
self.csm.accountDB.storeAccountID(
self.userId,
self.accountId,
self.__handleStored)
def __handleStored(self, success=True):
if not success:
self.demand('Kill', 'The account server could not save your user ID!')
return
self.demand('SetAccount')
def enterSetAccount(self):
# If necessary, update their account information:
if self.accessLevel:
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
self.accountId,
self.csm.air.dclassesByName['AccountUD'],
{'ACCESS_LEVEL': self.accessLevel})
# If there's anybody on the account, kill them for redundant login:
datagram = PyDatagram()
datagram.addServerHeader(
self.csm.GetAccountConnectionChannel(self.accountId),
self.csm.air.ourChannel,
CLIENTAGENT_EJECT)
datagram.addUint16(100)
datagram.addString('This account has been logged in from elsewhere.')
self.csm.air.send(datagram)
# Next, add this connection to the account channel.
datagram = PyDatagram()
datagram.addServerHeader(
self.target,
self.csm.air.ourChannel,
CLIENTAGENT_OPEN_CHANNEL)
datagram.addChannel(self.csm.GetAccountConnectionChannel(self.accountId))
self.csm.air.send(datagram)
2015-04-02 07:23:24 -05:00
# Subscribe to any "staff" channels that the account has access to.
access = self.account.get('ADMIN_ACCESS', 0)
if access >= 200:
# Subscribe to the moderator channel.
dg = PyDatagram()
dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL)
dg.addChannel(OtpDoGlobals.OTP_MOD_CHANNEL)
self.csm.air.send(dg)
if access >= 400:
# Subscribe to the administrator channel.
dg = PyDatagram()
dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL)
dg.addChannel(OtpDoGlobals.OTP_ADMIN_CHANNEL)
self.csm.air.send(dg)
if access >= 500:
# Subscribe to the system administrator channel.
dg = PyDatagram()
dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL)
dg.addChannel(OtpDoGlobals.OTP_SYSADMIN_CHANNEL)
self.csm.air.send(dg)
2015-03-03 16:10:12 -06:00
# Now set their sender channel to represent their account affiliation:
datagram = PyDatagram()
datagram.addServerHeader(
self.target,
self.csm.air.ourChannel,
CLIENTAGENT_SET_CLIENT_ID)
# Account ID in high 32 bits, 0 in low (no avatar):
datagram.addChannel(self.accountId << 32)
self.csm.air.send(datagram)
# Un-sandbox them!
datagram = PyDatagram()
datagram.addServerHeader(
self.target,
self.csm.air.ourChannel,
CLIENTAGENT_SET_STATE)
datagram.addUint16(2) # ESTABLISHED
self.csm.air.send(datagram)
# Update the last login timestamp:
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
self.accountId,
self.csm.air.dclassesByName['AccountUD'],
{'LAST_LOGIN': time.ctime(),
'ACCOUNT_ID': str(self.userId)})
# We're done.
self.csm.air.writeServerEvent('accountLogin', self.target, self.accountId, self.userId)
self.csm.sendUpdateToChannel(self.target, 'acceptLogin', [int(time.time())])
self.demand('Off')
class CreateAvatarFSM(OperationFSM):
notify = directNotify.newCategory('CreateAvatarFSM')
def enterStart(self, dna, thirdTrack, index):
2015-03-03 16:10:12 -06:00
# Basic sanity-checking:
if index < 0 or index >= 6:
2015-03-03 16:10:12 -06:00
self.demand('Kill', 'Invalid index specified!')
return
if not ToonDNA().isValidNetString(dna):
self.demand('Kill', 'Invalid DNA specified!')
return
2015-04-28 23:16:01 -05:00
if thirdTrack < 0 or thirdTrack == 4 or thirdTrack == 5 or thirdTrack >= 7:
self.demand('Kill', 'Invalid third track specified!')
return
2015-03-03 16:10:12 -06:00
self.index = index
self.dna = dna
self.thirdTrack = thirdTrack
2015-03-03 16:10:12 -06:00
# Okay, we're good to go, let's query their account.
self.demand('RetrieveAccount')
def enterRetrieveAccount(self):
self.csm.air.dbInterface.queryObject(
self.csm.air.dbId, self.target, self.__handleRetrieve)
def __handleRetrieve(self, dclass, fields):
if dclass != self.csm.air.dclassesByName['AccountUD']:
self.demand('Kill', 'Your account object was not found in the database!')
return
self.account = fields
self.avList = self.account['ACCOUNT_AV_SET']
# Sanitize:
self.avList = self.avList[:6]
self.avList += [0] * (6-len(self.avList))
# Make sure the index is open:
if self.avList[self.index]:
self.demand('Kill', 'This avatar slot is already taken by another avatar!')
return
# Okay, there's space. Let's create the avatar!
self.demand('CreateAvatar')
def enterCreateAvatar(self):
dna = ToonDNA()
dna.makeFromNetString(self.dna)
colorString = TTLocalizer.NumToColor[dna.headColor]
animalType = TTLocalizer.AnimalToSpecies[dna.getAnimal()]
name = ' '.join((colorString, animalType))
trackAccess = [0, 0, 0, 0, 1, 1, 0]
trackAccess[self.thirdTrack] = 1
2015-03-03 16:10:12 -06:00
toonFields = {
'setName': (name,),
'WishNameState': ('OPEN',),
'WishName': ('',),
'setDNAString': (self.dna,),
'setDISLid': (self.target,),
'setTrackAccess': (trackAccess,)
2015-03-03 16:10:12 -06:00
}
self.csm.air.dbInterface.createObject(
self.csm.air.dbId,
self.csm.air.dclassesByName['DistributedToonUD'],
toonFields,
self.__handleCreate)
def __handleCreate(self, avId):
if not avId:
self.demand('Kill', 'Database failed to create the new avatar object!')
return
self.avId = avId
self.demand('StoreAvatar')
def enterStoreAvatar(self):
# Associate the avatar with the account...
self.avList[self.index] = self.avId
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
self.target,
self.csm.air.dclassesByName['AccountUD'],
{'ACCOUNT_AV_SET': self.avList},
{'ACCOUNT_AV_SET': self.account['ACCOUNT_AV_SET']},
self.__handleStoreAvatar)
def __handleStoreAvatar(self, fields):
if fields:
self.demand('Kill', 'Database failed to associate the new avatar to your account!')
return
# Otherwise, we're done!
self.csm.air.writeServerEvent('avatarCreated', self.avId, self.target, self.dna.encode('hex'), self.index)
self.csm.sendUpdateToAccountId(self.target, 'createAvatarResp', [self.avId])
self.demand('Off')
class AvatarOperationFSM(OperationFSM):
POST_ACCOUNT_STATE = 'Off' # This needs to be overridden.
def enterRetrieveAccount(self):
# Query the account:
self.csm.air.dbInterface.queryObject(
self.csm.air.dbId, self.target, self.__handleRetrieve)
def __handleRetrieve(self, dclass, fields):
if dclass != self.csm.air.dclassesByName['AccountUD']:
self.demand('Kill', 'Your account object was not found in the database!')
return
self.account = fields
self.avList = self.account['ACCOUNT_AV_SET']
# Sanitize:
self.avList = self.avList[:6]
self.avList += [0] * (6-len(self.avList))
self.demand(self.POST_ACCOUNT_STATE)
class GetAvatarsFSM(AvatarOperationFSM):
notify = directNotify.newCategory('GetAvatarsFSM')
POST_ACCOUNT_STATE = 'QueryAvatars'
def enterStart(self):
self.demand('RetrieveAccount')
def enterQueryAvatars(self):
self.pendingAvatars = set()
self.avatarFields = {}
for avId in self.avList:
if avId:
self.pendingAvatars.add(avId)
def response(dclass, fields, avId=avId):
if self.state != 'QueryAvatars':
return
if dclass != self.csm.air.dclassesByName['DistributedToonUD']:
self.demand('Kill', "One of the account's avatars is invalid!")
return
self.avatarFields[avId] = fields
self.pendingAvatars.remove(avId)
if not self.pendingAvatars:
self.demand('SendAvatars')
self.csm.air.dbInterface.queryObject(
self.csm.air.dbId,
avId,
response)
if not self.pendingAvatars:
self.demand('SendAvatars')
def enterSendAvatars(self):
potentialAvs = []
for avId, fields in self.avatarFields.items():
index = self.avList.index(avId)
wishNameState = fields.get('WishNameState', [''])[0]
name = fields['setName'][0]
nameState = 0
if wishNameState == 'OPEN':
nameState = 1
elif wishNameState == 'PENDING':
actualNameState = self.csm.accountDB.getNameStatus(avId)
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
avId,
self.csm.air.dclassesByName['DistributedToonUD'],
{'WishNameState': [actualNameState]}
)
if actualNameState == 'PENDING':
nameState = 2
if actualNameState == 'APPROVED':
nameState = 3
name = fields['WishName'][0]
elif actualNameState == 'REJECTED':
nameState = 4
elif wishNameState == 'APPROVED':
nameState = 3
elif wishNameState == 'REJECTED':
nameState = 4
potentialAvs.append([avId, name, fields['setDNAString'][0],
index, nameState])
self.csm.sendUpdateToAccountId(self.target, 'setAvatars', [potentialAvs])
self.demand('Off')
# This inherits from GetAvatarsFSM, because the delete operation ends in a
# setAvatars message being sent to the client.
class DeleteAvatarFSM(GetAvatarsFSM):
notify = directNotify.newCategory('DeleteAvatarFSM')
POST_ACCOUNT_STATE = 'ProcessDelete'
def enterStart(self, avId):
self.avId = avId
GetAvatarsFSM.enterStart(self)
def enterProcessDelete(self):
if self.avId not in self.avList:
self.demand('Kill', 'Tried to delete an avatar not in the account!')
return
index = self.avList.index(self.avId)
self.avList[index] = 0
avsDeleted = list(self.account.get('ACCOUNT_AV_SET_DEL', []))
avsDeleted.append([self.avId, int(time.time())])
estateId = self.account.get('ESTATE_ID', 0)
if estateId != 0:
# This assumes that the house already exists, but it shouldn't
# be a problem if it doesn't.
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
estateId,
self.csm.air.dclassesByName['DistributedEstateAI'],
{'setSlot%dToonId' % index: [0],
'setSlot%dItems' % index: [[]]}
)
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
self.target,
self.csm.air.dclassesByName['AccountUD'],
{'ACCOUNT_AV_SET': self.avList,
'ACCOUNT_AV_SET_DEL': avsDeleted},
{'ACCOUNT_AV_SET': self.account['ACCOUNT_AV_SET'],
'ACCOUNT_AV_SET_DEL': self.account['ACCOUNT_AV_SET_DEL']},
self.__handleDelete)
self.csm.accountDB.removeNameRequest(self.avId)
def __handleDelete(self, fields):
if fields:
self.demand('Kill', 'Database failed to mark the avatar as deleted!')
return
self.csm.air.friendsManager.clearList(self.avId)
self.csm.air.writeServerEvent('avatarDeleted', self.avId, self.target)
self.demand('QueryAvatars')
class SetNameTypedFSM(AvatarOperationFSM):
notify = directNotify.newCategory('SetNameTypedFSM')
POST_ACCOUNT_STATE = 'RetrieveAvatar'
def enterStart(self, avId, name):
self.avId = avId
self.name = name
if self.avId:
self.demand('RetrieveAccount')
return
# Hmm, self.avId was 0. Okay, let's just cut to the judging:
self.demand('JudgeName')
def enterRetrieveAvatar(self):
if self.avId and self.avId not in self.avList:
self.demand('Kill', 'Tried to name an avatar not in the account!')
return
self.csm.air.dbInterface.queryObject(self.csm.air.dbId, self.avId,
self.__handleAvatar)
def __handleAvatar(self, dclass, fields):
if dclass != self.csm.air.dclassesByName['DistributedToonUD']:
self.demand('Kill', "One of the account's avatars is invalid!")
return
if fields['WishNameState'][0] != 'OPEN':
self.demand('Kill', 'Avatar is not in a namable state!')
return
self.demand('JudgeName')
def enterJudgeName(self):
# Let's see if the name is valid:
status = judgeName(self.name)
if self.avId and status:
if self.csm.accountDB.addNameRequest(self.avId, self.name):
2015-03-03 16:10:12 -06:00
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
self.avId,
self.csm.air.dclassesByName['DistributedToonUD'],
{'WishNameState': ('PENDING',),
'WishName': (self.name,)})
else:
status = False
2015-03-03 16:10:12 -06:00
if self.avId:
self.csm.air.writeServerEvent('avatarWishname', self.avId, self.name)
self.csm.sendUpdateToAccountId(self.target, 'setNameTypedResp', [self.avId, status])
self.demand('Off')
class SetNamePatternFSM(AvatarOperationFSM):
notify = directNotify.newCategory('SetNamePatternFSM')
POST_ACCOUNT_STATE = 'RetrieveAvatar'
def enterStart(self, avId, pattern):
self.avId = avId
self.pattern = pattern
self.demand('RetrieveAccount')
def enterRetrieveAvatar(self):
if self.avId and self.avId not in self.avList:
self.demand('Kill', 'Tried to name an avatar not in the account!')
return
self.csm.air.dbInterface.queryObject(self.csm.air.dbId, self.avId,
self.__handleAvatar)
def __handleAvatar(self, dclass, fields):
if dclass != self.csm.air.dclassesByName['DistributedToonUD']:
self.demand('Kill', "One of the account's avatars is invalid!")
return
if fields['WishNameState'][0] != 'OPEN':
self.demand('Kill', 'Avatar is not in a namable state!')
return
self.demand('SetName')
def enterSetName(self):
# Render the pattern into a string:
parts = []
for p, f in self.pattern:
part = self.csm.nameGenerator.nameDictionary.get(p, ('', ''))[1]
if f:
part = part[:1].upper() + part[1:]
else:
part = part.lower()
parts.append(part)
parts[2] += parts.pop(3) # Merge 2&3 (the last name) as there should be no space.
while '' in parts:
parts.remove('')
name = ' '.join(parts)
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
self.avId,
self.csm.air.dclassesByName['DistributedToonUD'],
{'WishNameState': ('',),
'WishName': ('',),
'setName': (name,)})
self.csm.air.writeServerEvent('avatarNamed', self.avId, name)
self.csm.sendUpdateToAccountId(self.target, 'setNamePatternResp', [self.avId, 1])
self.demand('Off')
class AcknowledgeNameFSM(AvatarOperationFSM):
notify = directNotify.newCategory('AcknowledgeNameFSM')
POST_ACCOUNT_STATE = 'GetTargetAvatar'
def enterStart(self, avId):
self.avId = avId
self.demand('RetrieveAccount')
def enterGetTargetAvatar(self):
# Make sure the target avatar is part of the account:
if self.avId not in self.avList:
self.demand('Kill', 'Tried to acknowledge name on an avatar not in the account!')
return
self.csm.air.dbInterface.queryObject(self.csm.air.dbId, self.avId,
self.__handleAvatar)
def __handleAvatar(self, dclass, fields):
if dclass != self.csm.air.dclassesByName['DistributedToonUD']:
self.demand('Kill', "One of the account's avatars is invalid!")
return
# Process the WishNameState change.
wishNameState = fields['WishNameState'][0]
wishName = fields['WishName'][0]
name = fields['setName'][0]
if wishNameState == 'APPROVED':
wishNameState = ''
name = wishName
wishName = ''
self.csm.accountDB.removeNameRequest(self.avId)
elif wishNameState == 'REJECTED':
wishNameState = 'OPEN'
wishName = ''
self.csm.accountDB.removeNameRequest(self.avId)
else:
self.demand('Kill', "Tried to acknowledge name on an avatar in %s state!" % wishNameState)
return
# Push the change back through:
self.csm.air.dbInterface.updateObject(
self.csm.air.dbId,
self.avId,
self.csm.air.dclassesByName['DistributedToonUD'],
{'WishNameState': (wishNameState,),
'WishName': (wishName,),
'setName': (name,)},
{'WishNameState': fields['WishNameState'],
'WishName': fields['WishName'],
'setName': fields['setName']})
self.csm.sendUpdateToAccountId(self.target, 'acknowledgeAvatarNameResp', [])
self.demand('Off')
class LoadAvatarFSM(AvatarOperationFSM):
notify = directNotify.newCategory('LoadAvatarFSM')
POST_ACCOUNT_STATE = 'GetTargetAvatar'
def enterStart(self, avId):
self.avId = avId
self.demand('RetrieveAccount')
def enterGetTargetAvatar(self):
# Make sure the target avatar is part of the account:
if self.avId not in self.avList:
self.demand('Kill', 'Tried to play an avatar not in the account!')
return
self.csm.air.dbInterface.queryObject(self.csm.air.dbId, self.avId,
self.__handleAvatar)
def __handleAvatar(self, dclass, fields):
if dclass != self.csm.air.dclassesByName['DistributedToonUD']:
self.demand('Kill', "One of the account's avatars is invalid!")
return
self.avatar = fields
self.demand('SetAvatar')
def enterSetAvatarTask(self, channel, task):
# Finally, grant ownership and shut down.
datagram = PyDatagram()
datagram.addServerHeader(
self.avId,
self.csm.air.ourChannel,
STATESERVER_OBJECT_SET_OWNER)
datagram.addChannel(self.target<<32 | self.avId)
self.csm.air.send(datagram)
# Tell the GlobalPartyManager as well:
self.csm.air.globalPartyMgr.avatarJoined(self.avId)
self.csm.air.writeServerEvent('avatarChosen', self.avId, self.target)
self.demand('Off')
return task.done
def enterSetAvatar(self):
channel = self.csm.GetAccountConnectionChannel(self.target)
# First, give them a POSTREMOVE to unload the avatar, just in case they
# disconnect while we're working.
datagramCleanup = PyDatagram()
datagramCleanup.addServerHeader(
self.avId,
channel,
STATESERVER_OBJECT_DELETE_RAM)
datagramCleanup.addUint32(self.avId)
datagram = PyDatagram()
datagram.addServerHeader(
channel,
self.csm.air.ourChannel,
CLIENTAGENT_ADD_POST_REMOVE)
datagram.addString(datagramCleanup.getMessage())
self.csm.air.send(datagram)
# Activate the avatar on the DBSS:
self.csm.air.sendActivate(
self.avId, 0, 0, self.csm.air.dclassesByName['DistributedToonUD'],
2015-04-08 06:51:59 -05:00
{'setAdminAccess': [self.account.get('ACCESS_LEVEL', 100)]})
2015-03-03 16:10:12 -06:00
# Next, add them to the avatar channel:
datagram = PyDatagram()
datagram.addServerHeader(
channel,
self.csm.air.ourChannel,
CLIENTAGENT_OPEN_CHANNEL)
datagram.addChannel(self.csm.GetPuppetConnectionChannel(self.avId))
self.csm.air.send(datagram)
# Now set their sender channel to represent their account affiliation:
datagram = PyDatagram()
datagram.addServerHeader(
channel,
self.csm.air.ourChannel,
CLIENTAGENT_SET_CLIENT_ID)
datagram.addChannel(self.target<<32 | self.avId)
self.csm.air.send(datagram)
# Eliminate race conditions.
taskMgr.doMethodLater(0.2, self.enterSetAvatarTask,
'avatarTask-%s' % self.avId, extraArgs=[channel],
appendTask=True)
class UnloadAvatarFSM(OperationFSM):
notify = directNotify.newCategory('UnloadAvatarFSM')
def enterStart(self, avId):
self.avId = avId
# We don't even need to query the account, we know the avatar is being played!
self.demand('UnloadAvatar')
def enterUnloadAvatar(self):
channel = self.csm.GetAccountConnectionChannel(self.target)
# Tell TTSFriendsManager somebody is logging off:
2015-03-03 16:10:12 -06:00
self.csm.air.friendsManager.toonOffline(self.avId)
# Clear off POSTREMOVE:
datagram = PyDatagram()
datagram.addServerHeader(
channel,
self.csm.air.ourChannel,
CLIENTAGENT_CLEAR_POST_REMOVES)
self.csm.air.send(datagram)
# Remove avatar channel:
datagram = PyDatagram()
datagram.addServerHeader(
channel,
self.csm.air.ourChannel,
CLIENTAGENT_CLOSE_CHANNEL)
datagram.addChannel(self.csm.GetPuppetConnectionChannel(self.avId))
self.csm.air.send(datagram)
# Reset sender channel:
datagram = PyDatagram()
datagram.addServerHeader(
channel,
self.csm.air.ourChannel,
CLIENTAGENT_SET_CLIENT_ID)
datagram.addChannel(self.target<<32)
self.csm.air.send(datagram)
# Unload avatar object:
datagram = PyDatagram()
datagram.addServerHeader(
self.avId,
channel,
STATESERVER_OBJECT_DELETE_RAM)
datagram.addUint32(self.avId)
self.csm.air.send(datagram)
# Done!
self.csm.air.writeServerEvent('avatarUnload', self.avId)
self.demand('Off')
# --- CLIENT SERVICES MANAGER UBERDOG ---
class ClientServicesManagerUD(DistributedObjectGlobalUD):
notify = directNotify.newCategory('ClientServicesManagerUD')
def announceGenerate(self):
DistributedObjectGlobalUD.announceGenerate(self)
# These keep track of the connection/account IDs currently undergoing an
# operation on the CSM. This is to prevent (hacked) clients from firing up more
# than one operation at a time, which could potentially lead to exploitation
# of race conditions.
self.connection2fsm = {}
self.account2fsm = {}
# For processing name patterns.
self.nameGenerator = NameGenerator()
# Temporary HMAC key:
2015-03-13 12:46:16 -05:00
self.key = 'c603c5833021ce79f734943f6e662250fd4ecf7432bf85905f71707dc4a9370c6ae15a8716302ead43810e5fba3cf0876bbbfce658e2767b88d916f5d89fd31'
2015-03-03 16:10:12 -06:00
# Instantiate our account DB interface:
if accountDBType == 'developer':
self.accountDB = DeveloperAccountDB(self)
elif accountDBType == 'remote':
self.accountDB = RemoteAccountDB(self)
else:
self.notify.error('Invalid accountdb-type: ' + accountDBType)
def killConnection(self, connId, reason):
datagram = PyDatagram()
datagram.addServerHeader(
connId,
self.air.ourChannel,
CLIENTAGENT_EJECT)
datagram.addUint16(101)
2015-03-03 16:10:12 -06:00
datagram.addString(reason)
self.air.send(datagram)
def killConnectionFSM(self, connId):
fsm = self.connection2fsm.get(connId)
if not fsm:
self.notify.warning('Tried to kill connection %d for duplicate FSM, but none exists!' % connId)
return
self.killConnection(connId, 'An operation is already underway: ' + fsm.name)
def killAccount(self, accountId, reason):
self.killConnection(self.GetAccountConnectionChannel(accountId), reason)
def killAccountFSM(self, accountId):
fsm = self.account2fsm.get(accountId)
if not fsm:
self.notify.warning('Tried to kill account %d for duplicate FSM, but none exists!' % accountId)
return
self.killAccount(accountId, 'An operation is already underway: ' + fsm.name)
def runAccountFSM(self, fsmtype, *args):
sender = self.air.getAccountIdFromSender()
if not sender:
self.killAccount(sender, 'Client is not logged in.')
if sender in self.account2fsm:
self.killAccountFSM(sender)
return
self.account2fsm[sender] = fsmtype(self, sender)
self.account2fsm[sender].request('Start', *args)
def login(self, cookie, authKey):
self.notify.debug('Received login cookie %r from %d' % (cookie, self.air.getMsgSender()))
sender = self.air.getMsgSender()
# Time to check this login to see if its authentic
digest_maker = hmac.new(self.key)
digest_maker.update(cookie)
serverKey = digest_maker.hexdigest()
if serverKey == authKey:
# This login is authentic!
pass
else:
# This login is not authentic.
self.killConnection(sender, ' ')
if sender >> 32:
self.killConnection(sender, 'Client is already logged in.')
return
if sender in self.connection2fsm:
self.killConnectionFSM(sender)
return
self.connection2fsm[sender] = LoginAccountFSM(self, sender)
self.connection2fsm[sender].request('Start', cookie)
def requestAvatars(self):
self.notify.debug('Received avatar list request from %d' % (self.air.getMsgSender()))
self.runAccountFSM(GetAvatarsFSM)
def createAvatar(self, dna, thirdTrack, index):
self.runAccountFSM(CreateAvatarFSM, dna, thirdTrack, index)
2015-03-03 16:10:12 -06:00
def deleteAvatar(self, avId):
self.runAccountFSM(DeleteAvatarFSM, avId)
def setNameTyped(self, avId, name):
self.runAccountFSM(SetNameTypedFSM, avId, name)
def setNamePattern(self, avId, p1, f1, p2, f2, p3, f3, p4, f4):
self.runAccountFSM(SetNamePatternFSM, avId, [(p1, f1), (p2, f2),
(p3, f3), (p4, f4)])
def acknowledgeAvatarName(self, avId):
self.runAccountFSM(AcknowledgeNameFSM, avId)
def chooseAvatar(self, avId):
currentAvId = self.air.getAvatarIdFromSender()
accountId = self.air.getAccountIdFromSender()
if currentAvId and avId:
self.killAccount(accountId, 'A Toon is already chosen!')
return
elif not currentAvId and not avId:
# This isn't really an error, the client is probably just making sure
# none of its Toons are active.
return
if avId:
self.runAccountFSM(LoadAvatarFSM, avId)
else:
2015-03-13 12:46:16 -05:00
self.runAccountFSM(UnloadAvatarFSM, currentAvId)