Esempio n. 1
0
    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})
Esempio n. 2
0
    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
Esempio n. 3
0
    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
Esempio n. 4
0
    def get_all_profiles(self, game: str, version: int) -> List[Tuple[UserID, ValidatedDict]]:
        # Fetch local and remote profiles, and then merge by adding remote profiles to local
        # profiles when we don't have a profile for that user ID yet.
        local_cards, local_profiles, remote_profiles = Parallel.execute([
            self.user.get_all_cards,
            lambda: self.user.get_all_profiles(game, version),
            lambda: Parallel.flatten(Parallel.call(
                [client.get_profiles for client in self.clients],
                game,
                version,
                APIConstants.ID_TYPE_SERVER,
                [],
            )),
        ])

        card_to_id = {cardid: userid for (cardid, userid) in local_cards}
        id_to_profile = {userid: profile for (userid, profile) in local_profiles}

        for profile in remote_profiles:
            cardids = sorted([card.upper() for card in profile.get('cards', [])])
            if len(cardids) == 0:
                # We don't care about anonymous profiles
                continue

            local_cards = [cardid for cardid in cardids if cardid in card_to_id]
            if len(local_cards) > 0:
                # We have a local version of this profile!
                continue

            # Create a fake user with this profile
            del profile['cards']

            exact_match = profile.get('match', 'partial') == 'exact'
            if not exact_match:
                continue

            userid = RemoteUser.card_to_userid(cardids[0])
            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
            profile['refid'] = refid
            profile['extid'] = extid

            id_to_profile[userid] = self.__format_profile(ValidatedDict(profile))

        return [(userid, id_to_profile[userid]) for userid in id_to_profile]
Esempio n. 5
0
    def __profile_request(self, game: str, version: int, userid: UserID, exact: bool) -> Optional[ValidatedDict]:
        # First, get or create the extid/refid for this virtual user
        cardid = RemoteUser.userid_to_card(userid)
        refid = self.user.get_refid(game, version, userid)
        extid = self.user.get_extid(game, version, userid)

        profiles = Parallel.flatten(Parallel.call(
            [client.get_profiles for client in self.clients],
            game,
            version,
            APIConstants.ID_TYPE_CARD,
            [cardid],
        ))
        for profile in profiles:
            cards = [card.upper() for card in profile.get('cards', [])]
            if cardid in cards:
                # Sanitize the returned data
                profile = copy.deepcopy(profile)
                del profile['cards']

                exact_match = profile.get('match', 'partial') == 'exact'
                if exact and (not exact_match):
                    # This is a partial match, not for this game/version
                    continue

                if 'match' in profile:
                    del profile['match']

                # Add in our defaults we always provide
                profile['game'] = game
                profile['version'] = version if exact_match else 0
                profile['refid'] = refid
                profile['extid'] = extid

                return self.__format_profile(ValidatedDict(profile))

        return None
Esempio n. 6
0
    def __merge_global_scores(
        self,
        game: str,
        version: int,
        localcards: List[Tuple[str, UserID]],
        localscores: List[Tuple[UserID, Score]],
        remotescores: List[Dict[str, Any]],
    ) -> List[Tuple[UserID, Score]]:
        card_to_id = {cardid: userid for (cardid, userid) in localcards}
        allscores: Dict[UserID, Dict[int, Dict[int, Score]]] = {}

        def add_score(userid: UserID, score: Score) -> None:
            if userid not in allscores:
                allscores[userid] = {}
            if score.id not in allscores[userid]:
                allscores[userid][score.id] = {}
            allscores[userid][score.id][score.chart] = score

        def get_score(userid: UserID, songid: int,
                      songchart: int) -> Optional[Score]:
            return allscores.get(userid, {}).get(songid, {}).get(songchart)

        # First, seed with local scores
        for (userid, score) in localscores:
            add_score(userid, score)

        # Second, merge in remote scorse
        for remotescore in remotescores:
            # Figure out the userid of this score
            cardids = sorted(
                [card.upper() for card in remotescore.get('cards', [])])
            if len(cardids) == 0:
                continue

            for cardid in cardids:
                if cardid in card_to_id:
                    userid = card_to_id[cardid]
                    break
            else:
                userid = RemoteUser.card_to_userid(cardids[0])

            songid = int(remotescore['song'])
            chart = int(remotescore['chart'])
            newscore = self.__format_score(game, version, songid, chart,
                                           remotescore)
            oldscore = get_score(userid, songid, chart)

            if oldscore is None:
                add_score(userid, newscore)
            else:
                add_score(
                    userid,
                    self.__merge_score(game, version, oldscore, newscore))

        # Finally, flatten and return
        finalscores: List[Tuple[UserID, Score]] = []
        for userid in allscores:
            for songid in allscores[userid]:
                for chart in allscores[userid][songid]:
                    finalscores.append(
                        (userid, allscores[userid][songid][chart]))

        return finalscores
