oldschool-toontown/otp/friends/FriendManagerAI.py
2024-06-25 21:00:27 -05:00

752 lines
No EOL
30 KiB
Python

from otp.ai.AIBaseGlobal import *
from pandac.PandaModules import *
from direct.distributed import DistributedObjectAI
from direct.directnotify import DirectNotifyGlobal
from direct.distributed.PyDatagram import *
from otp.avatar import DistributedAvatarAI
from otp.otpbase import OTPGlobals
import datetime
import json
import random
import os
# all of this is commented out because the friend manager was moved
# to the OTP server
# yeah which we don't have access too lul so lets uncomment it out
# we need something for the AI DC parser to load
class FriendManagerAI(DistributedObjectAI.DistributedObjectAI):
# These structures record the invitations currently being handled.
nextContext = 0
invites = {}
inviters = {}
invitees = {}
# This is the length of time, in seconds, to sit on a secret guess
# before processing it. This serves to make it difficult to guess
# passwords at random.
SecretDelay = 1.0
# This is the length of time that should elapse before we start to
# forget who has declined friendships from whom.
DeclineFriendshipTimeout = 600.0
notify = DirectNotifyGlobal.directNotify.newCategory("FriendManagerAI")
friendDataFolder = simbase.config.GetString('server-data-folder', 'dependencies/backups/')
# This subclass is used to record currently outstanding
# in-the-game invitation requests.
class Invite:
def __init__(self, context, inviterId, inviteeId):
self.context = context
self.inviterId = inviterId
self.inviteeId = inviteeId
self.inviter = None
self.invitee = None
self.inviteeKnows = 0
self.sendSpecialResponse = 0
def __init__(self, air):
DistributedObjectAI.DistributedObjectAI.__init__(self, air)
# We maintain two maps of toons who have declined
# friendships. We add entries to map1, and every ten
# minutes, we roll map1 into map2. This provides a
# timeout of ten to twenty minutes for a particular
# rejection, and also prevents the maps from growing very
# large in memory.
self.declineFriends1 = {}
self.declineFriends2 = {}
self.lastRollTime = 0
self.filename = self.getFilename()
self.trueFriendCodes = self.loadTrueFriendCodes()
self.district = str(self.air.districtId)
taskMgr.add(self.trueFriendCodesTask, 'truefriend-codes-clear-task')
def generate(self):
DistributedObjectAI.DistributedObjectAI.generate(self)
# The FriendManagerAI always listens for these events, which
# will be sent in response to secret requests to the database,
# via the AIR.
self.accept("makeFriendsReply", self.makeFriendsReply)
self.accept("requestSecretReply", self.__requestSecretReply)
self.accept("submitSecretReply", self.__submitSecretReply)
def delete(self):
self.ignore("makeFriendsReply")
self.ignore("requestSecretReply")
self.ignore("submitSecretReply")
# Clean up all outstanding secret request tasks.
taskMgr.removeTasksMatching("secret-*")
DistributedObjectAI.DistributedObjectAI.delete(self)
# Messages sent from inviter client to AI
def friendQuery(self, inviteeId):
"""friendQuery(self, int inviterId, int inviteeId)
Sent by the inviter to the AI to initiate a friendship
request.
"""
inviterId = self.air.getAvatarIdFromSender()
invitee = self.air.doId2do.get(inviteeId)
# see if the inviteeId is valid
if not invitee:
self.air.writeServerEvent('suspicious', inviteeId, 'FriendManagerAI.friendQuery not on list')
return
self.notify.debug("AI: friendQuery(%d, %d)" % (inviterId, inviteeId))
self.newInvite(inviterId, inviteeId)
def cancelFriendQuery(self, context):
"""cancelFriendQuery(self, int context)
Sent by the inviter to the AI to cancel a pending friendship
request.
"""
avId = self.air.getAvatarIdFromSender()
self.notify.debug("AI: cancelFriendQuery(%d)" % (context))
try:
invite = FriendManagerAI.invites[context]
except:
# The client might legitimately try to cancel a context
# that has already been cancelled.
#self.air.writeServerEvent('suspicious', avId, 'FriendManagerAI.cancelFriendQuery unknown context')
#FriendManagerAI.notify.warning('Message for unknown context ' + `context`)
return
self.cancelInvite(invite)
# Messages sent from invitee client to AI
def inviteeFriendConsidering(self, response, context):
"""inviteeFriendConsidering(self, int response, int context)
Sent by the invitee to the AI to indicate whether the invitee
is able to consider the request right now.
The responses are:
0 - no
1 - yes
4 - the invitee is ignoring you.
"""
self.notify.debug("AI: inviteeFriendConsidering(%d, %d)" % (response, context))
avId = self.air.getAvatarIdFromSender()
try:
invite = FriendManagerAI.invites[context]
except:
self.air.writeServerEvent('suspicious', avId, 'FriendManagerAI.inviteeFriendConsidering unknown context')
FriendManagerAI.notify.warning('Message for unknown context ' + context)
return
if response == 1:
self.inviteeAvailable(invite)
else:
self.inviteeUnavailable(invite, response)
def inviteeFriendResponse(self, yesNoMaybe, context):
"""inviteeFriendResponse(self, int yesNoMaybe, int context)
Sent by the invitee to the AI, following an affirmative
response in inviteeFriendConsidering, to indicate whether or
not the user decided to accept the friendship.
"""
self.notify.debug("AI: inviteeFriendResponse(%d, %d)" % (yesNoMaybe, context))
avId = self.air.getAvatarIdFromSender()
if not avId:
return
try:
invite = FriendManagerAI.invites[context]
except:
self.air.writeServerEvent('suspicious', avId, 'FriendManagerAI.inviteeFriendResponse unknown context')
FriendManagerAI.notify.warning('Message for unknown context ' + context)
return
if yesNoMaybe == 1:
invitee = invite.inviteeId
if invitee in self.air.doId2do:
invitee = self.air.doId2do[invitee]
else:
del FriendManagerAI.invites[context]
return
inviter = invite.inviterId
if not (invitee and inviter):
#probably logged off before a response
return
if inviter in self.air.doId2do:
inviter = self.air.doId2do[inviter]
else:
del FriendManagerAI.invites[context]
return
dg = PyDatagram()
dg.addServerHeader(self.GetPuppetConnectionChannel(invitee.getDoId()), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
dg.addUint32(inviter.getDoId())
dg.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(dg)
dg = PyDatagram()
dg.addServerHeader(self.GetPuppetConnectionChannel(inviter.getDoId()), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
dg.addUint32(invitee.getDoId())
dg.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(dg)
invitee.extendFriendsList(inviter.getDoId(), 0)
inviter.extendFriendsList(invitee.getDoId(), 0)
invitee.d_setFriendsList(invitee.getFriendsList())
inviter.d_setFriendsList(inviter.getFriendsList())
del FriendManagerAI.invites[context]
# else:
# self.noFriends(invite, yesNoMaybe)
def inviteeAcknowledgeCancel(self, context):
"""inviteeAcknowledgeCancel(self, int context)
Sent by the invitee to the AI, in response to an
inviteeCancelFriendQuery message. This simply acknowledges
receipt of the message and tells the AI that it is safe to
clean up the context.
"""
self.notify.debug("AI: inviteeAcknowledgeCancel(%d)" % (context))
avId = self.air.getAvatarIdFromSender()
try:
invite = FriendManagerAI.invites[context]
except:
# The client might legitimately try to cancel a context
# that has already been cancelled.
#self.air.writeServerEvent('suspicious', avId, 'FriendManagerAI.inviteeAcknowledgeCancel unknown context')
#FriendManagerAI.notify.warning('Message for unknown context ' + `context`)
return
self.clearInvite(invite)
# Messages sent from AI to inviter client
def down_friendConsidering(self, recipient, yesNoAlready, context):
"""friendConsidering(self, DistributedObject recipient,
int yesNoAlready, int context)
Sent by the AI to the inviter client to indicate whether the
invitee is able to consider the request right now.
The responses are:
# 0 - the invitee is busy
# 2 - the invitee is already your friend
# 3 - the invitee is yourself
# 4 - the invitee is ignoring you.
# 6 - the invitee not accepting friends
"""
self.sendUpdateToAvatarId(recipient, "friendConsidering", [yesNoAlready, context])
self.notify.debug("AI: friendConsidering(%d, %d)" % (yesNoAlready, context))
def down_friendResponse(self, recipient, yesNoMaybe, context):
"""friendResponse(self, DistributedOBject recipient,
int yesNoMaybe, int context)
Sent by the AI to the inviter client, following an affirmitive
response in friendConsidering, to indicate whether or not the
user decided to accept the friendship.
"""
self.sendUpdateToAvatarId(recipient, "friendResponse", [yesNoMaybe, context])
self.notify.debug("AI: friendResponse(%d, %d)" % (yesNoMaybe, context))
# Messages sent from AI to invitee client
def down_inviteeFriendQuery(self, recipient, inviterId, inviterName,
inviterDna, context):
"""inviteeFriendQuery(self, DistributedObject recipient,
int inviterId, string inviterName,
AvatarDNA inviterDna, int context)
Sent by the AI to the invitee client to initiate a friendship
request from the indiciated inviter. The invitee client
should respond immediately with inviteeFriendConsidering, to
indicate whether the invitee is able to consider the
invitation right now.
"""
self.sendUpdateToAvatarId(recipient, "inviteeFriendQuery",
[inviterId, inviterName,
inviterDna.makeNetString(), context])
self.notify.debug("AI: inviteeFriendQuery(%d, %s, dna, %d)" % (inviterId, inviterName, context))
def down_inviteeCancelFriendQuery(self, recipient, context):
"""inviteeCancelFriendQuery(self, DistributedObject recipient,
int context)
Sent by the AI to the invitee client to initiate that the
inviter has rescinded his/her previous invitation by clicking
the cancel button.
"""
self.sendUpdateToAvatarId(recipient, "inviteeCancelFriendQuery", [context])
self.notify.debug("AI: inviteeCancelFriendQuery(%d)" % (context))
def getFilename(self):
if not hasattr(self, 'district'):
self.district = str(self.air.districtId)
return '{0}{1}{2}{3}.json'.format(self.friendDataFolder, 'trueFriendCodes/', 'trueFriendCodes_', self.district)
def loadTrueFriendCodes(self):
try:
trueFriendCodesFile = open(self.filename, 'r')
trueFriendsCodesData = json.load(trueFriendCodesFile)
return trueFriendsCodesData
except:
return {}
# Messages involving secrets
def deleteSecret(self, secret):
#delete secrets in true friend codes
if secret in self.trueFriendCodes:
del self.trueFriendCodes[secret]
self.updateTrueFriendCodesFile()
def updateTrueFriendCodesFile(self):
#update the file for true friend codes
if not os.path.exists(os.path.dirname(self.filename)):
os.makedirs(os.path.dirname(self.filename))
trueFriendCodesFile = open(self.filename, 'w')
trueFriendCodesFile.seek(0)
json.dump(self.trueFriendCodes, trueFriendCodesFile)
trueFriendCodesFile.close()
def trueFriendCodesTask(self, task):
for trueFriendCode in list(self.trueFriendCodes.keys()):
trueFriendCodeInfo = self.trueFriendCodes[trueFriendCode] #get info about the code
trueFriendCodeDay = trueFriendCodeInfo[1] #get the day of the code
today = datetime.datetime.now().day
if trueFriendCodeDay + 2 == today:
#true friend code is 2 days old remove it
self.deleteSecret(trueFriendCode)
return task.again
def requestSecret(self):
"""requestSecret(self)
Sent by the client to the AI to request a new "secret" for the
user.
"""
avId = self.air.getAvatarIdFromSender()
av = self.air.doId2do.get(avId)
if not av:
self.notify.warning('no av from avid: {0}'.format(avId))
return
#if friendslist is less than the friends limit
if len(av.getFriendsList()) < OTPGlobals.MaxFriends:
#get current day
day = datetime.datetime.now().day
#cretae a true friend code
trueFriendCode = self.createTrueFriendCode()
#set the information up
self.trueFriendCodes[trueFriendCode] = (avId, day)
#update the file to include the true friend code
self.updateTrueFriendCodesFile()
self.down_requestSecretResponse(avId, 1, trueFriendCode)
self.air.writeServerEvent('true-friend-code-requested', avId=avId, trueFriendCode=trueFriendCode)
else:
self.down_requestSecretResponse(avId, 2, "")
#self.air.requestSecret(avId)
def down_requestSecretResponse(self, recipient, result, secret):
"""requestSecret(self, int8 result, string secret)
Sent by the AI to the client in response to requestSecret().
result is one of:
0 - Too many secrets outstanding. Try again later.
1 - Success. The new secret is supplied.
2 - Too many friends
"""
self.sendUpdateToAvatarId(recipient, 'requestSecretResponse', [result, secret])
def createTrueFriendCode(self):
chars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
def randomChar():
return random.choice(chars)
trueFriendCode = '{0}{1}{2} {3}{4}{5}'.format(randomChar(), randomChar(), randomChar(), randomChar(), randomChar(), randomChar())
return trueFriendCode
def submitSecret(self, secret):
"""submitSecret(self, string secret)
Sent by the client to the AI to submit a "secret" typed in by
the user.
"""
avId = self.air.getAvatarIdFromSender()
av = self.air.doId2do.get(avId)
if not av:
self.notify.warning('submitSecret: no av ')
return
secretInfo = self.trueFriendCodes.get(secret)
if not secretInfo:
self.down_submitSecretResponse(avId, 0, 0)
return
friendId = secretInfo[0]
friend = self.air.doId2do.get(friendId)
if av:
if friend:
if avId == friendId:
self.down_submitSecretResponse(avId, 3, 0)
self.deleteSecret(secret)
elif len(friend.getFriendsList()) < OTPGlobals.MaxFriends or len(
av.getFriendsList()) < OTPGlobals.MaxFriends:
dg = PyDatagram()
dg.addServerHeader(self.GetPuppetConnectionChannel(friendId), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
dg.addUint32(avId)
dg.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(dg)
dg = PyDatagram()
dg.addServerHeader(self.GetPuppetConnectionChannel(avId), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
dg.addUint32(friendId)
dg.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(dg)
friend.extendFriendsList(avId, 1)
av.extendFriendsList(friendId, 1)
friend.d_setFriendsList(friend.getFriendsList())
av.d_setFriendsList(av.getFriendsList())
av.addTrueFriend(friendId)
self.down_submitSecretResponse(avId, 1, friendId)
self.deleteSecret(secret)
else:
#friends list is full
self.down_submitSecretResponse(avId, 2, friendId)
else:
#offline friend
def handleAvatar(dclass, fields):
if dclass != self.air.dclassesByName['DistributedToonAI']:
return
newFriendsList = []
oldFriendsList = fields['setFriendsList'][0]
if len(oldFriendsList) >= OTPGlobals.MaxFriends:
self.d_submitSecretResponse(avId, 2, friendId)
return
for i in oldFriendsList:
newFriendsList.append(i)
newFriendsList.append((avId, 1))
self.air.dbInterface.updateObject(self.air.dbId, friendId,
self.air.dclassesByName['DistributedToonAI'],
{'setFriendsList': [newFriendsList]})
av.extendFriendsList(friendId, 1)
av.d_setFriendsList(av.getFriendsList())
self.d_submitSecretResponse(avId, 1, friendId)
self.deleteSecret(secret)
self.air.dbInterface.queryObject(self.air.dbId, friendId, handleAvatar)
self.air.writeServerEvent('truefriend-code-submitted', avId=avId, friendId=friendId, trueFriendCode=secret)
# We have to sit on this request for a few seconds before
# processing it. This delay is solely to discourage password
# guessing.
# taskName = "secret-" + str(avId)
# taskMgr.remove(taskName)
#if FriendManagerAI.SecretDelay:
# taskMgr.doMethodLater(FriendManagerAI.SecretDelay,
# self.continueSubmission,
#taskName,
#extraArgs = (avId, secret))
# else:
# No delay
# self.continueSubmission(avId, secret)
def continueSubmission(self, avId, secret):
"""continueSubmission(self, avId, secret)
Finishes the work of submitSecret, a short time later.
"""
self.air.submitSecret(avId, secret)
def down_submitSecretResponse(self, recipient, result, avId):
"""submitSecret(self, int8 result, int32 avId)
Sent by the AI to the client in response to submitSecret().
result is one of:
0 - Failure. The secret is unknown or has timed out.
1 - Success. You are now friends with the indicated avId.
2 - Failure. One of the avatars has too many friends already.
3 - Failure. You just used up your own secret.
"""
self.sendUpdateToAvatarId(recipient, 'submitSecretResponse', [result, avId])
# Support methods
def newInvite(self, inviterId, inviteeId):
context = FriendManagerAI.nextContext
FriendManagerAI.nextContext += 1
invite = FriendManagerAI.Invite(context, inviterId, inviteeId)
FriendManagerAI.invites[context] = invite
# If the invitee has previously (recently) declined a
# friendship from this inviter, don't ask again.
previous = self.__previousResponse(inviteeId, inviterId)
if previous != None:
self.inviteeUnavailable(invite, previous + 10)
return
# If the invitee is presently being invited by someone else,
# we don't even have to bother him.
if inviteeId in FriendManagerAI.invitees:
self.inviteeUnavailable(invite, 0)
return
if invite.inviterId == invite.inviteeId:
# You can't be friends with yourself.
self.inviteeUnavailable(invite, 3)
return
# If the inviter is already involved in some other context,
# that one is now void.
if inviterId in FriendManagerAI.inviters:
self.cancelInvite(FriendManagerAI.inviters[inviterId])
FriendManagerAI.inviters[inviterId] = invite
FriendManagerAI.invitees[inviteeId] = invite
#self.air.queryObject(inviteeId, self.gotInvitee, invite)
#self.air.queryObject(inviterId, self.gotInviter, invite)
invite.inviter = self.air.doId2do.get(inviterId)
invite.invitee = self.air.doId2do.get(inviteeId)
if invite.inviter and invite.invitee:
self.beginInvite(invite)
# def gotInviter(self, handle, invite):
# if not invite.inviter:
# invite.inviter = handle
# if invite.invitee:
# self.beginInvite(invite)
# def gotInvitee(self, handle, invite):
# if not invite.invitee:
# invite.invitee = handle
# if invite.inviter:
# self.beginInvite(invite)
def beginInvite(self, invite):
# Ask the invitee if he is available to consider being
# someone's friend--that is, that he's not busy playing a
# minigame or something.
invite.inviteeKnows = 1
self.down_inviteeFriendQuery(invite.inviteeId, invite.inviterId,
invite.inviter.getName(),
invite.inviter.dna, invite.context)
def inviteeUnavailable(self, invite, code):
# Cannot make the request for one of these reasons:
#
# 0 - the invitee is busy
# 2 - the invitee is already your friend
# 3 - the invitee is yourself
# 4 - the invitee is ignoring you.
# 6 - the invitee not accepting friends
self.down_friendConsidering(invite.inviterId, code, invite.context)
# That ends the invitation.
self.clearInvite(invite)
def inviteeAvailable(self, invite):
# The invitee is considering our friendship request.
self.down_friendConsidering(invite.inviterId, 1, invite.context)
def noFriends(self, invite, yesNoMaybe):
# The invitee declined to make friends.
#
# 0 - no
# 2 - unable to answer; e.g. entered a minigame or something.
# 3 - the invitee has too many friends already.
if yesNoMaybe == 0 or yesNoMaybe == 3:
# The user explictly said no or has too many friends.
# Disallow this guy from asking again for the next ten
# minutes or so.
if invite.inviteeId not in self.declineFriends1:
self.declineFriends1[invite.inviteeId] = {}
self.declineFriends1[invite.inviteeId][invite.inviterId] = yesNoMaybe
self.down_friendResponse(invite.inviterId, yesNoMaybe, invite.context)
self.clearInvite(invite)
def makeFriends(self, invite):
# The invitee agreed to make friends.
self.air.makeFriends(invite.inviteeId, invite.inviterId, 0,
invite.context)
self.down_friendResponse(invite.inviterId, 1, invite.context)
# The reply will clear the context out when it comes in.
def makeSpecialFriends(self, requesterId, avId):
# The "requester" has typed in a codeword that successfully
# matches a friend in the world. Attempt to make the
# friendship (with chat permission), and send back a code
# indicating success or failure.
# Get a special Invite structure just for this purpose.
context = FriendManagerAI.nextContext
FriendManagerAI.nextContext += 1
invite = FriendManagerAI.Invite(context, requesterId, avId)
FriendManagerAI.invites[context] = invite
invite.sendSpecialResponse = 1
self.air.makeFriends(invite.inviteeId, invite.inviterId, 1,
invite.context)
def clearInvite(self, invite):
try:
del FriendManagerAI.invites[invite.context]
except:
pass
try:
del FriendManagerAI.inviters[invite.inviterId]
except:
pass
try:
del FriendManagerAI.invitees[invite.inviteeId]
except:
pass
def cancelInvite(self, invite):
if invite.inviteeKnows:
self.down_inviteeCancelFriendQuery(invite.inviteeId, invite.context)
invite.inviteeKnows = 0
def makeFriendsReply(self, result, context):
try:
invite = FriendManagerAI.invites[context]
except:
FriendManagerAI.notify.warning('Message for unknown context ' + context)
return
if result:
# By now, the server has OK'ed the friends transaction.
# Update our internal bookkeeping so we remember who's
# friends with whom. This is mainly useful for correct
# accounting of the make-a-friend quest.
invitee = self.air.doId2do.get(invite.inviteeId)
inviter = self.air.doId2do.get(invite.inviterId)
if invitee != None:
invitee.extendFriendsList(invite.inviterId, invite.sendSpecialResponse)
self.air.questManager.toonMadeFriend(invitee, inviter)
#inviter = self.air.doId2do.get(invite.inviterId)
if inviter != None:
inviter.extendFriendsList(invite.inviteeId, invite.sendSpecialResponse)
self.air.questManager.toonMadeFriend(inviter, invitee)
if invite.sendSpecialResponse:
# If this flag is set, the "invite" was generated via the
# codeword system, instead of through the normal path. In
# this case, we need to send the acknowledgement back to
# the client.
if result:
# Success! Send a result code of 1.
result = 1
else:
# Failure, some friends list problem. Result code of 2.
result = 2
self.down_submitSecretResponse(invite.inviterId, result,
invite.inviteeId)
# Also send a notification to the other avatar, if he's on.
avatar = DistributedAvatarAI.DistributedAvatarAI(self.air)
avatar.doId = invite.inviteeId
avatar.d_friendsNotify(invite.inviterId, 2)
self.clearInvite(invite)
def __requestSecretReply(self, result, secret, requesterId):
self.notify.debug("request secret result = %d, secret = '%s', requesterId = %d" % (result, secret, requesterId))
self.down_requestSecretResponse(requesterId, result, secret)
def __submitSecretReply(self, result, secret, requesterId, avId):
self.notify.debug("submit secret result = %d, secret = '%s', requesterId = %d, avId = %d" % (result, secret, requesterId, avId))
if result == 1:
# We successfully matched the secret, so we should do a
# few more sanity checks and then try to make friends.
if avId == requesterId:
# This is the requester's own secret!
result = 3
else:
# Ok, make us special friends.
self.makeSpecialFriends(requesterId, avId)
# In this case, the response gets sent after the
# friends transaction completes.
return
self.down_submitSecretResponse(requesterId, result, avId)
def __previousResponse(self, inviteeId, inviterId):
# Return the previous rejection code if this invitee has
# previously (recently) declined a friendship from this
# inviter, or None if there was no previous rejection.
now = globalClock.getRealTime()
if now - self.lastRollTime >= self.DeclineFriendshipTimeout:
self.declineFriends2 = self.declineFriends1
self.declineFriends1 = {}
# Now, is the invitee/inviter combination present in either
# map?
previous = None
if inviteeId in self.declineFriends1:
previous = self.declineFriends1[inviteeId].get(inviterId)
if previous != None:
return previous
if inviteeId in self.declineFriends2:
previous = self.declineFriends2[inviteeId].get(inviterId)
if previous != None:
return previous
# Nope, go ahead and ask.
return None