def add_card(self, userid: UserID, cardid: str) -> None: """ Given a user ID and a card ID, link that card with that user. Note that this is the E004 number as stored on the card. Not the 16 digit ASCII value on the back. Use CardCipher to convert. Parameters: userid - Integer user ID, as looked up by one of the above functions. cardid - 16-digit card ID to add. """ sql = "INSERT INTO card (userid, id) VALUES (:userid, :cardid)" self.execute(sql, {'userid': userid, 'cardid': cardid}) oldid = RemoteUser.card_to_userid(cardid) if RemoteUser.is_remote(oldid): # Kill any refid/extid that related to this card, since its now associated # with another existing account. sql = "DELETE FROM extid WHERE userid = :oldid" self.execute(sql, {'oldid': oldid}) sql = "DELETE FROM refid WHERE userid = :oldid" self.execute(sql, {'oldid': oldid}) # Point at the new account for any rivals against this card. Note that this # might result in a duplicate rival, but its a very small edge case. sql = "UPDATE link SET other_userid = :newid WHERE other_userid = :oldid" self.execute(sql, {'newid': userid, 'oldid': oldid})
def get_score(self, game: str, version: int, userid: UserID, songid: int, songchart: int) -> Optional[Score]: # Helper function so we can iterate over all servers for a single card def get_scores_for_card(cardid: str) -> List[Score]: return Parallel.flatten( Parallel.call( [client.get_records for client in self.clients], game, version, APIConstants.ID_TYPE_INSTANCE, [songid, songchart, cardid], )) relevant_cards = self.__get_cardids(userid) if RemoteUser.is_remote(userid): # No need to look up local score for this user scores = Parallel.flatten( Parallel.map( get_scores_for_card, relevant_cards, )) localscore = None else: localscore, scores = Parallel.execute([ lambda: self.music.get_score(game, version, userid, songid, songchart), lambda: Parallel.flatten( Parallel.map( get_scores_for_card, relevant_cards, )), ]) topscore = localscore for score in scores: if int(score['song']) != songid: continue if int(score['chart']) != songchart: continue newscore = self.__format_score(game, version, songid, songchart, score) if topscore is None: # No merging needed topscore = newscore continue topscore = self.__merge_score(game, version, topscore, newscore) return topscore
def create_account(self, cardid: str, pin: str) -> Optional[UserID]: """ Given a Card ID and a PIN, create a new account. Parameters: cardid - 16-digit card ID of the card we are creating an account for. pin - Four digit PIN as entered by the user on a cabinet. Returns: A User ID if creation was successful, or None otherwise. """ # First, create a user account sql = "INSERT INTO user (pin, admin) VALUES (:pin, 0)" cursor = self.execute(sql, {'pin': pin}) if cursor.rowcount != 1: return None userid = cursor.lastrowid # Now, insert the card, tying it to the account sql = "INSERT INTO card (id, userid) VALUES (:cardid, :userid)" cursor = self.execute(sql, {'cardid': cardid, 'userid': userid}) if cursor.rowcount != 1: return None # Now, if this user played on a remote network and their profile # was ever fetched locally or they were ever rivaled against, # convert those locally too so that players don't lose rivals # on new account creation. oldid = RemoteUser.card_to_userid(cardid) if RemoteUser.is_remote(oldid): sql = "UPDATE extid SET userid = :newid WHERE userid = :oldid" self.execute(sql, {'newid': userid, 'oldid': oldid}) sql = "UPDATE refid SET userid = :newid WHERE userid = :oldid" self.execute(sql, {'newid': userid, 'oldid': oldid}) sql = "UPDATE link SET other_userid = :newid WHERE other_userid = :oldid" self.execute(sql, {'newid': userid, 'oldid': oldid}) # Finally, return the user ID return userid
def get_scores( self, game: str, version: int, userid: UserID, since: Optional[int] = None, until: Optional[int] = None, ) -> List[Score]: relevant_cards = self.__get_cardids(userid) if RemoteUser.is_remote(userid): # No need to look up local score for this user scores = Parallel.flatten( Parallel.call( [client.get_records for client in self.clients], game, version, APIConstants.ID_TYPE_CARD, relevant_cards, since, until, )) localscores: List[Score] = [] else: localscores, scores = Parallel.execute([ lambda: self.music.get_scores(game, version, userid, since, until), lambda: Parallel.flatten( Parallel.call( [client.get_records for client in self.clients], game, version, APIConstants.ID_TYPE_CARD, relevant_cards, since, until, )), ]) allscores: Dict[int, Dict[int, Score]] = {} def add_score(score: Score) -> None: if score.id not in allscores: allscores[score.id] = {} allscores[score.id][score.chart] = score def get_score(songid: int, songchart: int) -> Optional[Score]: return allscores.get(songid, {}).get(songchart) # First, seed with local scores for score in localscores: add_score(score) # Second, merge in remote scorse for remotescore in scores: songid = int(remotescore['song']) chart = int(remotescore['chart']) newscore = self.__format_score(game, version, songid, chart, remotescore) oldscore = get_score(songid, chart) if oldscore is None: add_score(newscore) else: add_score(self.__merge_score(game, version, oldscore, newscore)) # Finally, flatten and return finalscores: List[Score] = [] for songid in allscores: for chart in allscores[songid]: finalscores.append(allscores[songid][chart]) return finalscores
def __get_cardids(self, userid: UserID) -> List[str]: if RemoteUser.is_remote(userid): return [RemoteUser.userid_to_card(userid)] else: return self.user.get_cards(userid)
def get_any_profiles(self, game: str, version: int, userids: List[UserID]) -> List[Tuple[UserID, Optional[ValidatedDict]]]: if len(userids) == 0: return [] remote_ids = [ userid for userid in userids if RemoteUser.is_remote(userid) ] local_ids = [ userid for userid in userids if not RemoteUser.is_remote(userid) ] if len(remote_ids) == 0: # We only have local profiles here, just pass on to the underlying layer return self.user.get_any_profiles(game, version, local_ids) else: # We have to fetch some local profiles and some remote profiles, and then # merge them together card_to_userid = { RemoteUser.userid_to_card(userid): userid for userid in remote_ids } local_profiles, remote_profiles = Parallel.execute([ lambda: self.user.get_any_profiles(game, version, local_ids), lambda: Parallel.flatten(Parallel.call( [client.get_profiles for client in self.clients], game, version, APIConstants.ID_TYPE_CARD, [RemoteUser.userid_to_card(userid) for userid in remote_ids], )) ]) for profile in remote_profiles: cards = [card.upper() for card in profile.get('cards', [])] for card in cards: # Map it back to the requested user userid = card_to_userid.get(card) if userid is None: continue # Sanitize the returned data profile = copy.deepcopy(profile) del profile['cards'] exact_match = profile.get('match', 'partial') == 'exact' if 'match' in profile: del profile['match'] refid = self.user.get_refid(game, version, userid) extid = self.user.get_extid(game, version, userid) # Add in our defaults we always provide profile['game'] = game profile['version'] = version if exact_match else 0 profile['refid'] = refid profile['extid'] = extid local_profiles.append( (userid, self.__format_profile(ValidatedDict(profile))), ) # Mark that we saw this card/user del card_to_userid[card] # Finally, mark all missing remote profiles as None for card in card_to_userid: local_profiles.append((card_to_userid[card], None)) return local_profiles
def get_any_profile(self, game: str, version: int, userid: UserID) -> Optional[ValidatedDict]: if RemoteUser.is_remote(userid): return self.__profile_request(game, version, userid, exact=False) else: return self.user.get_any_profile(game, version, userid)
def test_is_remote(self) -> None: self.assertTrue(RemoteUser.is_remote(UserID(2**64 - 1))) self.assertTrue(RemoteUser.is_remote(UserID(2**32))) self.assertFalse(RemoteUser.is_remote(UserID(2**32 - 1))) self.assertFalse(RemoteUser.is_remote(UserID(0))) self.assertFalse(RemoteUser.is_remote(UserID(1)))