Esempio n. 7
0
    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
Esempio n. 8
0
 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)
Esempio n. 9
0
    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
Esempio n. 10
0
 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)
Esempio n. 11
0
 def from_cardid(self, cardid: str) -> Optional[UserID]:
     userid = self.user.from_cardid(cardid)
     if userid is None:
         userid = RemoteUser.card_to_userid(cardid)
     return userid
Esempio n. 12
0
    def __merge_global_records(
        self,
        game: str,
        version: int,
        localcards: List[Tuple[str, UserID]],
        localscores: List[Tuple[UserID, Score]],
        remotescores: List[Dict[str, Any]],
    ) -> List[Tuple[UserID, Score]]:
        card_to_id = {cardid: userid for (cardid, userid) in localcards}
        allscores: Dict[int, Dict[int, Tuple[UserID, Score]]] = {}

        def add_score(userid: UserID, score: Score) -> None:
            if score.id not in allscores:
                allscores[score.id] = {}
            allscores[score.id][score.chart] = (userid, score)

        def get_score(
                songid: int,
                songchart: int) -> Tuple[Optional[UserID], Optional[Score]]:
            return allscores.get(songid, {}).get(songchart, (None, None))

        # First, seed with local records
        for (userid, score) in localscores:
            add_score(userid, score)

        # Second, merge in remote records
        for remotescore in remotescores:
            # Figure out the userid of this score
            cardids = sorted(
                [card.upper() for card in remotescore.get('cards', [])])
            if len(cardids) == 0:
                continue

            for cardid in cardids:
                if cardid in card_to_id:
                    userid = card_to_id[cardid]
                    break
            else:
                userid = RemoteUser.card_to_userid(cardids[0])

            songid = int(remotescore['song'])
            chart = int(remotescore['chart'])
            newscore = self.__format_score(game, version, songid, chart,
                                           remotescore)
            oldid, oldscore = get_score(songid, chart)

            if oldscore is None:
                add_score(userid, newscore)
            else:
                # if IDs are the same then we should merge them
                if oldid == userid:
                    add_score(
                        userid,
                        self.__merge_score(game, version, oldscore, newscore))
                else:
                    # if the IDs are different we need to check which score actually belongs
                    if newscore.points > oldscore.points:
                        add_score(userid, newscore)

        # Finally, flatten and return
        finalscores: List[Tuple[UserID, Score]] = []
        for songid in allscores:
            for chart in allscores[songid]:
                finalscores.append(
                    (allscores[songid][chart][0], allscores[songid][chart][1]))

        return finalscores
Esempio n. 13
0
 def test_id_mangling(self) -> None:
     card = "E0040100DEADBEEF"
     userid = RemoteUser.card_to_userid(card)
     self.assertTrue(userid > (2**32 - 1))
     newcard = RemoteUser.userid_to_card(userid)
     self.assertEqual(card, newcard)
Esempio n. 14
0
 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)))