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