diff --git a/api/util.py b/api/util.py index 493fa11..83e3f15 100644 --- a/api/util.py +++ b/api/util.py @@ -218,17 +218,22 @@ def status(self): """ Shows your current status """ + console = self.client.userData['consoles'][0] if self.client.userData.get('consoles') else None text = [ '%s: %s' % ('Exception', self.client.userData['Exception']), - '%s: %s' % ('Friend Code', self.client.userData['User']['friendCode']), - '%s: %s' % ('Online', self.client.userData['User']['online']), - '%s: %s' % ('Message', self.client.userData['User']['message']), ] - if self.client.userData['User']['username']: - text.append('%s: %s' % ('Username', self.client.userData['User']['username'])) - text.append('%s: %s' % ('Mii', self.client.userData['User']['mii']['face'])) - if self.client.userData['User']['online']: - text.append('%s: %s' % ('Game', self.client.userData['User']['Presence']['game']['name'])) + if not console: + text.append('%s: %s' % ('Online', False)) + return self._log('\n'.join(text), Color.BLUE) + text.append('%s: %s' % ('Friend Code', console['friendCode'])) + text.append('%s: %s' % ('Network', console['network'])) + text.append('%s: %s' % ('Online', console['online'])) + text.append('%s: %s' % ('Message', console['message'])) + if console.get('username'): + text.append('%s: %s' % ('Username', console['username'])) + text.append('%s: %s' % ('Mii', console['mii']['face'])) + if console['online'] and console.get('Presence'): + text.append('%s: %s' % ('Game', console['Presence']['game']['name'])) return self._log('\n'.join(text), Color.BLUE) def discord(self, command:typing.Literal['connect', 'disconnect'] = 'connect', pipe:int = '0'): @@ -291,6 +296,21 @@ def config(self, command:typing.Literal['help', 'profilebutton', 'elapsedtime', self.client.reflectConfig() return self._log('Done', Color.BLUE) + def apikey(self, newKey:str): + """ + Sets a new API key + self, newKey:str + """ + self.client.updateKey(newKey) + return self._log('Done', Color.BLUE) + + def refresh(self): + """ + Manually fetches the latest status from the server + """ + self.client.loop() + return self._log('Done', Color.BLUE) + def log(self): """ Shows activity log @@ -299,12 +319,23 @@ def log(self): # Exception handling def APIExcept(r): - text = r.text - if '429' in r.text: - text = 'You have reached your rate-limit for this resource.' - elif '502' in r.text: - text = 'The frontend is offline. Please try again later.' - raise APIException(text) + status = r.status_code + if status == 429: + raise APIException('You have reached your rate-limit for this resource.') + if status == 404: + raise APIException('Not found (404): the configured endpoint does not expose this API route. Make sure you selected the correct endpoint.') + if status == 502: + raise APIException('Backend currently offline. please try again later') + try: + data = r.json() + text = str(data.get('Exception') or data) + except Exception: + text = r.text + if ' 200: + text = text[:200] + '...' + raise APIException(text or 'Unexpected response from the server') class APIException(Exception): pass diff --git a/app.py b/app.py index 4182479..aef1b8f 100644 --- a/app.py +++ b/app.py @@ -8,18 +8,42 @@ from client import _REGION import webbrowser +import time + +from enum import IntEnum + +class Page(IntEnum): + WELCOME = 0 + ENDPOINT = 1 + LINK = 2 + LOADING = 3 + MAIN = 4 + SETTINGS = 5 + +class UIBridge(QObject): + updated = pyqtSignal(object) + errored = pyqtSignal(object, object) + reauthRequested = pyqtSignal() + statusChanged = pyqtSignal(str) + +def fmtTime(ts): + if not ts: + return '\u2014' + try: + return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(ts))) + except (ValueError, TypeError, OverflowError): + return str(ts) # 3DS Variables -friendCode = None +apiKey = '' config = {} if not os.path.isdir(path): os.mkdir(path) if os.path.isfile(privateFile): with open(privateFile, 'r') as file: js = json.loads(file.read()) - friendCode = js['friendCode'] + apiKey = js.get('apiKey', '') config = js - del config['friendCode'] client = None # PyQt5 Variables @@ -29,7 +53,10 @@ def loadPix(url): _pixmap = QPixmap() - _pixmap.loadFromData(requests.get(url, verify = False).content) + try: + _pixmap.loadFromData(requests.get(url, verify = False, timeout = 10).content) + except Exception: + pass return _pixmap def up(_label,image): @@ -49,46 +76,116 @@ def __init__(self, MainWindow): self.underLyingButton2.clicked.connect(lambda a : self.errorMes()) self.err = None self.traceback = None + self._customEndpoint = '' + self.reauthing = False + self.styled = False + self.bridge = UIBridge() + self.bridge.updated.connect(self.update) + self.bridge.errored.connect(self.error) + self.bridge.reauthRequested.connect(self.reauthorize) + self.bridge.statusChanged.connect(self.setStatus) def selfService(self, app): self.app = app self.MainWindow.setStyleSheet(style) self.assignVariables() - self.page = 0 + self.versionLabel.setText('3DS-RPC v%s' % version) + self.page = Page.WELCOME self.updatePage() self.MainWindow.closeEvent = self.closeEvent self.underLyingButton = QPushButton() self.underLyingButton.clicked.connect(lambda a : self.updateColor()) - if self.state and client.userData.get('User'): + if self.state and client.userData.get('consoles'): self.stylize() self.closeButton.clicked.connect(sys.exit) - self.settingsButton.clicked.connect(lambda a : self.updatePage(3)) - self.okButton.clicked.connect(lambda a : self.updatePage(2)) + self.settingsButton.clicked.connect(lambda a : self.updatePage(Page.SETTINGS)) + self.okButton.clicked.connect(lambda a : self.updatePage(Page.MAIN)) + self.refreshButton.clicked.connect(self.refresh) self.state = False - threading.Thread(target = self.grabCode, daemon = True).start() def assignVariables(self): - self.continueButton.clicked.connect(lambda a : self.updatePage(1)) + self.continueButton.clicked.connect(lambda a : self.updatePage(Page.ENDPOINT)) self.loginButton.clicked.connect(self.changeState) - self.botFCLabel.setText('-'.join(nintendoBotFC[i:i+4] for i in range(0, len(nintendoBotFC), 4))) + self.welcomeLogo.setPixmap(QPixmap(getPath('layout/resources/logo.png'))) + + self.endpointButton.clicked.connect(lambda a : self.showEndpointPage()) + self.backEndpointButton.clicked.connect(lambda a : self.updatePage(Page.WELCOME if not client else Page.SETTINGS)) + self.saveEndpointButton.clicked.connect(self.saveEndpoint) + self.endpointInput.currentIndexChanged.connect(lambda a : self.updateEndpointField()) + self.customEndpointInput.textChanged.connect(self.captureCustomEndpoint) + self.endpointHint.setStyleSheet('color: #D32F2F;') + self.updateEndpointField() + + def captureCustomEndpoint(self, text): + if self.customEndpointInput.isEnabled(): + self._customEndpoint = text + self.updateSaveState() + + def updateEndpointField(self): + endpoint = self.endpointInput.currentText().strip() + if endpoint == 'custom': + self.customEndpointInput.setEnabled(True) + self.customEndpointInput.setText(self._customEndpoint or (client.customEndpoint if client else config.get('customEndpoint', ''))) + else: + self.customEndpointInput.setEnabled(False) + self.customEndpointInput.setText(getHost(endpoint)) + self.updateSaveState() + + def updateSaveState(self): + endpoint = self.endpointInput.currentText().strip() + if endpoint == 'custom': + try: + validateEndpoint(self.customEndpointInput.text()) + valid, message = True, '' + except ValueError as e: + valid, message = False, str(e) + else: + valid, message = True, '' + self.saveEndpointButton.setEnabled(valid) + self.endpointHint.setText(message) + self.endpointHint.setVisible(endpoint == 'custom' and bool(message)) + + def showEndpointPage(self): + if not client: + return + index = self.endpointInput.findText(client.endpoint) + if index != -1: + self.endpointInput.setCurrentIndex(index) + self.updateEndpointField() + self.updatePage(Page.ENDPOINT) + + def saveEndpoint(self): + global config + config['endpoint'] = self.endpointInput.currentText().strip() or 'official' + config['customEndpoint'] = '' + if config['endpoint'] == 'custom': + try: + config['customEndpoint'] = validateEndpoint(self.customEndpointInput.text()) + except ValueError as e: + dlg = QMessageBox() + dlg.setWindowTitle('3DS-RPC') + dlg.setText(str(e)) + dlg.exec_() + return + if client: + client.setEndpoint(config['endpoint'], config['customEndpoint']) + self.updatePage(Page.SETTINGS) + else: + self.updatePage(Page.LINK) def stylize(self): + self.styled = True self.underLyingButton.click() - # Update dynamic elements - self.setFontText(self.username, client.userData['User']['username']) - self.miiLabel.setScaledContents(True) - self.gameIcon.setScaledContents(True) - if client.userData['User']['mii']: - up(self.miiLabel, client.userData['User']['mii']['face']) + self.updateProfile() + self.updateDetails() # Update others - self.friendCard.mouseReleaseEvent = lambda event : self.openLink(host + '/user/%s' % client.userData['User']['friendCode']) self.okButtonLogout.clicked.connect(self.logout) for button in ((self.showElapsedOff, self.showElapsedOn, 'showElapsed'), (self.showProfileButtonOff, self.showProfileButtonOn, 'showProfileButton'), (self.showSmallImageOff, self.showSmallImageOn, 'showSmallImage')): self.setFontText(button[0], 'No') @@ -99,6 +196,30 @@ def stylize(self): for label in (self.showElapsedText, self.showProfileButtonText, self.showSmallImageText): self.setFontText(label, label.text()) + def updateProfile(self): + console = client.userData['consoles'][0] if client.userData.get('consoles') else None + if not console: + return + self.miiLabel.setScaledContents(True) + self.gameIcon.setScaledContents(True) + self.setFontText(self.username, console.get('username', '')) + if console.get('mii'): + up(self.miiLabel, console['mii']['face']) + self.friendCard.mouseReleaseEvent = lambda event : self.openLink(client.host + '/user/%s' % console['friendCode']) + + def refresh(self): + if not client: + return + try: + client.loop() + except Exception as e: + dlg = QMessageBox() + dlg.setWindowTitle('3DS-RPC') + dlg.setText('Refresh failed: %s' % str(e)) + dlg.exec_() + return + self.updateProfile() + def updateSettings(self, button, activate): client.__dict__[button[2]] = activate client.reflectConfig() @@ -107,18 +228,20 @@ def updateSettings(self, button, activate): def updateColor(self): self.MainWindow.setStyleSheet('') - if not client.userData['User']['online']: + console = client.userData['consoles'][0] if client.userData.get('consoles') else None + online = bool(console and console.get('online')) + if not online: self.MainWindow.setStyleSheet(offlineStyle) else: self.MainWindow.setStyleSheet(style) - self.status.setText('Online' if client.userData['User']['online'] else 'Offline') + self.status.setText('Online' if online else 'Offline') def setFontText(self, label, text): label.setText(text) i = 21 width = 30000 - while label.width() < width: + while label.width() < width and i > 1: i -= 1 font = QFont('Arial', i) label.setFont(font) @@ -129,64 +252,78 @@ def openLink(self, url:str): webbrowser.open(url) def closeEvent(self, event): - if friendCode and client: + if apiKey and client: event.ignore() self.MainWindow.hide() tray.show() - elif friendCode: + elif apiKey: event.accept() self.app.quit() else: sys.exit() - def grabCode(self): - global friendCode - if friendCode: - return - friendCode = str(principal_id_to_friend_code(friend_code_to_principal_id(self.waitUntil()))).zfill(12) - - def waitUntil(self): - while True: - while not self.state: - pass - try: - friend_code_to_principal_id(self.fcInput.text().strip()) - break - except: - self.state = False - return self.fcInput.text().strip() - def changeState(self): - self.state = True - while not friendCode: - if not self.state: - dlg = QMessageBox() - dlg.setWindowTitle('3DS-RPC') - dlg.setText('An invalid friendcode has been passed') - dlg.exec_() - return - self.MainWindow.close() + global apiKey + newKey = self.keyInput.text().strip() + if not newKey: + dlg = QMessageBox() + dlg.setWindowTitle('3DS-RPC') + dlg.setText('Please enter your API key.') + dlg.exec_() + return + apiKey = newKey + if client: + client.updateKey(newKey) + self.reauthing = False + self.refresh() + self.updatePage(Page.MAIN) + else: + self.MainWindow.close() def updatePage(self, page = None): self.page = page if page != None else self.page self.stackedWidget.setCurrentIndex(page if page != None else self.page) + def updateDetails(self): + if not client: + return + self.endpointDetail.setText('Endpoint: %s' % client.host) + console = client.userData['consoles'][0] if client.userData.get('consoles') else None + if not console: + self.networkDetail.setText('Network: \u2014') + self.lastActiveDetail.setText('Last Active: \u2014') + self.lastOnlineDetail.setText('Last Online: \u2014') + return + self.networkDetail.setText('Network: %s' % console.get('network', '\u2014').title()) + self.lastActiveDetail.setText('Last Active: %s' % fmtTime(console.get('lastAccessed'))) + self.lastOnlineDetail.setText('Last Online: %s' % fmtTime(console.get('lastOnline'))) + def update(self, data): - if data: - self.gamePlate.show() - game = client.userData['User']['Presence']['game'] - self.gamePlate.mouseReleaseEvent = lambda a : self.openLink('https://www.google.com/search?q=%s' % '+'.join((game['name'] + ' ' + game['publisher']['name']).split(' '))) - if data.get('large_image') and not local: - up(self.gameIcon, data['large_image']) - self.setFontText(self.gameName, data['details']) - else: - self.gamePlate.hide() - self.underLyingButton.click() + try: + if self.page == Page.LOADING: + self.updatePage(Page.MAIN) + if client.userData.get('consoles') and not self.styled: + self.stylize() + self.updateProfile() + self.updateDetails() + if data: + self.gamePlate.show() + game = client.userData['consoles'][0]['Presence']['game'] + publisher = (game.get('publisher') or {}).get('name', '') + self.gamePlate.mouseReleaseEvent = lambda a : self.openLink('https://www.google.com/search?q=%s' % '+'.join((game['name'] + ' ' + publisher).split(' '))) + if data.get('large_image') and not client.local: + up(self.gameIcon, data['large_image']) + self.setFontText(self.gameName, data['details']) + else: + self.gamePlate.hide() + self.underLyingButton.click() + except Exception as e: + print('Update error: %s' % e) def error(self, error, traceback): self.err = error self.traceback = traceback - print(self.error) + print(self.err) self.underLyingButton2.click() def errorMes(self): @@ -202,6 +339,16 @@ def logout(self): os.remove(privateFile) sys.exit() + def reauthorize(self): + self.reauthing = True + self.keyInput.setText('') + self.updatePage(Page.LINK) + self.MainWindow.show() + + def setStatus(self, message): + if self.page == Page.LOADING: + self.loadingText.setText(message) + class SystemTrayApp(QSystemTrayIcon): def __init__(self, icon, parent): QSystemTrayIcon.__init__(self, icon, parent) @@ -224,6 +371,11 @@ def reopen(self): app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(False) + lock = acquireLock() + if not lock: + QMessageBox.warning(None, '3DS-RPC', '3DS-RPC is already running.') + sys.exit(0) + MainWindow = QMainWindow() window = GUI(MainWindow) @@ -232,19 +384,20 @@ def reopen(self): window.setupUi(MainWindow) window.selfService(app) - if not friendCode: + if not apiKey: MainWindow.show() app.exec_() - client = Client(friendCode, config, GUI = window) - client.connect() - threading.Thread(target = client.background, daemon = True).start() - while not client.userData and not window.err: - pass + config['apiKey'] = apiKey + + client = Client(apiKey, config, GUI = window) window.state = True window.setupUi(MainWindow) window.selfService(app) - window.updatePage(2) + window.updatePage(Page.LOADING) MainWindow.show() + client.connect() + threading.Thread(target = client.background, daemon = True).start() + sys.exit(app.exec_()) diff --git a/client.py b/client.py index 96c4986..e753f43 100644 --- a/client.py +++ b/client.py @@ -4,26 +4,41 @@ import xmltodict, json import pickle import asyncio, threading +from urllib.parse import urlsplit try: from api import * except: sys.path.append('../') from api import * import pypresence +from PyQt5.QtCore import QLockFile requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) -local = False -version = 0.31 - -host = 'https://3dsrpc.com' # Change the host as you'd wish -if local: - host = 'http://127.0.0.1:2277' - -## The below contains 3dsrpc.com-specific information -## You will have to provide your own 'bot' FC if you are planning -## on running your own front and backend. -friend_code_to_principal_id(nintendoBotFC) # A quick verification check +version = 0.32 + +# Endpoint presets. The selection lives client-side in the config. +officialHost = 'https://3dsrpc.com' +host = officialHost + +def getHost(endpoint:str, customEndpoint:str = '') -> str: + endpoint = (endpoint or 'official').lower() + if endpoint == 'custom' and customEndpoint: + return customEndpoint.strip().rstrip('/') + return officialHost + +def validateEndpoint(url:str) -> str: + url = (url or '').strip() + if not url: + raise ValueError('Please enter a custom endpoint URL.') + parts = urlsplit(url) + if parts.scheme not in ('http', 'https') or not parts.netloc: + raise ValueError('Custom endpoint must be a full URL, e.g. https://your-server.example.com') + if parts.path not in ('', '/'): + raise ValueError('Custom endpoint cannot include a subpath. Use just the host, e.g. https://your-server.example.com') + if parts.query or parts.fragment: + raise ValueError('Custom endpoint cannot include a query string or fragment.') + return url.rstrip('/') _REGION = typing.Literal['ALL', 'US', 'JP', 'GB', 'KR', 'TW'] path = getAppPath() @@ -32,37 +47,68 @@ # Config template configTemplate = { - 'friendCode': '', + 'apiKey': '', + 'endpoint': 'official', + 'customEndpoint': '', 'showElapsed': True, 'showProfileButton': False, 'showSmallImage': False, - 'fetchTime': 30, + 'fetchTime': 60, } +lockPath = os.path.join(path, '3dsrpc.lock') + +def acquireLock(timeout:int = 100): + lock = QLockFile(lockPath) + return lock if lock.tryLock(timeout) else None + def log(text:str): with open(logFile, 'a') as file: file.write('%s: %s\n' % (time.time(), text.replace('\n',' '))) print(Color.RED + text) +class RateLimitedError(Exception): + def __init__(self, retry_after = None): + try: + self.wait = float(retry_after) + except (TypeError, ValueError): + self.wait = 30 + super().__init__('Rate limited. Waiting %ss.' % self.wait) + +class BackendOfflineError(Exception): + def __init__(self): + super().__init__('Backend currently offline. Retrying later.') + +class InvalidAPIKeyError(Exception): + def __init__(self): + super().__init__('Invalid console API key.') + class Client(): - def __init__(self, friendCode: str, config:dict, *, GUI = None): - ### Maintain typing ### - friendCode = str(principal_id_to_friend_code(friend_code_to_principal_id(friendCode))).zfill(12) # Friend Code check - with open(privateFile, 'w') as file: # Save FC and config to file + def __init__(self, apiKey: str, config:dict, *, GUI = None): + with open(privateFile, 'w') as file: # Save API key and config to file js = configTemplate - js['friendCode'] = friendCode + js['apiKey'] = apiKey for key in config.keys(): if key in configTemplate.keys(): js[key] = config[key] file.write(json.dumps(js)) - # FC variables - self.friendCode = friendCode + # Server-auth variables + self.apiKey = js.get('apiKey', '') + + # Endpoint variables + self.endpoint = js.get('endpoint', 'official') + self.customEndpoint = js.get('customEndpoint', '') + self.host = getHost(self.endpoint, self.customEndpoint) + self.local = '127.0.0.1' in self.host or 'localhost' in self.host # Client-config self.connected = False self.userData = {} + # Re-authentication coordination + self.authWait = threading.Event() + # Discord-related variables self.currentGame = {'@id': None} self.showElapsed = js['showElapsed'] @@ -84,13 +130,23 @@ def reflectConfig(self): js[key] = self.__dict__[key] file.write(json.dumps(js)) + # Change the endpoint (official / custom) + def setEndpoint(self, endpoint:str, customEndpoint:str = ''): + self.endpoint = endpoint + self.customEndpoint = customEndpoint + self.host = getHost(endpoint, customEndpoint) + self.local = '127.0.0.1' in self.host or 'localhost' in self.host + self.reflectConfig() + + # Set a new API key after an invalid/expired one + def updateKey(self, apiKey:str): + self.apiKey = apiKey + self.authWait.set() + self.reflectConfig() + # Get from API def APIget(self, route:str, content:dict = {}): - return requests.get(host + '/api/' + route, data = content, headers = {'User-Agent':'3DS-RPC/%s' % version,}) - - # Post to API - def APIpost(self, route:str, content:dict = {}): - return requests.post(host + '/api/' + route, data = content, headers = {'User-Agent':'3DS-RPC/%s' % version,}) + return requests.get(self.host + '/api/' + route, params = content, headers = {'User-Agent':'3DS-RPC/%s' % version, 'X-API-KEY': self.apiKey,}) # Connect to PyPresence def connect(self, pipe:str = '0'): @@ -99,7 +155,7 @@ def connect(self, pipe:str = '0'): self.rpc.connect() except Exception as e: if self.GUI: - self.GUI.error(str(e), traceback.format_exc()) + self.GUI.bridge.errored.emit(str(e), traceback.format_exc()) else: raise e self.connected = True @@ -113,39 +169,35 @@ def disconnect(self): self.rpc = None self.connected = False - def login(self): - r = self.APIpost('user/create/%s' % self.friendCode) - try: - r = r.json() - except: - APIExcept(r) - if r['Exception']: - if not 'UNIQUE constraint failed: friends.friendCode' in r['Exception']['Error']: - raise APIException(r['Exception']) - return r - def fetch(self): - r = self.APIget('user/%s' % self.friendCode) + r = self.APIget('user/activeConsoles') + if r.status_code == 429: + raise RateLimitedError(r.headers.get('Retry-After')) + if r.status_code == 502: + raise BackendOfflineError() try: r = r.json() except: APIExcept(r) if r['Exception']: - if 'not recognized' in r['Exception']['Error']: - print('%sRemember, the bot\'s friend code is as follows:\n%s%s' % (Color.YELLOW, '-'.join(nintendoBotFC[i:i+4] for i in range(0, len(nintendoBotFC), 4)), Color.DEFAULT)) + error = r['Exception']['Error'] + if 'offline' in error: + raise BackendOfflineError() + if 'invalid console API key' in error: + raise InvalidAPIKeyError() raise APIException(r['Exception']) return r def loop(self): userData = self.fetch();self.userData = userData - presence = userData['User']['Presence'] + console = userData['consoles'][0] if userData.get('consoles') else None - _pass = None - if userData['User']['online'] and presence: + logger = 'Update' + if console and console.get('online') and console.get('Presence'): + presence = console['Presence'] game = presence['game'] - logger = 'Update' if self.currentGame != game: logger += ' [%s -> %s]' % (self.currentGame['@id'], game['@id']) self.currentGame = game @@ -158,18 +210,18 @@ def loop(self): # Include View Profile setting? # Certainly something when presence['joinable'] == True } - if game['icon_url']: - kwargs['large_image'] = game['icon_url'].replace('/cdn/', host + '/cdn/') + if game.get('icon_url'): + kwargs['large_image'] = game['icon_url'].replace('/cdn/', self.host + '/cdn/') kwargs['large_text'] = game['name'] - if presence['gameDescription']: + if presence.get('gameDescription'): kwargs['state'] = presence['gameDescription'] - if self.showProfileButton and userData['User']['username']: - kwargs['buttons'] = [{'label': 'Profile', 'url': host + '/user/' + userData['User']['friendCode']},] + if self.showProfileButton and console.get('username'): + kwargs['buttons'] = [{'label': 'Profile', 'url': self.host + '/user/' + console['friendCode']},] if self.showElapsed: kwargs['start'] = self.start - if self.showSmallImage and userData['User']['username'] and game['icon_url']: - kwargs['small_image'] = userData['User']['mii']['face'] - kwargs['small_text'] = '-'.join(userData['User']['friendCode'][i:i+4] for i in range(0, 12, 4)) + if self.showSmallImage and console.get('username') and game.get('icon_url'): + kwargs['small_image'] = console['mii']['face'] + kwargs['small_text'] = '-'.join(console['friendCode'][i:i+4] for i in range(0, 12, 4)) for key in list(kwargs): # Blatant rip from OpenEmuRPC (also made by me. Check it out if you want) if isinstance(kwargs[key], str) and not 'image' in key: if len(kwargs[key]) < 2: @@ -177,57 +229,95 @@ def loop(self): elif len(kwargs[key]) > 128: kwargs[key] = kwargs[key][:128] if self.connected:self.rpc.update(**kwargs) - if self.GUI:self.GUI.update(kwargs) + if self.GUI:self.GUI.bridge.updated.emit(kwargs) else: logger = 'Clear [%s -> %s]' % (self.currentGame['@id'], None) self.currentGame = {'@id': None} if self.connected:self.rpc.clear() - if self.GUI:self.GUI.update(None) + if self.GUI:self.GUI.bridge.updated.emit(None) self.gameLog.append(logger) def background(self): try: - self.login() # Create account if not yet existent while True: - self.loop() - time.sleep(self.fetchTime) # Wait 30 seconds between calls + try: + self.loop() + except InvalidAPIKeyError: + log('Invalid API key.') + if self.GUI: + self.GUI.bridge.reauthRequested.emit() + else: + log('Use the \'apikey\' command to set a new key.') + self.authWait.wait() + self.authWait.clear() + except (RateLimitedError, BackendOfflineError) as e: + log(str(e)) + if self.GUI: + self.GUI.bridge.statusChanged.emit('Rate limited. Retrying shortly...' if isinstance(e, RateLimitedError) else 'Backend offline. Retrying...') + time.sleep(getattr(e, 'wait', self.fetchTime)) + time.sleep(self.fetchTime) # Wait 60 seconds between calls except Exception as e: if self.GUI: - self.GUI.error(str(e), traceback.format_exc()) + self.GUI.bridge.errored.emit(str(e), traceback.format_exc()) else: log('Failed\n' + str(e)) print(traceback.format_exc()) os._exit(0) def main(): - friendCode = None + lock = acquireLock() + if not lock: + print('%sAnother instance of 3DS-RPC is already running.%s' % (Color.RED, Color.DEFAULT)) + os._exit(0) + + apiKey = None + config = {} - # Create directory for logging and friend code saving + # Create directory for logging and API key saving if not os.path.isdir(path): os.mkdir(path) try: if os.path.isfile(privateFile): with open(privateFile, 'r') as file: js = json.loads(file.read()) - friendCode = js['friendCode'] + apiKey = js.get('apiKey', '') config = js - del config['friendCode'] - else: - raise Exception() + config.pop('apiKey', None) except: - print('%sPlease take this time to add the bot\'s FC to your target 3DS\' friends list.\n%sBot FC: %s%s' % (Color.YELLOW, Color.DEFAULT, Color.BLUE, '-'.join(nintendoBotFC[i:i+4] for i in range(0, 12, 4)))) - input('%s[Press enter to continue]%s' % (Color.GREEN, Color.DEFAULT)) - friendCode = input('Please enter your 3DS\' friend code\n> %s' % Color.PURPLE) + apiKey = None config = {} + + # The endpoint must be known before we can connect, so ask first if it's missing + if not config.get('endpoint'): + print('%sWhich endpoint would you like to use? (official or custom)%s' % (Color.YELLOW, Color.DEFAULT)) + endpoint = input('Please enter your endpoint (official or custom)\n> %s' % Color.PURPLE) + if endpoint.lower() not in ('official', 'custom'): + endpoint = 'official' + config['endpoint'] = endpoint + if endpoint.lower() == 'custom': + while True: + customUrl = input('Please enter your custom endpoint URL\n> %s' % Color.PURPLE) + try: + config['customEndpoint'] = validateEndpoint(customUrl) + break + except ValueError as e: + print('%s%s%s' % (Color.RED, e, Color.DEFAULT)) + print(Color.DEFAULT, end = '') + + if not apiKey: + print('%sPlease grab your API key from the Settings page on your endpoint%s' % (Color.YELLOW, Color.DEFAULT)) + apiKey = input('Please enter your API key\n> %s' % Color.PURPLE) print(Color.DEFAULT, end = '') try: - client = Client(friendCode, config) - except (AssertionError, FriendCodeValidityError) as e: + client = Client(apiKey, config) + except (AssertionError) as e: if os.path.isfile(privateFile): os.remove(privateFile) raise e + print('%sConnecting to %s%s' % (Color.BLUE, client.host, Color.DEFAULT)) + # Begin main thread for user configuration con = Console(client) try: diff --git a/layout/__init__.py b/layout/__init__.py index 9d45b24..2002c82 100644 --- a/layout/__init__.py +++ b/layout/__init__.py @@ -1,252 +1,356 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'mainwindow.ui' -# -# Created by: PyQt5 UI code generator 5.15.7 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(600, 600) - self.stackedWidget = QtWidgets.QStackedWidget(MainWindow) - self.stackedWidget.setGeometry(QtCore.QRect(0, 0, 601, 601)) - self.stackedWidget.setObjectName("stackedWidget") - self.bot = QtWidgets.QWidget() - self.bot.setObjectName("bot") - self.description = QtWidgets.QLabel(self.bot) - self.description.setGeometry(QtCore.QRect(130, 80, 331, 71)) - self.description.setAlignment(QtCore.Qt.AlignCenter) - self.description.setWordWrap(True) - self.description.setObjectName("description") - self.section = QtWidgets.QGroupBox(self.bot) - self.section.setGeometry(QtCore.QRect(40, 190, 511, 211)) - self.section.setTitle("") - self.section.setObjectName("section") - self.continueButton = QtWidgets.QPushButton(self.section) - self.continueButton.setGeometry(QtCore.QRect(160, 140, 181, 31)) - self.continueButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.continueButton.setObjectName("continueButton") - self.botFCLabel = QtWidgets.QLabel(self.section) - self.botFCLabel.setGeometry(QtCore.QRect(90, 40, 331, 71)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setUnderline(True) - font.setWeight(75) - self.botFCLabel.setFont(font) - self.botFCLabel.setAlignment(QtCore.Qt.AlignCenter) - self.botFCLabel.setWordWrap(True) - self.botFCLabel.setObjectName("botFCLabel") - self.title = QtWidgets.QLabel(self.bot) - self.title.setGeometry(QtCore.QRect(100, 20, 391, 61)) - font = QtGui.QFont() - font.setPointSize(31) - self.title.setFont(font) - self.title.setAlignment(QtCore.Qt.AlignCenter) - self.title.setObjectName("title") - self.description_2 = QtWidgets.QLabel(self.bot) - self.description_2.setGeometry(QtCore.QRect(130, 420, 331, 81)) - self.description_2.setAlignment(QtCore.Qt.AlignCenter) - self.description_2.setWordWrap(True) - self.description_2.setOpenExternalLinks(True) - self.description_2.setObjectName("description_2") - self.stackedWidget.addWidget(self.bot) - self.link = QtWidgets.QWidget() - self.link.setObjectName("link") - self.description2 = QtWidgets.QLabel(self.link) - self.description2.setGeometry(QtCore.QRect(130, 80, 331, 71)) - self.description2.setAlignment(QtCore.Qt.AlignCenter) - self.description2.setWordWrap(True) - self.description2.setObjectName("description2") - self.section2 = QtWidgets.QGroupBox(self.link) - self.section2.setGeometry(QtCore.QRect(40, 190, 511, 211)) - self.section2.setTitle("") - self.section2.setObjectName("section2") - self.loginButton = QtWidgets.QPushButton(self.section2) - self.loginButton.setGeometry(QtCore.QRect(160, 140, 181, 31)) - self.loginButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.loginButton.setObjectName("loginButton") - self.fcInput = QtWidgets.QLineEdit(self.section2) - self.fcInput.setGeometry(QtCore.QRect(80, 70, 341, 24)) - self.fcInput.setObjectName("fcInput") - self.title2 = QtWidgets.QLabel(self.link) - self.title2.setGeometry(QtCore.QRect(100, 20, 391, 61)) - font = QtGui.QFont() - font.setPointSize(31) - self.title2.setFont(font) - self.title2.setAlignment(QtCore.Qt.AlignCenter) - self.title2.setObjectName("title2") - self.description_3 = QtWidgets.QLabel(self.link) - self.description_3.setGeometry(QtCore.QRect(130, 420, 331, 81)) - self.description_3.setAlignment(QtCore.Qt.AlignCenter) - self.description_3.setWordWrap(True) - self.description_3.setOpenExternalLinks(True) - self.description_3.setObjectName("description_3") - self.stackedWidget.addWidget(self.link) - self.main = QtWidgets.QWidget() - self.main.setObjectName("main") - self.settingsButton = QtWidgets.QPushButton(self.main) - self.settingsButton.setGeometry(QtCore.QRect(0, -20, 601, 61)) - self.settingsButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.settingsButton.setObjectName("settingsButton") - self.friendCard = QtWidgets.QGroupBox(self.main) - self.friendCard.setGeometry(QtCore.QRect(80, 190, 451, 231)) - self.friendCard.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.friendCard.setTitle("") - self.friendCard.setObjectName("friendCard") - self.namePlate = QtWidgets.QGroupBox(self.friendCard) - self.namePlate.setGeometry(QtCore.QRect(0, 170, 451, 81)) - self.namePlate.setTitle("") - self.namePlate.setObjectName("namePlate") - self.username = QtWidgets.QLabel(self.namePlate) - self.username.setGeometry(QtCore.QRect(280, 10, 151, 41)) - self.username.setText("") - self.username.setAlignment(QtCore.Qt.AlignCenter) - self.username.setObjectName("username") - self.status = QtWidgets.QLabel(self.friendCard) - self.status.setGeometry(QtCore.QRect(10, 10, 101, 21)) - self.status.setText("") - self.status.setObjectName("status") - self.miiLabel = QtWidgets.QLabel(self.friendCard) - self.miiLabel.setGeometry(QtCore.QRect(280, 20, 151, 151)) - self.miiLabel.setText("") - self.miiLabel.setObjectName("miiLabel") - self.closeButton = QtWidgets.QPushButton(self.main) - self.closeButton.setGeometry(QtCore.QRect(70, 570, 461, 61)) - self.closeButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.closeButton.setObjectName("closeButton") - self.gamePlate = QtWidgets.QGroupBox(self.main) - self.gamePlate.setGeometry(QtCore.QRect(30, 230, 321, 111)) - self.gamePlate.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.gamePlate.setTitle("") - self.gamePlate.setObjectName("gamePlate") - self.gameIcon = QtWidgets.QLabel(self.gamePlate) - self.gameIcon.setGeometry(QtCore.QRect(20, 30, 51, 51)) - self.gameIcon.setText("") - self.gameIcon.setObjectName("gameIcon") - self.gameName = QtWidgets.QLabel(self.gamePlate) - self.gameName.setGeometry(QtCore.QRect(80, 40, 231, 31)) - self.gameName.setText("") - self.gameName.setObjectName("gameName") - self.stackedWidget.addWidget(self.main) - self.settings = QtWidgets.QWidget() - self.settings.setObjectName("settings") - self.okButton = QtWidgets.QPushButton(self.settings) - self.okButton.setGeometry(QtCore.QRect(-1, 563, 601, 41)) - self.okButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.okButton.setObjectName("okButton") - self.showElapsedOff = QtWidgets.QPushButton(self.settings) - self.showElapsedOff.setGeometry(QtCore.QRect(30, 70, 270, 61)) - self.showElapsedOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.showElapsedOff.setObjectName("showElapsedOff") - self.showElapsedOn = QtWidgets.QPushButton(self.settings) - self.showElapsedOn.setGeometry(QtCore.QRect(300, 70, 270, 61)) - self.showElapsedOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.showElapsedOn.setObjectName("showElapsedOn") - self.showProfileButtonOff = QtWidgets.QPushButton(self.settings) - self.showProfileButtonOff.setGeometry(QtCore.QRect(30, 220, 270, 61)) - self.showProfileButtonOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.showProfileButtonOff.setObjectName("showProfileButtonOff") - self.showProfileButtonOn = QtWidgets.QPushButton(self.settings) - self.showProfileButtonOn.setGeometry(QtCore.QRect(300, 220, 270, 61)) - self.showProfileButtonOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.showProfileButtonOn.setObjectName("showProfileButtonOn") - self.showSmallImageOff = QtWidgets.QPushButton(self.settings) - self.showSmallImageOff.setGeometry(QtCore.QRect(30, 370, 270, 61)) - self.showSmallImageOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.showSmallImageOff.setObjectName("showSmallImageOff") - self.showSmallImageOn = QtWidgets.QPushButton(self.settings) - self.showSmallImageOn.setGeometry(QtCore.QRect(300, 370, 270, 61)) - self.showSmallImageOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.showSmallImageOn.setObjectName("showSmallImageOn") - self.showElapsedText = QtWidgets.QLabel(self.settings) - self.showElapsedText.setGeometry(QtCore.QRect(15, 20, 571, 31)) - self.showElapsedText.setAlignment(QtCore.Qt.AlignCenter) - self.showElapsedText.setObjectName("showElapsedText") - self.showProfileButtonText = QtWidgets.QLabel(self.settings) - self.showProfileButtonText.setGeometry(QtCore.QRect(15, 170, 571, 31)) - self.showProfileButtonText.setAlignment(QtCore.Qt.AlignCenter) - self.showProfileButtonText.setObjectName("showProfileButtonText") - self.showSmallImageText = QtWidgets.QLabel(self.settings) - self.showSmallImageText.setGeometry(QtCore.QRect(15, 320, 571, 31)) - self.showSmallImageText.setAlignment(QtCore.Qt.AlignCenter) - self.showSmallImageText.setObjectName("showSmallImageText") - self.showElapsed = QtWidgets.QGroupBox(self.settings) - self.showElapsed.setGeometry(QtCore.QRect(25, 65, 550, 71)) - self.showElapsed.setTitle("") - self.showElapsed.setObjectName("showElapsed") - self.okButtonLogout = QtWidgets.QPushButton(self.settings) - self.okButtonLogout.setGeometry(QtCore.QRect(165, 470, 270, 61)) - self.okButtonLogout.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.okButtonLogout.setObjectName("okButtonLogout") - self.showProfileButton = QtWidgets.QGroupBox(self.settings) - self.showProfileButton.setGeometry(QtCore.QRect(25, 215, 550, 71)) - self.showProfileButton.setTitle("") - self.showProfileButton.setObjectName("showProfileButton") - self.showSmallImage = QtWidgets.QGroupBox(self.settings) - self.showSmallImage.setGeometry(QtCore.QRect(25, 365, 550, 71)) - self.showSmallImage.setTitle("") - self.showSmallImage.setObjectName("showSmallImage") - self.showSmallImage.raise_() - self.showProfileButton.raise_() - self.showElapsed.raise_() - self.okButton.raise_() - self.showElapsedOff.raise_() - self.showElapsedOn.raise_() - self.showProfileButtonOff.raise_() - self.showProfileButtonOn.raise_() - self.showSmallImageOff.raise_() - self.showSmallImageOn.raise_() - self.showElapsedText.raise_() - self.showProfileButtonText.raise_() - self.showSmallImageText.raise_() - self.okButtonLogout.raise_() - self.stackedWidget.addWidget(self.settings) - - self.retranslateUi(MainWindow) - self.stackedWidget.setCurrentIndex(1) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "3DS Rich Presence")) - self.description.setText(_translate("MainWindow", "Add the bot\'s FC to your friend list before continuing to the next page.")) - self.continueButton.setText(_translate("MainWindow", "Continue")) - self.botFCLabel.setText(_translate("MainWindow", "XXXX-XXXX-XXXX")) - self.title.setText(_translate("MainWindow", "Link a Friend Code")) - self.description_2.setText(_translate("MainWindow", "
Please be aware that by using this product you are agreeing to the Terms and Services listed on the MCMi460/3DS-RPC Github page.
")) - self.description2.setText(_translate("MainWindow", "Make certain that you have added the bot account\'s FC to your friendlist before pressing \"Log In\".")) - self.loginButton.setText(_translate("MainWindow", "Log In")) - self.fcInput.setPlaceholderText(_translate("MainWindow", "Enter your friend code here!")) - self.title2.setText(_translate("MainWindow", "Link a Friend Code")) - self.description_3.setText(_translate("MainWindow", "Please be aware that by using this product you are agreeing to the Terms and Services listed on the MCMi460/3DS-RPC Github page.
")) - self.settingsButton.setText(_translate("MainWindow", "Settings")) - self.closeButton.setText(_translate("MainWindow", "X Close")) - self.okButton.setText(_translate("MainWindow", "OK")) - self.showElapsedOff.setText(_translate("MainWindow", "No")) - self.showElapsedOn.setText(_translate("MainWindow", "Yes")) - self.showProfileButtonOff.setText(_translate("MainWindow", "No")) - self.showProfileButtonOn.setText(_translate("MainWindow", "Yes")) - self.showSmallImageOff.setText(_translate("MainWindow", "No")) - self.showSmallImageOn.setText(_translate("MainWindow", "Yes")) - self.showElapsedText.setText(_translate("MainWindow", "Allow others to see your current time in-game?")) - self.showProfileButtonText.setText(_translate("MainWindow", "Allow others to click your status to view your profile?")) - self.showSmallImageText.setText(_translate("MainWindow", "Show a small image of your Mii to Discord?")) - self.okButtonLogout.setText(_translate("MainWindow", "Logout")) - - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - MainWindow = QtWidgets.QWidget() - ui = Ui_MainWindow() - ui.setupUi(MainWindow) - MainWindow.show() - sys.exit(app.exec_()) +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'mainwindow.ui' +# +# Created by: PyQt5 UI code generator 5.15.11 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(600, 600) + self.stackedWidget = QtWidgets.QStackedWidget(MainWindow) + self.stackedWidget.setGeometry(QtCore.QRect(0, 0, 601, 601)) + self.stackedWidget.setObjectName("stackedWidget") + self.bot = QtWidgets.QWidget() + self.bot.setObjectName("bot") + self.welcomeLogo = QtWidgets.QLabel(self.bot) + self.welcomeLogo.setGeometry(QtCore.QRect(190, 25, 220, 220)) + self.welcomeLogo.setText("") + self.welcomeLogo.setScaledContents(True) + self.welcomeLogo.setObjectName("welcomeLogo") + self.title = QtWidgets.QLabel(self.bot) + self.title.setGeometry(QtCore.QRect(100, 255, 400, 60)) + font = QtGui.QFont() + font.setPointSize(31) + self.title.setFont(font) + self.title.setAlignment(QtCore.Qt.AlignCenter) + self.title.setObjectName("title") + self.description = QtWidgets.QLabel(self.bot) + self.description.setGeometry(QtCore.QRect(100, 315, 400, 75)) + self.description.setAlignment(QtCore.Qt.AlignCenter) + self.description.setWordWrap(True) + self.description.setObjectName("description") + self.continueButton = QtWidgets.QPushButton(self.bot) + self.continueButton.setGeometry(QtCore.QRect(180, 410, 240, 45)) + self.continueButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.continueButton.setObjectName("continueButton") + self.description_2 = QtWidgets.QLabel(self.bot) + self.description_2.setGeometry(QtCore.QRect(110, 500, 380, 80)) + self.description_2.setAlignment(QtCore.Qt.AlignCenter) + self.description_2.setWordWrap(True) + self.description_2.setOpenExternalLinks(True) + self.description_2.setObjectName("description_2") + self.stackedWidget.addWidget(self.bot) + self.endpoint = QtWidgets.QWidget() + self.endpoint.setObjectName("endpoint") + self.endpointTitle = QtWidgets.QLabel(self.endpoint) + self.endpointTitle.setGeometry(QtCore.QRect(100, 50, 400, 60)) + font = QtGui.QFont() + font.setPointSize(31) + self.endpointTitle.setFont(font) + self.endpointTitle.setAlignment(QtCore.Qt.AlignCenter) + self.endpointTitle.setObjectName("endpointTitle") + self.endpointDescription = QtWidgets.QLabel(self.endpoint) + self.endpointDescription.setGeometry(QtCore.QRect(100, 115, 400, 70)) + self.endpointDescription.setAlignment(QtCore.Qt.AlignCenter) + self.endpointDescription.setWordWrap(True) + self.endpointDescription.setObjectName("endpointDescription") + self.section3 = QtWidgets.QGroupBox(self.endpoint) + self.section3.setGeometry(QtCore.QRect(80, 210, 440, 220)) + self.section3.setTitle("") + self.section3.setObjectName("section3") + self.endpointInput = QtWidgets.QComboBox(self.section3) + self.endpointInput.setGeometry(QtCore.QRect(130, 50, 270, 30)) + self.endpointInput.setObjectName("endpointInput") + self.endpointInput.addItem("") + self.endpointInput.addItem("") + self.endpointLabel = QtWidgets.QLabel(self.section3) + self.endpointLabel.setGeometry(QtCore.QRect(30, 50, 90, 30)) + self.endpointLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter) + self.endpointLabel.setObjectName("endpointLabel") + self.customEndpointInput = QtWidgets.QLineEdit(self.section3) + self.customEndpointInput.setGeometry(QtCore.QRect(130, 100, 270, 30)) + self.customEndpointInput.setObjectName("customEndpointInput") + self.endpointHint = QtWidgets.QLabel(self.section3) + self.endpointHint.setGeometry(QtCore.QRect(130, 133, 270, 22)) + self.endpointHint.setText("") + self.endpointHint.setAlignment(QtCore.Qt.AlignCenter) + self.endpointHint.setObjectName("endpointHint") + self.saveEndpointButton = QtWidgets.QPushButton(self.section3) + self.saveEndpointButton.setGeometry(QtCore.QRect(230, 160, 150, 40)) + self.saveEndpointButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.saveEndpointButton.setObjectName("saveEndpointButton") + self.backEndpointButton = QtWidgets.QPushButton(self.section3) + self.backEndpointButton.setGeometry(QtCore.QRect(60, 160, 150, 40)) + self.backEndpointButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.backEndpointButton.setObjectName("backEndpointButton") + self.stackedWidget.addWidget(self.endpoint) + self.link = QtWidgets.QWidget() + self.link.setObjectName("link") + self.title2 = QtWidgets.QLabel(self.link) + self.title2.setGeometry(QtCore.QRect(100, 70, 400, 60)) + font = QtGui.QFont() + font.setPointSize(31) + self.title2.setFont(font) + self.title2.setAlignment(QtCore.Qt.AlignCenter) + self.title2.setObjectName("title2") + self.description2 = QtWidgets.QLabel(self.link) + self.description2.setGeometry(QtCore.QRect(100, 140, 400, 60)) + self.description2.setAlignment(QtCore.Qt.AlignCenter) + self.description2.setWordWrap(True) + self.description2.setObjectName("description2") + self.section2 = QtWidgets.QGroupBox(self.link) + self.section2.setGeometry(QtCore.QRect(80, 220, 440, 210)) + self.section2.setTitle("") + self.section2.setObjectName("section2") + self.keyLabel = QtWidgets.QLabel(self.section2) + self.keyLabel.setGeometry(QtCore.QRect(30, 85, 90, 30)) + self.keyLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter) + self.keyLabel.setObjectName("keyLabel") + self.keyInput = QtWidgets.QLineEdit(self.section2) + self.keyInput.setGeometry(QtCore.QRect(130, 85, 270, 30)) + self.keyInput.setObjectName("keyInput") + self.loginButton = QtWidgets.QPushButton(self.section2) + self.loginButton.setGeometry(QtCore.QRect(140, 150, 220, 40)) + self.loginButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.loginButton.setObjectName("loginButton") + self.description_3 = QtWidgets.QLabel(self.link) + self.description_3.setGeometry(QtCore.QRect(110, 490, 380, 80)) + self.description_3.setAlignment(QtCore.Qt.AlignCenter) + self.description_3.setWordWrap(True) + self.description_3.setOpenExternalLinks(True) + self.description_3.setObjectName("description_3") + self.stackedWidget.addWidget(self.link) + self.loading = QtWidgets.QWidget() + self.loading.setObjectName("loading") + self.loadingTitle = QtWidgets.QLabel(self.loading) + self.loadingTitle.setGeometry(QtCore.QRect(100, 250, 400, 60)) + font = QtGui.QFont() + font.setPointSize(31) + self.loadingTitle.setFont(font) + self.loadingTitle.setAlignment(QtCore.Qt.AlignCenter) + self.loadingTitle.setObjectName("loadingTitle") + self.loadingText = QtWidgets.QLabel(self.loading) + self.loadingText.setGeometry(QtCore.QRect(100, 320, 400, 40)) + self.loadingText.setAlignment(QtCore.Qt.AlignCenter) + self.loadingText.setObjectName("loadingText") + self.stackedWidget.addWidget(self.loading) + self.main = QtWidgets.QWidget() + self.main.setObjectName("main") + self.settingsButton = QtWidgets.QPushButton(self.main) + self.settingsButton.setGeometry(QtCore.QRect(0, -20, 601, 61)) + self.settingsButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.settingsButton.setObjectName("settingsButton") + self.refreshButton = QtWidgets.QPushButton(self.main) + self.refreshButton.setGeometry(QtCore.QRect(215, 65, 170, 40)) + self.refreshButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.refreshButton.setObjectName("refreshButton") + self.friendCard = QtWidgets.QGroupBox(self.main) + self.friendCard.setGeometry(QtCore.QRect(80, 115, 451, 231)) + self.friendCard.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.friendCard.setTitle("") + self.friendCard.setObjectName("friendCard") + self.namePlate = QtWidgets.QGroupBox(self.friendCard) + self.namePlate.setGeometry(QtCore.QRect(0, 170, 451, 81)) + self.namePlate.setTitle("") + self.namePlate.setObjectName("namePlate") + self.username = QtWidgets.QLabel(self.namePlate) + self.username.setGeometry(QtCore.QRect(280, 10, 151, 41)) + self.username.setText("") + self.username.setAlignment(QtCore.Qt.AlignCenter) + self.username.setObjectName("username") + self.status = QtWidgets.QLabel(self.friendCard) + self.status.setGeometry(QtCore.QRect(10, 10, 101, 21)) + self.status.setText("") + self.status.setObjectName("status") + self.miiLabel = QtWidgets.QLabel(self.friendCard) + self.miiLabel.setGeometry(QtCore.QRect(280, 20, 151, 151)) + self.miiLabel.setText("") + self.miiLabel.setObjectName("miiLabel") + self.closeButton = QtWidgets.QPushButton(self.main) + self.closeButton.setGeometry(QtCore.QRect(70, 570, 461, 61)) + self.closeButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.closeButton.setObjectName("closeButton") + self.gamePlate = QtWidgets.QGroupBox(self.main) + self.gamePlate.setGeometry(QtCore.QRect(30, 155, 321, 111)) + self.gamePlate.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.gamePlate.setVisible(False) + self.gamePlate.setTitle("") + self.gamePlate.setObjectName("gamePlate") + self.gameIcon = QtWidgets.QLabel(self.gamePlate) + self.gameIcon.setGeometry(QtCore.QRect(20, 30, 51, 51)) + self.gameIcon.setText("") + self.gameIcon.setObjectName("gameIcon") + self.gameName = QtWidgets.QLabel(self.gamePlate) + self.gameName.setGeometry(QtCore.QRect(80, 40, 231, 31)) + self.gameName.setText("") + self.gameName.setObjectName("gameName") + self.detailsCard = QtWidgets.QGroupBox(self.main) + self.detailsCard.setGeometry(QtCore.QRect(80, 375, 451, 135)) + self.detailsCard.setTitle("") + self.detailsCard.setObjectName("detailsCard") + self.networkDetail = QtWidgets.QLabel(self.detailsCard) + self.networkDetail.setGeometry(QtCore.QRect(20, 44, 411, 24)) + self.networkDetail.setText("") + self.networkDetail.setAlignment(QtCore.Qt.AlignCenter) + self.networkDetail.setObjectName("networkDetail") + self.endpointDetail = QtWidgets.QLabel(self.detailsCard) + self.endpointDetail.setGeometry(QtCore.QRect(20, 18, 411, 24)) + self.endpointDetail.setText("") + self.endpointDetail.setAlignment(QtCore.Qt.AlignCenter) + self.endpointDetail.setObjectName("endpointDetail") + self.lastActiveDetail = QtWidgets.QLabel(self.detailsCard) + self.lastActiveDetail.setGeometry(QtCore.QRect(20, 70, 411, 24)) + self.lastActiveDetail.setText("") + self.lastActiveDetail.setAlignment(QtCore.Qt.AlignCenter) + self.lastActiveDetail.setObjectName("lastActiveDetail") + self.lastOnlineDetail = QtWidgets.QLabel(self.detailsCard) + self.lastOnlineDetail.setGeometry(QtCore.QRect(20, 96, 411, 24)) + self.lastOnlineDetail.setText("") + self.lastOnlineDetail.setAlignment(QtCore.Qt.AlignCenter) + self.lastOnlineDetail.setObjectName("lastOnlineDetail") + self.versionLabel = QtWidgets.QLabel(self.main) + self.versionLabel.setGeometry(QtCore.QRect(100, 528, 400, 25)) + self.versionLabel.setText("") + self.versionLabel.setAlignment(QtCore.Qt.AlignCenter) + self.versionLabel.setObjectName("versionLabel") + self.stackedWidget.addWidget(self.main) + self.settings = QtWidgets.QWidget() + self.settings.setObjectName("settings") + self.okButton = QtWidgets.QPushButton(self.settings) + self.okButton.setGeometry(QtCore.QRect(-1, 563, 601, 41)) + self.okButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.okButton.setObjectName("okButton") + self.showElapsedOff = QtWidgets.QPushButton(self.settings) + self.showElapsedOff.setGeometry(QtCore.QRect(30, 70, 270, 61)) + self.showElapsedOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.showElapsedOff.setObjectName("showElapsedOff") + self.showElapsedOn = QtWidgets.QPushButton(self.settings) + self.showElapsedOn.setGeometry(QtCore.QRect(300, 70, 270, 61)) + self.showElapsedOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.showElapsedOn.setObjectName("showElapsedOn") + self.showProfileButtonOff = QtWidgets.QPushButton(self.settings) + self.showProfileButtonOff.setGeometry(QtCore.QRect(30, 220, 270, 61)) + self.showProfileButtonOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.showProfileButtonOff.setObjectName("showProfileButtonOff") + self.showProfileButtonOn = QtWidgets.QPushButton(self.settings) + self.showProfileButtonOn.setGeometry(QtCore.QRect(300, 220, 270, 61)) + self.showProfileButtonOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.showProfileButtonOn.setObjectName("showProfileButtonOn") + self.showSmallImageOff = QtWidgets.QPushButton(self.settings) + self.showSmallImageOff.setGeometry(QtCore.QRect(30, 370, 270, 61)) + self.showSmallImageOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.showSmallImageOff.setObjectName("showSmallImageOff") + self.showSmallImageOn = QtWidgets.QPushButton(self.settings) + self.showSmallImageOn.setGeometry(QtCore.QRect(300, 370, 270, 61)) + self.showSmallImageOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.showSmallImageOn.setObjectName("showSmallImageOn") + self.showElapsedText = QtWidgets.QLabel(self.settings) + self.showElapsedText.setGeometry(QtCore.QRect(15, 20, 571, 31)) + self.showElapsedText.setAlignment(QtCore.Qt.AlignCenter) + self.showElapsedText.setObjectName("showElapsedText") + self.showProfileButtonText = QtWidgets.QLabel(self.settings) + self.showProfileButtonText.setGeometry(QtCore.QRect(15, 170, 571, 31)) + self.showProfileButtonText.setAlignment(QtCore.Qt.AlignCenter) + self.showProfileButtonText.setObjectName("showProfileButtonText") + self.showSmallImageText = QtWidgets.QLabel(self.settings) + self.showSmallImageText.setGeometry(QtCore.QRect(15, 320, 571, 31)) + self.showSmallImageText.setAlignment(QtCore.Qt.AlignCenter) + self.showSmallImageText.setObjectName("showSmallImageText") + self.showElapsed = QtWidgets.QGroupBox(self.settings) + self.showElapsed.setGeometry(QtCore.QRect(25, 65, 550, 71)) + self.showElapsed.setTitle("") + self.showElapsed.setObjectName("showElapsed") + self.okButtonLogout = QtWidgets.QPushButton(self.settings) + self.okButtonLogout.setGeometry(QtCore.QRect(165, 470, 270, 61)) + self.okButtonLogout.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.okButtonLogout.setObjectName("okButtonLogout") + self.showProfileButton = QtWidgets.QGroupBox(self.settings) + self.showProfileButton.setGeometry(QtCore.QRect(25, 215, 550, 71)) + self.showProfileButton.setTitle("") + self.showProfileButton.setObjectName("showProfileButton") + self.showSmallImage = QtWidgets.QGroupBox(self.settings) + self.showSmallImage.setGeometry(QtCore.QRect(25, 365, 550, 71)) + self.showSmallImage.setTitle("") + self.showSmallImage.setObjectName("showSmallImage") + self.endpointButton = QtWidgets.QPushButton(self.settings) + self.endpointButton.setGeometry(QtCore.QRect(165, 531, 270, 27)) + self.endpointButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.endpointButton.setObjectName("endpointButton") + self.showSmallImage.raise_() + self.showProfileButton.raise_() + self.showElapsed.raise_() + self.okButton.raise_() + self.showElapsedOff.raise_() + self.showElapsedOn.raise_() + self.showProfileButtonOff.raise_() + self.showProfileButtonOn.raise_() + self.showSmallImageOff.raise_() + self.showSmallImageOn.raise_() + self.showElapsedText.raise_() + self.showProfileButtonText.raise_() + self.showSmallImageText.raise_() + self.okButtonLogout.raise_() + self.endpointButton.raise_() + self.stackedWidget.addWidget(self.settings) + + self.retranslateUi(MainWindow) + self.stackedWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "3DS Rich Presence")) + self.title.setText(_translate("MainWindow", "Welcome")) + self.description.setText(_translate("MainWindow", "Choose your endpoint and enter your API key to get started.")) + self.continueButton.setText(_translate("MainWindow", "Continue")) + self.description_2.setText(_translate("MainWindow", "Please be aware that by using this product you are agreeing to the Terms and Services listed on the MCMi460/3DS-RPC Github page.
")) + self.endpointTitle.setText(_translate("MainWindow", "Endpoint")) + self.endpointDescription.setText(_translate("MainWindow", "Choose which server to connect to. Official is hosted; custom lets you point the app at any compatible server.")) + self.endpointInput.setItemText(0, _translate("MainWindow", "official")) + self.endpointInput.setItemText(1, _translate("MainWindow", "custom")) + self.endpointLabel.setText(_translate("MainWindow", "Endpoint")) + self.customEndpointInput.setPlaceholderText(_translate("MainWindow", "https://your-server.example.com")) + self.saveEndpointButton.setText(_translate("MainWindow", "Save")) + self.backEndpointButton.setText(_translate("MainWindow", "Back")) + self.title2.setText(_translate("MainWindow", "Enter your API Key")) + self.description2.setText(_translate("MainWindow", "You can find your API key on the Settings page of your endpoint.")) + self.keyLabel.setText(_translate("MainWindow", "API Key")) + self.keyInput.setPlaceholderText(_translate("MainWindow", "Enter your API key from your endpoint\'s Settings page!")) + self.loginButton.setText(_translate("MainWindow", "Log In")) + self.description_3.setText(_translate("MainWindow", "Please be aware that by using this product you are agreeing to the Terms and Services listed on the MCMi460/3DS-RPC Github page.
")) + self.loadingTitle.setText(_translate("MainWindow", "Fetching profile...")) + self.loadingText.setText(_translate("MainWindow", "Retrieving your latest activity from the server.")) + self.settingsButton.setText(_translate("MainWindow", "Settings")) + self.refreshButton.setText(_translate("MainWindow", "Refresh")) + self.closeButton.setText(_translate("MainWindow", "X Close")) + self.versionLabel.setStyleSheet(_translate("MainWindow", "color: #9E9E9E; font-size: 11px;")) + self.okButton.setText(_translate("MainWindow", "OK")) + self.showElapsedOff.setText(_translate("MainWindow", "No")) + self.showElapsedOn.setText(_translate("MainWindow", "Yes")) + self.showProfileButtonOff.setText(_translate("MainWindow", "No")) + self.showProfileButtonOn.setText(_translate("MainWindow", "Yes")) + self.showSmallImageOff.setText(_translate("MainWindow", "No")) + self.showSmallImageOn.setText(_translate("MainWindow", "Yes")) + self.showElapsedText.setText(_translate("MainWindow", "Allow others to see your current time in-game?")) + self.showProfileButtonText.setText(_translate("MainWindow", "Allow others to click your status to view your profile?")) + self.showSmallImageText.setText(_translate("MainWindow", "Show a small image of your Mii to Discord?")) + self.okButtonLogout.setText(_translate("MainWindow", "Logout")) + self.endpointButton.setText(_translate("MainWindow", "Endpoint")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QWidget() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + MainWindow.show() + sys.exit(app.exec_()) diff --git a/layout/mainwindow.ui b/layout/mainwindow.ui index 32476d3..25a0dfe 100644 --- a/layout/mainwindow.ui +++ b/layout/mainwindow.ui @@ -23,91 +23,32 @@