diff --git a/api/networks.py b/api/networks.py index 1a3bb04..90a633f 100644 --- a/api/networks.py +++ b/api/networks.py @@ -1,5 +1,5 @@ from enum import IntEnum -from api.public import nintendoBotFC, pretendoBotFC +from api.public import NINTENDO_BOT_FC, PRETENDO_BOT_FC class InvalidNetworkError(Exception): @@ -15,9 +15,12 @@ def friend_code(self) -> str: """Returns the configured friend code for this network type.""" match self: case self.NINTENDO: - return nintendoBotFC + return NINTENDO_BOT_FC case self.PRETENDO: - return pretendoBotFC + return PRETENDO_BOT_FC + + # Default to Nintendo. + return NINTENDO_BOT_FC def column_name(self) -> str: """Returns the database column name for this network type.""" @@ -27,12 +30,15 @@ def column_name(self) -> str: case self.PRETENDO: return "pretendo_friends" + # Default to Nintendo. + return "nintendo_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: +def name_to_network_type(network_name: str) -> NetworkType: # Assume Nintendo Network as a fallback. if network_name is None: return NetworkType.NINTENDO diff --git a/api/public.py b/api/public.py index 18d1a66..d4da413 100644 --- a/api/public.py +++ b/api/public.py @@ -1,4 +1,4 @@ # Friend codes for 3dsrpc.com # TODO: This will become dynamic once multi-console support is available. -pretendoBotFC = str(242170380419).zfill(12) -nintendoBotFC = str(233790548638).zfill(12) +PRETENDO_BOT_FC = str(242170380419).zfill(12) +NINTENDO_BOT_FC = str(233790548638).zfill(12) diff --git a/backend.py b/backend.py index cd0e695..e60c814 100644 --- a/backend.py +++ b/backend.py @@ -118,12 +118,29 @@ async def main_friends_loop(friends_client: friends.FriendsClientV1, session: Se await friends_client.update_comment('3dsrpc.com') # Synchronize our current roster of friends. + # By bulk syncing friends, we can remove all existing friends, + # and then add our new friends with only one call. # - # We expect the remote NEX implementation to remove all existing - # relationships, and replace them with the 100 PIDs specified. - # As of writing, both Nintendo and Pretendo support this. + # Although both Nintendo and Pretendo currently support + # the bulk `sync_friends` RPC call, Pretendo's + # implementation is not optimized, and overloads their servers. all_friend_pids: list[int] = [ f.pid for f in current_rotation ] - await friends_client.sync_friend(0, all_friend_pids, []) + if network == NetworkType.PRETENDO: + # Clear our current, registered friends. + removables = await friends_client.get_all_friends() + for friend in removables: + time.sleep(delay) + await friends_client.remove_friend_by_principal_id(friend.pid) + + # Individually add all pending friend PIDs. + for friend_pid in all_friend_pids: + time.sleep(delay) + await friends_client.add_friend_by_principal_id(0, friend_pid) + else: + # We expect the remote NEX implementation to remove all existing + # relationships, and replace them with the 100 PIDs specified. + # This path is currently only for Nintendo. + await friends_client.sync_friend(0, all_friend_pids, []) time.sleep(delay) diff --git a/server.py b/server.py index 09b2840..d7a9457 100644 --- a/server.py +++ b/server.py @@ -10,8 +10,8 @@ from api.love2 import * from api.private import CLIENT_ID, CLIENT_SECRET, HOST -from api.public import pretendoBotFC, nintendoBotFC -from api.networks import NetworkType, nameToNetworkType +from api.public import PRETENDO_BOT_FC, NINTENDO_BOT_FC +from api.networks import NetworkType, name_to_network_type from database import * app = Flask(__name__) @@ -86,40 +86,87 @@ def cache_titles(): t = pickle.loads(file.read()) title_database = t[0] titles_to_uid = t[1] - else: - title_database = [] - titles_to_uid = [] - - # Create progress bar - bar = ProgressBar() + return - for region in ['US', 'JP', 'GB', 'KR', 'TW']: - title_database.append( - xmltodict.parse(requests.get('https://samurai.ctr.shop.nintendo.net/samurai/ws/%s/titles?shop_id=1&limit=5000&offset=0' % region, verify = False).text) - ) + title_database = [] + titles_to_uid = [] - # Update progress bar as database requests complete - bar.update(.5 / 5) - titles_to_uid += requests.get('https://raw.githubusercontent.com/hax0kartik/3dsdb/master/jsons/list_%s.json' % region).json() - bar.update(.5 / 5) + # Create progress bar + bar = ProgressBar() - bar.end() # End the progress bar + for region in ['US', 'JP', 'GB', 'KR', 'TW']: + title_database.append( + xmltodict.parse(requests.get('https://samurai.ctr.shop.nintendo.net/samurai/ws/%s/titles?shop_id=1&limit=5000&offset=0' % region, verify = False).text) + ) - # Save databases to file - with open(database_path, 'wb') as file: - file.write(pickle.dumps( - (title_database, - titles_to_uid) - )) - print('[Saved database to file]') + # Update progress bar as database requests complete + bar.update(.5 / 5) + titles_to_uid += requests.get('https://raw.githubusercontent.com/hax0kartik/3dsdb/master/jsons/list_%s.json' % region).json() + bar.update(.5 / 5) + + bar.end() # End the progress bar + + # We now have an array of dictionaries in the following structure: + # { + # "eshop": { + # "contents": { + # "content": [ ... ], + # } + # } + # } + # + # TODO(spotlightishere): Normalize this. + for page_index, eshop_page in enumerate(title_database): + all_page_titles = eshop_page["eshop"]["contents"]["content"] + for title_index, title in enumerate(all_page_titles): + shop_title = title["title"] + title_name = shop_title["name"] + + if "
" not in title_name: + continue + + # Occasionally, some titles will contain a newline + # alongside the HTML element "
". + # + # First, remove our newline. + fixed_title_name = title_name.replace("\n", "") + # Replace a leading space, newline, and
with a single space. + fixed_title_name = fixed_title_name.replace("
", " ") + # A
with a following newline must similarly be a space. + fixed_title_name = fixed_title_name.replace("
", " ") + # # If we now have double spaces, normalize this. + fixed_title_name = fixed_title_name.replace(" ", " ") + + title_database[page_index]["eshop"]["contents"]["content"][title_index]["title"]["name"] = fixed_title_name + + # Next, we do the same for the reverse. + for entry_index, title_entry in enumerate(titles_to_uid): + title_name = title_entry["Name"] + if "
" not in title_name: + continue + + # Do the same as the above. + fixed_title_name = title_name.replace("\n", "") + fixed_title_name = fixed_title_name.replace("
", " ") + fixed_title_name = fixed_title_name.replace("
", " ") + fixed_title_name = fixed_title_name.replace(" ", " ") + titles_to_uid[entry_index]["Name"] = fixed_title_name + + # Save databases to file + with open(database_path, 'wb') as file: + file.write(pickle.dumps( + (title_database, + titles_to_uid) + )) + print('[Saved database to file]') # Create entry in database with friendCode def create_user(friend_code: int, network: NetworkType, add_new_instance: bool): # Make sure the user isn't trying to create any registered bot friend code. - if int(friend_code) == int(pretendoBotFC): + if int(friend_code) == int(PRETENDO_BOT_FC): raise Exception('invalid FC') - if int(friend_code) == int(nintendoBotFC): + if int(friend_code) == int(NINTENDO_BOT_FC): raise Exception('invalid FC') try: @@ -617,7 +664,7 @@ def user_page(friend_code: str): network: NetworkType try: - network = nameToNetworkType(request.args.get('network')) + network = name_to_network_type(request.args.get('network')) friend_code_int = int(friend_code.replace('-', '')) user_data = get_presence(friend_code_int, network, False) @@ -670,7 +717,7 @@ def new_user(friend_code: int, network: int = -1, user_check: bool = True): try: request_arg = request.data.decode('utf-8').split(',')[0] - network = nameToNetworkType(request_arg) + network = name_to_network_type(request_arg) except: pass create_user(friend_code, network, True) @@ -692,7 +739,7 @@ def user_presence(friend_code: int): # Check if a specific network is being specified as a query parameter. network_name = request.args.get('network') if network_name: - network = nameToNetworkType(network_name) + network = name_to_network_type(network_name) else: network = NetworkType.NINTENDO @@ -705,7 +752,7 @@ def user_presence(friend_code: int): def toggler(friend_code: int): network = NetworkType.NINTENDO if request.data.decode('utf-8').split(',')[2]: - network = nameToNetworkType(request.data.decode('utf-8').split(',')[2]) + network = name_to_network_type(request.data.decode('utf-8').split(',')[2]) try: fc = str(principal_id_to_friend_code(friend_code_to_principal_id(friend_code))).zfill(12) except: @@ -782,7 +829,7 @@ def deleter(friend_code: int): data = request.data.decode('utf-8').split(',') token = data[0] - network = nameToNetworkType(data[1]) + network = name_to_network_type(data[1]) discord_id = user_from_token(token).id db.session.execute( diff --git a/templates/package-lock.json b/templates/package-lock.json index 7d1dd36..150c881 100644 --- a/templates/package-lock.json +++ b/templates/package-lock.json @@ -9,14 +9,14 @@ "version": "7.0.5", "license": "MIT", "dependencies": { - "bootstrap": "5.3.5" + "bootstrap": "5.3.6" }, "devDependencies": { "autoprefixer": "10.4.21", "postcss": "8.5.3", "prettier": "3.5.3", "pug": "3.0.3", - "sass": "1.87.0" + "sass": "1.89.0" }, "engines": { "node": ">=20.0.0" @@ -43,13 +43,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", + "integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -59,9 +59,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", "dev": true, "license": "MIT", "dependencies": { @@ -472,9 +472,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", - "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", + "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", "funding": [ { "type": "github", @@ -505,9 +505,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -525,10 +525,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -569,9 +569,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001716", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz", - "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -663,9 +663,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.149", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.149.tgz", - "integrity": "sha512-UyiO82eb9dVOx8YO3ajDf9jz2kKyt98DEITRdeLPstOEuTlLzDA4Gyq5K9he71TQziU5jUVu2OAu5N48HmQiyQ==", + "version": "1.5.158", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz", + "integrity": "sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ==", "dev": true, "license": "ISC" }, @@ -845,9 +845,9 @@ } }, "node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", + "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", "dev": true, "license": "MIT" }, @@ -1299,9 +1299,9 @@ } }, "node_modules/sass": { - "version": "1.87.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", - "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz", + "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/templates/package.json b/templates/package.json index 00503a8..041a04c 100644 --- a/templates/package.json +++ b/templates/package.json @@ -17,13 +17,13 @@ "David Miller (https://davidmiller.io/)" ], "dependencies": { - "bootstrap": "5.3.5" + "bootstrap": "5.3.6" }, "devDependencies": { "autoprefixer": "10.4.21", "postcss": "8.5.3", "prettier": "3.5.3", "pug": "3.0.3", - "sass": "1.87.0" + "sass": "1.89.0" } } diff --git a/templates/src/pug/layouts/includes/footer.pug b/templates/src/pug/layouts/includes/footer.pug index cbba8dc..b490973 100644 --- a/templates/src/pug/layouts/includes/footer.pug +++ b/templates/src/pug/layouts/includes/footer.pug @@ -2,7 +2,7 @@ footer.py-4.bg-light.mt-auto .container-fluid.px-4 .d-flex.align-items-center.justify-content-between.small .text-muted - | Copyright © 3DS-RPC 2025 + | Copyright © 3DS-RPC 2026 .text-muted | By using this service, you agree to the a(href='/terms') Terms of Service