diff --git a/api/networks.py b/api/networks.py index 59a85f7..1a3bb04 100644 --- a/api/networks.py +++ b/api/networks.py @@ -1,24 +1,44 @@ from enum import IntEnum from api.public import nintendoBotFC, pretendoBotFC -# Selectable networks -class NetworkIDsToName(IntEnum): - nintendo = 0 - pretendo = 1 - -def getBotFriendCodeFromNetworkId(network:int): - match network: - case 0: - return nintendoBotFC - case 1: - return pretendoBotFC -def nameToNetworkId(network:int): - if network == None: - network = 0 - else: - try: - network = NetworkIDsToName[network].value - except: - network = 0 - return network \ No newline at end of file +class InvalidNetworkError(Exception): + pass + + +class NetworkType(IntEnum): + """Selectable network types.""" + NINTENDO = 0 + PRETENDO = 1 + + def friend_code(self) -> str: + """Returns the configured friend code for this network type.""" + match self: + case self.NINTENDO: + return nintendoBotFC + case self.PRETENDO: + return pretendoBotFC + + def column_name(self) -> str: + """Returns the database column name for this network type.""" + match self: + case self.NINTENDO: + return "nintendo_friends" + case self.PRETENDO: + return "pretendo_friends" + + def lower_name(self) -> str: + """Returns a lowercase name of this enum member's name for API compatibility.""" + return self.name.lower() + + +def nameToNetworkType(network_name: str) -> NetworkType: + # Assume Nintendo Network as a fallback. + if network_name is None: + return NetworkType.NINTENDO + + try: + # All enum members are uppercase, so ensure we are, too. + return NetworkType[network_name.upper()] + except: + return NetworkType.NINTENDO diff --git a/server/backend.py b/server/backend.py index c11d067..76aa74a 100644 --- a/server/backend.py +++ b/server/backend.py @@ -10,7 +10,7 @@ sys.path.append('../') from api.private import SERIAL_NUMBER, MAC_ADDRESS, DEVICE_CERT, DEVICE_NAME, REGION, LANGUAGE, NINTENDO_PID, PRETENDO_PID, PID_HMAC, NINTENDO_NEX_PASSWORD, PRETENDO_NEX_PASSWORD from api import * from api.love2 import * -from api.networks import NetworkIDsToName +from api.networks import NetworkType, InvalidNetworkError import logging logging.basicConfig(level=logging.INFO) @@ -21,7 +21,7 @@ quicker = 15 begun = time.time() scrape_only = False -network:int = 0 +network: NetworkType = NetworkType.NINTENDO async def main(): while True: @@ -29,7 +29,7 @@ async def main(): print('Grabbing new friends...') with sqlite3.connect('sqlite/fcLibrary.db') as con: cursor = con.cursor() - cursor.execute('SELECT friendCode, lastAccessed FROM ' + NetworkIDsToName(network).name + "_friends") + cursor.execute('SELECT friendCode, lastAccessed FROM ' + network.column_name()) result = cursor.fetchall() if not result: continue @@ -45,19 +45,17 @@ async def main(): client.set_title(0x0004013000003202, 20) # to be honest, this should be seperate between networks, so if one eg, gets banned, you still get to keep the friend code for the other network, but i'm lazy. client.set_locale(REGION, LANGUAGE) - if network == 0: # Nintendo Network + if network == NetworkType.NINTENDO: client.set_url("nasc.nintendowifi.net") PID = NINTENDO_PID NEX_PASSWORD = NINTENDO_NEX_PASSWORD - - elif network == 1: + elif network == NetworkType.PRETENDO: client.set_url("nasc.pretendo.cc") client.context.set_authority(None) PID = PRETENDO_PID NEX_PASSWORD = PRETENDO_NEX_PASSWORD - else: - raise Exception(NetworkIDsToName(network).name + " is not a valid network") + raise InvalidNetworkError(f"Network type {network} is not configured for querying") client.set_device(SERIAL_NUMBER, MAC_ADDRESS, DEVICE_CERT, DEVICE_NAME) client.set_user(PID , PID_HMAC) @@ -88,10 +86,13 @@ async def main(): removeList = [] cleanUp = [] - if network == 1: + + # The add_friend_by_principal_ids method is not yet + # implemented on Pretendo, so this is a fix for now. + if network == NetworkType.PRETENDO: for friend_pid in rotation: time.sleep(delay / quicker) - await friends_client.add_friend_by_principal_id(0, friend_pid) # the add_friend_by_principal_ids hasn't been implemented yet on pretendo, so this is a fix for now. + await friends_client.add_friend_by_principal_id(0, friend_pid) else: time.sleep(delay) await friends_client.add_friend_by_principal_ids(0, rotation) @@ -112,7 +113,8 @@ async def main(): cleanUp.append(t1.pid) for remover in removeList: - cursor.execute('DELETE FROM ' + NetworkIDsToName(network).name + "_friends" + ' WHERE friendCode = ?', (str(convertPrincipalIdtoFriendCode(remover)).zfill(12),)) + column_name = network.column_name() + cursor.execute('DELETE FROM ' + column_name + ' WHERE friendCode = ?', (str(convertPrincipalIdtoFriendCode(remover)).zfill(12),)) cursor.execute('DELETE FROM discordFriends WHERE friendCode = ? AND network = ?', (str(convertPrincipalIdtoFriendCode(remover)).zfill(12), str(network))) con.commit() @@ -132,9 +134,9 @@ async def main(): if not gameDescription: gameDescription = '' joinable = bool(game.presence.join_availability_flag) - cursor.execute('UPDATE ' + NetworkIDsToName(network).name + "_friends" + ' SET online = ?, titleID = ?, updID = ?, joinable = ?, gameDescription = ?, lastOnline = ? WHERE friendCode = ?', (True, game.presence.game_key.title_id, game.presence.game_key.title_version, joinable, gameDescription, time.time(), str(convertPrincipalIdtoFriendCode(users[-1])).zfill(12))) + cursor.execute('UPDATE ' + network.column_name() + ' SET online = ?, titleID = ?, updID = ?, joinable = ?, gameDescription = ?, lastOnline = ? WHERE friendCode = ?', (True, game.presence.game_key.title_id, game.presence.game_key.title_version, joinable, gameDescription, time.time(), str(convertPrincipalIdtoFriendCode(users[-1])).zfill(12))) for user in [ h for h in rotation if not h in users ]: - cursor.execute('UPDATE ' + NetworkIDsToName(network).name + "_friends" + ' SET online = ?, titleID = ?, updID = ? WHERE friendCode = ?', (False, 0, 0, str(convertPrincipalIdtoFriendCode(user)).zfill(12))) + cursor.execute('UPDATE ' + network.column_name() + ' SET online = ?, titleID = ?, updID = ? WHERE friendCode = ?', (False, 0, 0, str(convertPrincipalIdtoFriendCode(user)).zfill(12))) con.commit() @@ -176,7 +178,7 @@ async def main(): jeuFavori = j1[0].game_key.title_id else: comment = '' - cursor.execute('UPDATE ' + NetworkIDsToName(network).name + "_friends" + ' SET username = ?, message = ?, mii = ?, jeuFavori = ? WHERE friendCode = ?', (username, comment, face, jeuFavori, str(convertPrincipalIdtoFriendCode(ti.pid)).zfill(12))) + cursor.execute('UPDATE ' + network.column_name() + ' SET username = ?, message = ?, mii = ?, jeuFavori = ? WHERE friendCode = ?', (username, comment, face, jeuFavori, str(convertPrincipalIdtoFriendCode(ti.pid)).zfill(12))) con.commit() for friend in rotation + cleanUp: @@ -194,10 +196,10 @@ async def main(): if __name__ == '__main__': try: parser = argparse.ArgumentParser() - parser.add_argument('-n', '--network', choices=[member.name.lower() for member in NetworkIDsToName], required=True) + parser.add_argument('-n', '--network', choices=[member.lower_name() for member in NetworkType], required=True) args = parser.parse_args() - network = NetworkIDsToName[args.network.lower()].value + network = NetworkType[args.network.upper()] startDBTime(begun, network) anyio.run(main) except (KeyboardInterrupt, Exception) as e: diff --git a/server/discord.py b/server/discord.py index e15d36e..5fa2853 100644 --- a/server/discord.py +++ b/server/discord.py @@ -4,7 +4,7 @@ sys.path.append('../') from api import * from api.love2 import * from api.private import CLIENT_ID, CLIENT_SECRET, HOST -from api.networks import NetworkIDsToName, nameToNetworkId +from api.networks import NetworkType API_ENDPOINT:str = 'https://discord.com/api/v10' @@ -63,10 +63,10 @@ class Discord(): if presence['gameDescription']: data['activities'][0]['details'] = presence['gameDescription'] if userData['User']['username'] and bool(config[0]): - data['activities'][0]['buttons'] = [{'label': 'Profile', 'url': HOST + '/user/' + userData['User']['friendCode'] + '/?network=' + NetworkIDsToName(network).name},] + data['activities'][0]['buttons'] = [{'label': 'Profile', 'url': HOST + '/user/' + userData['User']['friendCode'] + '/?network=' + NetworkType(network).name},] if userData['User']['username'] and game['icon_url'] and bool(config[1]): data['activities'][0]['assets']['small_image'] = userData['User']['mii']['face'] - data['activities'][0]['assets']['small_text'] = '-'.join(userData['User']['friendCode'][i:i+4] for i in range(0, 12, 4)) + ' on ' + NetworkIDsToName(network).name.capitalize() + data['activities'][0]['assets']['small_text'] = '-'.join(userData['User']['friendCode'][i:i+4] for i in range(0, 12, 4)) + ' on ' + NetworkType(network).name.capitalize() if session: data['token'] = session headers = { @@ -193,7 +193,7 @@ while True: continue for r in discordFriends: print('[RUNNING %s - %s]' % (r[0], r[1])) - cursor.execute('SELECT * FROM ' + NetworkIDsToName(r[2]).name + '_friends WHERE friendCode = ?', (r[1],)) + cursor.execute('SELECT * FROM ' + NetworkType(r[2]).column_name() + ' WHERE friendCode = ?', (r[1],)) v2 = cursor.fetchone() cursor.execute('SELECT * FROM discord WHERE ID = ?', (r[0],)) v3 = cursor.fetchone() diff --git a/server/server.py b/server/server.py index 36b2986..20179b5 100644 --- a/server/server.py +++ b/server/server.py @@ -10,7 +10,7 @@ from api import * from api.love2 import * from api.private import CLIENT_ID, CLIENT_SECRET, HOST from api.public import pretendoBotFC, nintendoBotFC -from api.networks import NetworkIDsToName, nameToNetworkId, getBotFriendCodeFromNetworkId +from api.networks import NetworkType, nameToNetworkType app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.abspath('sqlite/fcLibrary.db') @@ -94,7 +94,7 @@ def cacheTitles(): print('[Saved database to file]') # Create entry in database with friendCode -def createUser(friendCode:int, network:int, addNewInstance:bool = False): +def createUser(friendCode:int, network:NetworkType, addNewInstance:bool = False): if int(friendCode) == int(pretendoBotFC): raise Exception('invalid FC') if int(friendCode) == int(nintendoBotFC): @@ -103,11 +103,11 @@ def createUser(friendCode:int, network:int, addNewInstance:bool = False): convertFriendCodeToPrincipalId(friendCode) if not addNewInstance: raise Exception('UNIQUE constraint failed: friends.friendCode') - db.session.execute('INSERT INTO ' + NetworkIDsToName(network).name + '_friends (friendCode, online, titleID, updID, lastAccessed, accountCreation, lastOnline, jeuFavori) VALUES (\'%s\', %s, %s, %s, %s, %s, %s, %s)' % (str(friendCode).zfill(12), False, '0', '0', time.time() + 300, time.time(), time.time(), 0)) + db.session.execute('INSERT INTO ' + network.column_name() + ' (friendCode, online, titleID, updID, lastAccessed, accountCreation, lastOnline, jeuFavori) VALUES (\'%s\', %s, %s, %s, %s, %s, %s, %s)' % (str(friendCode).zfill(12), False, '0', '0', time.time() + 300, time.time(), time.time(), 0)) db.session.commit() except Exception as e: if 'UNIQUE constraint failed: friends.friendCode' in str(e): - db.session.execute('UPDATE ' + NetworkIDsToName(network).name + '_friends SET lastAccessed = %s WHERE friendCode = \'%s\'' % (time.time(), str(friendCode).zfill(12))) + db.session.execute('UPDATE ' + network.column_name() + ' SET lastAccessed = %s WHERE friendCode = \'%s\'' % (time.time(), str(friendCode).zfill(12))) db.session.commit() def fetchBearerToken(code:str): @@ -212,7 +212,7 @@ def userAgentCheck(): except: raise Exception('this client is invalid') -def getPresence(friendCode:int, network:int, *, createAccount:bool = True, ignoreUserAgent = False, ignoreBackend = False): +def getPresence(friendCode:int, network:NetworkType, *, createAccount:bool = True, ignoreUserAgent = False, ignoreBackend = False): try: if not ignoreUserAgent: userAgentCheck() @@ -225,7 +225,7 @@ def getPresence(friendCode:int, network:int, *, createAccount:bool = True, ignor if createAccount: createUser(friendCode, network, False) principalId = convertFriendCodeToPrincipalId(friendCode) - result = db.session.execute('SELECT * FROM ' + NetworkIDsToName(network).name + '_friends WHERE friendCode = \'%s\'' % friendCode) + result = db.session.execute('SELECT * FROM ' + network.column_name() + ' WHERE friendCode = \'%s\'' % friendCode) result = result.fetchone() if not result: raise Exception('friendCode not recognized\nHint: You may not have added the bot as a friend') @@ -273,7 +273,7 @@ def getPresence(friendCode:int, network:int, *, createAccount:bool = True, ignor # Index page @app.route('/') def index(): - results = db.session.execute(' UNION '.join([f'SELECT *, "{network.name}" FROM {network.name}_friends WHERE online = True AND username != ""' for network in NetworkIDsToName]) + ' ORDER BY lastAccessed DESC') + results = db.session.execute(' UNION '.join([f'SELECT *, "{network.lower_name()}" FROM {network.column_name()} WHERE online = True AND username != ""' for network in NetworkType]) + ' ORDER BY lastAccessed DESC') results = results.fetchall() num = len(results) data = sidenav() @@ -286,11 +286,12 @@ def index(): 'game': getTitle(user[2], titlesToUID, titleDatabase), 'friendCode': str(user[0]).zfill(12), 'joinable': bool(user[9]), + # Adjust for uppercase enum naming. 'network': str(user[13]), }) for user in results if user[6] ] data['active'] = data['active'][:2] - results = db.session.execute(' UNION '.join([f'SELECT *, "{network.name}" FROM {network.name}_friends WHERE username != ""' for network in NetworkIDsToName]) + ' ORDER BY accountCreation DESC LIMIT 6') + results = db.session.execute(' UNION '.join([f'SELECT *, "{network.lower_name()}" FROM {network.column_name()} WHERE username != ""' for network in NetworkType]) + ' ORDER BY accountCreation DESC LIMIT 6') results = results.fetchall() data['new'] = [ ({ 'mii':MiiData().mii_studio_url(user[8]), @@ -351,7 +352,7 @@ def settingsRedirect(): # Roster page @app.route('/roster') def roster(): - results = db.session.execute(' UNION '.join([f'SELECT *, "{network.name}" AS network FROM {network.name}_friends WHERE username != ""' for network in NetworkIDsToName]) + ' ORDER BY accountCreation DESC LIMIT 8') + results = db.session.execute(' UNION '.join([f'SELECT *, "{network.lower_name()}" AS network FROM {network.column_name()} WHERE username != ""' for network in NetworkType]) + ' ORDER BY accountCreation DESC LIMIT 8') results = results.fetchall() data = sidenav() data['title'] = 'New Users' @@ -370,7 +371,7 @@ def roster(): # Active page @app.route('/active') def active(): - results = db.session.execute(' UNION '.join([f'SELECT *, "{network.name}" AS network FROM {network.name}_friends WHERE online = True AND username != ""' for network in NetworkIDsToName]) + ' ORDER BY lastAccessed DESC') + results = db.session.execute(' UNION '.join([f'SELECT *, "{network.lower_name()}" AS network FROM {network.column_name()} WHERE online = True AND username != ""' for network in NetworkType]) + ' ORDER BY lastAccessed DESC') results = results.fetchall() data = sidenav() data['title'] = 'Active Users' @@ -392,16 +393,17 @@ def active(): def register(): network = request.args.get('network') - if network == None: - response = make_response(render_template('dist/registerselectnetwork.html')) - else: - try: - network = NetworkIDsToName[network].value - response = make_response(render_template('dist/register.html', data = {'botFC':'-'.join(getBotFriendCodeFromNetworkId(network)[i:i+4] for i in range(0, len(getBotFriendCodeFromNetworkId(network)), 4)), 'network':network})) - except: - network = 0 - response = make_response(render_template('dist/registerselectnetwork.html')) - return response + if network is None: + return make_response(render_template('dist/registerselectnetwork.html')) + + try: + network = NetworkType[network.upper()] + response = make_response(render_template('dist/register.html', data = { + 'botFC': '-'.join(network.friend_code()[i:i+4] for i in range(0, len(network.friend_code()), 4)), + 'network': network + })) + except: + return make_response(render_template('dist/registerselectnetwork.html')) # Register page redirect @app.route('/register') @@ -450,14 +452,15 @@ def consoles(): response.set_cookie('pfp', '', expires = 0) return response return redirect('/') - for console, active, network in getConnectedConsoles(id): - result = db.session.execute('SELECT * FROM ' + NetworkIDsToName(network).name + '_friends WHERE friendCode = \'%s\'' % console) + for console, active, network_type in getConnectedConsoles(id): + network = NetworkType(network_type) + result = db.session.execute('SELECT * FROM ' + network.column_name() + ' WHERE friendCode = \'%s\'' % console) result = result.fetchone() data['consoles'].append({ 'fc': '-'.join(console[i:i+4] for i in range(0, 12, 4)), 'username': result[6], 'active': active, - 'network': NetworkIDsToName(network).name + 'network': network.lower_name() }) data.update(sidenav()) response = render_template('dist/consoles.html', data = data) @@ -465,18 +468,21 @@ def consoles(): @app.route('/user//') def userPage(friendCode:str): + network: NetworkType + try: - network = nameToNetworkId(request.args.get('network')) + network = nameToNetworkType(request.args.get('network')) userData = getPresence(int(friendCode.replace('-', '')), network, createAccount= False, ignoreUserAgent = True, ignoreBackend = True) if userData['Exception'] or not userData['User']['username']: raise Exception(userData['Exception']) except: return render_template('dist/404.html') + if not userData['User']['online'] or not userData['User']['Presence']: userData['User']['Presence']['game'] = None userData['User']['favoriteGame'] = getTitle(userData['User']['favoriteGame'], titlesToUID, titleDatabase) - userData['User']['network'] = NetworkIDsToName(nameToNetworkId(request.args.get('network'))).name + userData['User']['network'] = network.lower_name() if userData['User']['favoriteGame']['name'] == 'Home Screen': userData['User']['favoriteGame'] = None for i in ('accountCreation','lastAccessed','lastOnline'): @@ -510,9 +516,11 @@ def newUser(friendCode:int, network:int=-1, userCheck:bool = True): if userCheck: userAgentCheck() if network == -1: - network = 0 - if request.data.decode('utf-8').split(',')[0].isnumeric(): - network = NetworkIDsToName(request.data.decode('utf-8').split(',')[0]).value + network = NetworkType.NINTENDO + + request_arg = request.data.decode('utf-8').split(',')[0] + if request_arg.isnumeric(): + network = NetworkType(request_arg.upper()) createUser(friendCode, network, True) return { 'Exception': False, @@ -528,7 +536,7 @@ def newUser(friendCode:int, network:int=-1, userCheck:bool = True): @app.route('/api/user//', methods=['GET']) @limiter.limit(userPresenceLimit) def userPresence(friendCode:int, network:str="nintendo", *, createAccount:bool = True, ignoreUserAgent = False, ignoreBackend = False): - return getPresence(friendCode, nameToNetworkId(network), createAccount=createAccount, ignoreUserAgent = ignoreUserAgent, ignoreBackend = ignoreBackend) + return getPresence(friendCode, nameToNetworkType(network), createAccount=createAccount, ignoreUserAgent = ignoreUserAgent, ignoreBackend = ignoreBackend) # Alias @app.route('/api/u//', methods=['GET']) @@ -536,48 +544,48 @@ def userPresence(friendCode:int, network:str="nintendo", *, createAccount:bool = def userAlias(friendCode:int): network = 0 if request.args.get('network') != None: - network = nameToNetworkId(request.args.get('network')) + network = nameToNetworkType(request.args.get('network')) return userPresence(friendCode, network) # Alias @app.route('/api/u/c//', methods=['POST']) @limiter.limit(newUserLimit) def newAlias1(friendCode:int): - network = 0 + network = NetworkType.NINTENDO if (request.data.decode('utf-8').split(','))[0] != None: - network = nameToNetworkId((request.data.decode('utf-8').split(','))[0]) + network = nameToNetworkType((request.data.decode('utf-8').split(','))[0]) return newUser(friendCode, network) # Alias @app.route('/api/user/c//', methods=['POST']) @limiter.limit(newUserLimit) def newAlias2(friendCode:int): - network = 0 + network = NetworkType.NINTENDO if (request.data.decode('utf-8').split(','))[0] != None: - network = nameToNetworkId((request.data.decode('utf-8').split(','))[0]) + network = nameToNetworkType((request.data.decode('utf-8').split(','))[0]) return newUser(friendCode, network) # Alias @app.route('/api/u/create//', methods=['POST']) @limiter.limit(newUserLimit) def newAlias3(friendCode:int): - network = 0 + network = NetworkType.NINTENDO if (request.data.decode('utf-8').split(','))[0] != None: - network = nameToNetworkId((request.data.decode('utf-8').split(','))[0]) + network = nameToNetworkType((request.data.decode('utf-8').split(','))[0]) return newUser(friendCode, network) # Toggle @app.route('/api/toggle//', methods=['POST']) @limiter.limit(togglerLimit) def toggler(friendCode:int): - network = 0 + network = NetworkType.NINTENDO if request.data.decode('utf-8').split(',')[2] != None: - network = nameToNetworkId(request.data.decode('utf-8').split(',')[2]) + network = nameToNetworkType(request.data.decode('utf-8').split(',')[2]) try: fc = str(convertPrincipalIdtoFriendCode(convertFriendCodeToPrincipalId(friendCode))).zfill(12) except: return 'failure!\nthat is not a real friendCode!' - result = db.session.execute('SELECT * FROM ' + NetworkIDsToName(network).name + '_friends WHERE friendCode = \'%s\'' % fc) + result = db.session.execute('SELECT * FROM ' + NetworkType(network).column_name() + ' WHERE friendCode = \'%s\'' % fc) result = result.fetchone() if not result: return 'failure!\nthat is not an existing friendCode!' @@ -618,7 +626,7 @@ def deleter(friendCode:int): data = request.data.decode('utf-8').split(',') token = data[0] - network = nameToNetworkId(data[1]) + network = nameToNetworkType(data[1]) id = userFromToken(token)[0] db.session.execute('DELETE FROM discordFriends WHERE friendCode = \'%s\' AND ID = %s AND network = %s' % (fc, id, network)) db.session.commit() @@ -626,7 +634,7 @@ def deleter(friendCode:int): result = db.session.execute('SELECT * FROM discordFriends WHERE friendCode = \'%s\' AND network = %s' % (fc, network)) result = result.fetchone() if result == None: - db.session.execute('DELETE FROM ' + NetworkIDsToName(network).name + '_friends WHERE friendCode = \'%s\'' % (fc)) + db.session.execute('DELETE FROM ' + network.column_name() + ' WHERE friendCode = \'%s\'' % (fc)) db.session.commit() # end of optional @@ -671,15 +679,14 @@ def localImageCdn(file:str): def login(): try: fc = str(convertPrincipalIdtoFriendCode(convertFriendCodeToPrincipalId(request.form['fc']))).zfill(12) - if request.form['network'] == None: - networkName = NetworkIDsToName(0).name + if request.form['network'] is None: + network = NetworkType.NINTENDO else: - networkName = NetworkIDsToName(int(request.form['network'])).name - networkId = nameToNetworkId(networkName) - newUser(fc, networkId, False) + network = NetworkType(int(request.form['network'])) + newUser(fc, network, False) except: return redirect('/failure.html') - return redirect(f'/success.html?fc={fc}&network={networkName}') + return redirect(f'/success.html?fc={fc}&network={network}') # Discord route @app.route('/authorize')