mirror of
https://github.com/MCMi460/3DS-RPC.git
synced 2025-06-19 14:05:41 -04:00
163 lines
5.7 KiB
Python
163 lines
5.7 KiB
Python
import requests, os, sys, time
|
|
import xmltodict, json
|
|
from api import *
|
|
from api.private import *
|
|
import pypresence
|
|
from typing import Literal, get_args
|
|
|
|
port = '2277'
|
|
host = 'http://127.0.0.1' + (':' + port if port else '')
|
|
|
|
def getAppPath(): # Credit to @HotaruBlaze
|
|
applicationPath = os.path.expanduser('~/Documents/3DS-RPC')
|
|
# Windows allows you to move your UserProfile subfolders, Such as Documents, Videos, Music etc.
|
|
# However os.path.expanduser does not actually check and assumes it's in the default location.
|
|
# This tries to correctly resolve the Documents path and fallbacks to default if it fails.
|
|
if os.name == 'nt':
|
|
try:
|
|
import ctypes.wintypes
|
|
CSIDL_PERSONAL = 5 # My Documents
|
|
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
|
|
buf=ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
|
|
ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
|
|
applicationPath = os.path.join(buf.value,'3DS-RPC')
|
|
except:pass
|
|
return applicationPath
|
|
|
|
_REGION = Literal['US', 'JP', 'GB', 'KR', 'TW']
|
|
|
|
class APIException(Exception):
|
|
pass
|
|
|
|
class TitleIDMatchError(Exception):
|
|
pass
|
|
|
|
class GameMatchError(Exception):
|
|
pass
|
|
|
|
class Client():
|
|
def __init__(self, region: _REGION, friendCode: str):
|
|
### Maintain typing ###
|
|
assert region in get_args(_REGION), '\'%s\' does not match _REGION' % region # Region assertion
|
|
convertFriendCodeToPrincipalId(friendCode) # Friend Code check
|
|
|
|
# Region and FC variables
|
|
self.region = region
|
|
self.friendCode = friendCode
|
|
|
|
# Try signing in
|
|
## If there is an error other than the user's account not existing, then error
|
|
try:
|
|
self.signUp()
|
|
except APIException as ex:
|
|
if not 'UNIQUE constraint failed: friends.friendCode' in str(ex):
|
|
raise ex
|
|
|
|
# Pull databases
|
|
self.titleDatabase = xmltodict.parse(requests.get('https://samurai.ctr.shop.nintendo.net/samurai/ws/%s/titles?shop_id=1&limit=5000&offset=0' % self.region, verify = False).text)
|
|
self.titlesToUID = requests.get('https://raw.githubusercontent.com/hax0kartik/3dsdb/master/jsons/list_%s.json' % self.region).json()
|
|
|
|
# Connect to Discord
|
|
self.connect()
|
|
# Discord-related variables
|
|
self.currentGame = {'@id': None}
|
|
|
|
# Get from API
|
|
def APIget(self, route:str, content:dict = {}):
|
|
return requests.get(host + '/' + route, data = content)
|
|
|
|
# Post to API
|
|
def APIpost(self, route:str, content:dict = {}):
|
|
return requests.post(host + '/' + route, data = content)
|
|
|
|
# Connect to PyPresence
|
|
def connect(self):
|
|
self.rpc = pypresence.Presence('1023094010383970304')
|
|
self.rpc.connect()
|
|
|
|
def signUp(self):
|
|
r = self.APIpost('user/c/%s' % self.friendCode)
|
|
try:
|
|
r = r.json()
|
|
except:
|
|
raise APIException(r.content)
|
|
if r['Exception']:
|
|
raise APIException(r['Exception'])
|
|
|
|
def fetch(self):
|
|
r = self.APIget('user/%s' % self.friendCode)
|
|
try:
|
|
r = r.json()
|
|
except:
|
|
raise APIException(r.content)
|
|
if r['Exception']:
|
|
raise APIException(r['Exception'])
|
|
return r
|
|
|
|
def loop(self):
|
|
userData = self.fetch()
|
|
presence = userData['User']['Presence']
|
|
|
|
if userData['User']['online'] and presence:
|
|
uid = None
|
|
tid = hex(int(presence['titleID']))[2:].zfill(16).upper()
|
|
for game in self.titlesToUID:
|
|
if game['TitleID'] == tid:
|
|
uid = game['UID']
|
|
break
|
|
if not uid:
|
|
raise TitleIDMatchError('unknown title id: %s' % tid)
|
|
|
|
game = None
|
|
#print(self.titleDatabase)
|
|
for title in self.titleDatabase['eshop']['contents']['content']:
|
|
if title['title']['@id'] == uid:
|
|
game = title['title']
|
|
break
|
|
if not game:
|
|
raise GameMatchError('unknown game: %s' % uid)
|
|
|
|
print('Update', end = '')
|
|
if self.currentGame != game:
|
|
print(' [%s -> %s]' % (self.currentGame['@id'], game['@id']), end = '')
|
|
self.currentGame = game
|
|
self.start = int(time.time())
|
|
print()
|
|
self.rpc.update(
|
|
details = game['name'],
|
|
large_image = game['icon_url'].replace('https://kanzashi-ctr.cdn.nintendo.net/i/', host + '/cdn/i/'),
|
|
large_text = game['name'],
|
|
start = self.start,
|
|
#buttons = [{'label': 'Nintendo eShop', 'url': game[]},]
|
|
# eShop URL could be https://api.qrserver.com/v1/create-qr-code/?data=ESHOP://{uid}
|
|
# But that's dumb so no
|
|
)
|
|
else:
|
|
print('Clear [%s -> %s]' % (self.currentGame['@id'], None))
|
|
self.currentGame = {'@id': None}
|
|
self.rpc.clear()
|
|
time.sleep(30)
|
|
|
|
def main():
|
|
# Create directory for logging and friend code saving
|
|
path = getAppPath()
|
|
if not os.path.isdir(path):
|
|
os.mkdir(path)
|
|
privateFile = os.path.join(path, 'private.txt')
|
|
if not os.path.isfile(privateFile):
|
|
friendCode = input('Please enter your 3DS\' friend code\n> ')
|
|
with open(privateFile, 'w') as file:
|
|
file.write(json.dumps({
|
|
'friendCode': friendCode,
|
|
}))
|
|
else:
|
|
with open(privateFile, 'r') as file:
|
|
friendCode = json.loads(file.read())['friendCode']
|
|
|
|
client = Client('US', friendCode)
|
|
while True:
|
|
client.loop()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|