mirror of
https://github.com/Sneed-Group/Poodletooth-iLand
synced 2024-12-26 13:12:30 -06:00
1154 lines
No EOL
40 KiB
Python
1154 lines
No EOL
40 KiB
Python
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
|
|
|
|
|
|
# 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)
|
|
|
|
accountServerEndpoint = simbase.config.GetString(
|
|
'account-server-endpoint', 'https://toontownunlimited.com/api/')
|
|
accountServerSecret = simbase.config.GetString(
|
|
'account-server-secret', '6163636f756e7473')
|
|
|
|
|
|
http = HTTPClient()
|
|
http.setVerifySsl(0)
|
|
|
|
|
|
def executeHttpRequest(url, **extras):
|
|
timestamp = str(int(time.time()))
|
|
signature = hmac.new(accountServerSecret, timestamp, hashlib.sha256)
|
|
request = urllib2.Request(accountServerEndpoint + url)
|
|
request.add_header('User-Agent', 'TTU-CSM')
|
|
request.add_header('X-CSM-Timestamp', timestamp)
|
|
request.add_header('X-CSM-Signature', signature.hexdigest())
|
|
for k, v in extras.items():
|
|
request.add_header('X-CSM-' + k, v)
|
|
try:
|
|
return urllib2.urlopen(request).read()
|
|
except:
|
|
return None
|
|
|
|
|
|
blacklist = executeHttpRequest('names/blacklist.json')
|
|
if blacklist:
|
|
blacklist = json.loads(blacklist)
|
|
|
|
|
|
def judgeName(name):
|
|
if not name:
|
|
return False
|
|
if blacklist:
|
|
for namePart in name.split(' '):
|
|
namePart = namePart.lower()
|
|
if len(namePart) < 1:
|
|
return False
|
|
for banned in blacklist.get(namePart[0], []):
|
|
if banned in namePart:
|
|
return False
|
|
return True
|
|
|
|
|
|
# --- ACCOUNT DATABASES ---
|
|
# These classes make up the available account databases for Toontown Unlimited.
|
|
# 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 600 access automatically upon login.
|
|
|
|
|
|
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 'Success'
|
|
|
|
def getNameStatus(self, avId):
|
|
return 'APPROVED'
|
|
|
|
def removeNameRequest(self, avId):
|
|
return 'Success'
|
|
|
|
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 600 access just because they are a
|
|
# developer:
|
|
response = {
|
|
'success': True,
|
|
'userId': username,
|
|
'accountId': 0,
|
|
'accessLevel': max(600, minAccessLevel)
|
|
}
|
|
callback(response)
|
|
return response
|
|
|
|
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
|
|
|
|
|
|
# This is the same as the DeveloperAccountDB, except it doesn't automatically
|
|
# give the user an access level of 600. Instead, the first user that is created
|
|
# gets 700 access, and every user created afterwards gets 100 access:
|
|
|
|
|
|
class LocalAccountDB(AccountDB):
|
|
notify = directNotify.newCategory('LocalAccountDB')
|
|
|
|
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!
|
|
response = {
|
|
'success': True,
|
|
'userId': username,
|
|
'accountId': 0,
|
|
'accessLevel': max((700 if not self.dbm else 100), minAccessLevel)
|
|
}
|
|
callback(response)
|
|
return response
|
|
|
|
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
|
|
|
|
|
|
class RemoteAccountDB(AccountDB):
|
|
notify = directNotify.newCategory('RemoteAccountDB')
|
|
|
|
def addNameRequest(self, avId, name):
|
|
return executeHttpRequest('names/append', ID=str(avId), Name=name)
|
|
|
|
def getNameStatus(self, avId):
|
|
return executeHttpRequest('names/status/?Id=' + str(avId))
|
|
|
|
def removeNameRequest(self, avId):
|
|
return executeHttpRequest('names/remove', ID=str(avId))
|
|
|
|
def lookup(self, token, callback):
|
|
# 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):
|
|
self.notify.warning('Invalid token length!')
|
|
response = {
|
|
'success': False,
|
|
'reason': 'Invalid token length.'
|
|
}
|
|
callback(response)
|
|
return response
|
|
|
|
# Next, decrypt the token using AES-128 in CBC mode:
|
|
accountServerSecret = simbase.config.GetString(
|
|
'account-server-secret', '6163636f756e7473')
|
|
|
|
# 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
|
|
|
|
# Next, check if this token has expired:
|
|
expiration = simbase.config.GetInt('account-token-expiration', 1800)
|
|
tokenDelta = int(time.time()) - token['timestamp']
|
|
if tokenDelta > expiration:
|
|
response = {
|
|
'success': False,
|
|
'reason': 'This token has expired.'
|
|
}
|
|
callback(response)
|
|
return response
|
|
|
|
# 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
|
|
|
|
|
|
# --- 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),
|
|
'ACCESS_LEVEL': self.accessLevel,
|
|
'MONEY': 0
|
|
}
|
|
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)
|
|
|
|
# Add this connection to extra channels which may be useful:
|
|
if self.accessLevel > 100:
|
|
datagram = PyDatagram()
|
|
datagram.addServerHeader(self.target, self.csm.air.ourChannel,
|
|
CLIENTAGENT_OPEN_CHANNEL)
|
|
datagram.addChannel(OtpDoGlobals.OTP_STAFF_CHANNEL)
|
|
self.csm.air.send(datagram)
|
|
|
|
# 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, index):
|
|
# Basic sanity-checking:
|
|
if index >= 6:
|
|
self.demand('Kill', 'Invalid index specified!')
|
|
return
|
|
|
|
if not ToonDNA().isValidNetString(dna):
|
|
self.demand('Kill', 'Invalid DNA specified!')
|
|
return
|
|
|
|
self.index = index
|
|
self.dna = dna
|
|
|
|
# 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))
|
|
toonFields = {
|
|
'setName': (name,),
|
|
'WishNameState': ('OPEN',),
|
|
'WishName': ('',),
|
|
'setDNAString': (self.dna,),
|
|
'setDISLid': (self.target,)
|
|
}
|
|
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:
|
|
resp = self.csm.accountDB.addNameRequest(self.avId, self.name)
|
|
if resp != 'Success':
|
|
status = False
|
|
else:
|
|
self.csm.air.dbInterface.updateObject(
|
|
self.csm.air.dbId,
|
|
self.avId,
|
|
self.csm.air.dclassesByName['DistributedToonUD'],
|
|
{'WishNameState': ('PENDING',),
|
|
'WishName': (self.name,)})
|
|
|
|
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'],
|
|
{'setAdminAccess': [self.account.get('ACCESS_LEVEL', 100)],
|
|
'setBankMoney': [self.account.get('MONEY', 0)]})
|
|
|
|
# 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 TTUFriendsManager somebody is logging off:
|
|
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:
|
|
self.key = '0c603c5833021ce79f734943f6e662250fd4ecf7432bf85905f71707dc4a9370c6ae15a8716302ead43810e5fba3cf0876bbbfce658e2767b88d916f5d89fd31'
|
|
|
|
# Instantiate our account DB interface:
|
|
if accountDBType == 'developer':
|
|
self.accountDB = DeveloperAccountDB(self)
|
|
elif accountDBType == 'local':
|
|
self.accountDB = LocalAccountDB(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(122)
|
|
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, index):
|
|
self.runAccountFSM(CreateAvatarFSM, dna, index)
|
|
|
|
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:
|
|
self.runAccountFSM(UnloadAvatarFSM, currentAvId) |