mirror of
				https://github.com/nixietab/picodulce.git
				synced 2025-10-31 05:25:12 +00:00 
			
		
		
		
	Compare commits
	
		
			No commits in common. "9523f1ab0ca4c379e909092d714cd5ce82c1169e" and "32e47832187bff94fad59564ff81240c02c22b45" have entirely different histories.
		
	
	
		
			9523f1ab0c
			...
			32e4783218
		
	
		
							
								
								
									
										315
									
								
								authser.py
									
									
									
									
									
								
							
							
						
						
									
										315
									
								
								authser.py
									
									
									
									
									
								
							| @ -1,26 +1,16 @@ | |||||||
| import sys | import sys | ||||||
| import json | import re | ||||||
| import os | import colorama | ||||||
| import uuid | import requests | ||||||
| import asyncio |  | ||||||
| import aiohttp |  | ||||||
| from datetime import datetime, timezone |  | ||||||
| from pathlib import Path |  | ||||||
| from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,  | from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,  | ||||||
|                           QPushButton, QLineEdit, QMessageBox) |                           QPushButton, QLineEdit, QMessageBox) | ||||||
| from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject | from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject, QTimer | ||||||
| from PyQt5.QtGui import QDesktopServices | from PyQt5.QtGui import QDesktopServices | ||||||
| from picomc.logging import logger | from picomc.logging import logger | ||||||
| from picomc.launcher import get_default_root, Launcher |  | ||||||
| 
 | 
 | ||||||
| # Constants for Microsoft Authentication | # Constants | ||||||
| URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode" | URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode" | ||||||
| URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" | URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" | ||||||
| URL_XBL = "https://user.auth.xboxlive.com/user/authenticate" |  | ||||||
| URL_XSTS = "https://xsts.auth.xboxlive.com/xsts/authorize" |  | ||||||
| URL_MC = "https://api.minecraftservices.com/authentication/login_with_xbox" |  | ||||||
| URL_PROFILE = "https://api.minecraftservices.com/minecraft/profile" |  | ||||||
| 
 |  | ||||||
| CLIENT_ID = "c52aed44-3b4d-4215-99c5-824033d2bc0f" | CLIENT_ID = "c52aed44-3b4d-4215-99c5-824033d2bc0f" | ||||||
| SCOPE = "XboxLive.signin offline_access" | SCOPE = "XboxLive.signin offline_access" | ||||||
| GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code" | GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code" | ||||||
| @ -92,133 +82,76 @@ class AuthenticationThread(QThread): | |||||||
|     error_occurred = pyqtSignal(str) |     error_occurred = pyqtSignal(str) | ||||||
|     auth_error_detected = pyqtSignal(str) |     auth_error_detected = pyqtSignal(str) | ||||||
|     finished = pyqtSignal() |     finished = pyqtSignal() | ||||||
|     access_token_received = pyqtSignal(dict) |     access_token_received = pyqtSignal(str, str) | ||||||
|      |      | ||||||
|     def __init__(self, username): |     def __init__(self, account): | ||||||
|         super().__init__() |         super().__init__() | ||||||
|         self.username = username |         self.account = account | ||||||
|         self.device_code = None |         self.device_code = None | ||||||
|         self.is_running = True |         self.is_running = True | ||||||
| 
 | 
 | ||||||
|     async def _ms_oauth(self): |  | ||||||
|         data = {"client_id": CLIENT_ID, "scope": SCOPE} |  | ||||||
|          |  | ||||||
|         async with aiohttp.ClientSession() as session: |  | ||||||
|             async with session.post(URL_DEVICE_AUTH, data=data) as resp: |  | ||||||
|                 if resp.status != 200: |  | ||||||
|                     raise Exception(f"Failed to get device code: {await resp.text()}") |  | ||||||
|                 j = await resp.json() |  | ||||||
|                 self.device_code = j["device_code"] |  | ||||||
|                 self.auth_data_received.emit({ |  | ||||||
|                     'url': j["verification_uri"], |  | ||||||
|                     'code': j["user_code"] |  | ||||||
|                 }) |  | ||||||
| 
 |  | ||||||
|             while self.is_running: |  | ||||||
|                 data = { |  | ||||||
|                     "grant_type": GRANT_TYPE, |  | ||||||
|                     "client_id": CLIENT_ID, |  | ||||||
|                     "device_code": self.device_code |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 async with session.post(URL_TOKEN, data=data) as resp: |  | ||||||
|                     j = await resp.json() |  | ||||||
|                     if resp.status == 400: |  | ||||||
|                         if j["error"] == "authorization_pending": |  | ||||||
|                             await asyncio.sleep(2) |  | ||||||
|                             continue |  | ||||||
|                         else: |  | ||||||
|                             raise Exception(j["error_description"]) |  | ||||||
|                     elif resp.status != 200: |  | ||||||
|                         raise Exception(f"Token request failed: {j}") |  | ||||||
| 
 |  | ||||||
|                     return j["access_token"], j["refresh_token"] |  | ||||||
| 
 |  | ||||||
|     async def _xbl_auth(self, access_token): |  | ||||||
|         data = { |  | ||||||
|             "Properties": { |  | ||||||
|                 "AuthMethod": "RPS", |  | ||||||
|                 "SiteName": "user.auth.xboxlive.com", |  | ||||||
|                 "RpsTicket": f"d={access_token}" |  | ||||||
|             }, |  | ||||||
|             "RelyingParty": "http://auth.xboxlive.com", |  | ||||||
|             "TokenType": "JWT" |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         async with aiohttp.ClientSession() as session: |  | ||||||
|             async with session.post(URL_XBL, json=data) as resp: |  | ||||||
|                 if resp.status != 200: |  | ||||||
|                     raise Exception(f"XBL auth failed: {await resp.text()}") |  | ||||||
|                 j = await resp.json() |  | ||||||
|                 return j["Token"], j["DisplayClaims"]["xui"][0]["uhs"] |  | ||||||
| 
 |  | ||||||
|     async def _xsts_auth(self, xbl_token): |  | ||||||
|         data = { |  | ||||||
|             "Properties": { |  | ||||||
|                 "SandboxId": "RETAIL", |  | ||||||
|                 "UserTokens": [xbl_token] |  | ||||||
|             }, |  | ||||||
|             "RelyingParty": "rp://api.minecraftservices.com/", |  | ||||||
|             "TokenType": "JWT" |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         async with aiohttp.ClientSession() as session: |  | ||||||
|             async with session.post(URL_XSTS, json=data) as resp: |  | ||||||
|                 if resp.status != 200: |  | ||||||
|                     raise Exception(f"XSTS auth failed: {await resp.text()}") |  | ||||||
|                 j = await resp.json() |  | ||||||
|                 return j["Token"] |  | ||||||
| 
 |  | ||||||
|     async def _mc_auth(self, uhs, xsts_token): |  | ||||||
|         data = { |  | ||||||
|             "identityToken": f"XBL3.0 x={uhs};{xsts_token}" |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         async with aiohttp.ClientSession() as session: |  | ||||||
|             async with session.post(URL_MC, json=data) as resp: |  | ||||||
|                 if resp.status != 200: |  | ||||||
|                     raise Exception(f"MC auth failed: {await resp.text()}") |  | ||||||
|                 j = await resp.json() |  | ||||||
|                 return j["access_token"] |  | ||||||
| 
 |  | ||||||
|     async def _get_profile(self, mc_token): |  | ||||||
|         headers = { |  | ||||||
|             "Authorization": f"Bearer {mc_token}" |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         async with aiohttp.ClientSession() as session: |  | ||||||
|             async with session.get(URL_PROFILE, headers=headers) as resp: |  | ||||||
|                 if resp.status != 200: |  | ||||||
|                     raise Exception(f"Profile request failed: {await resp.text()}") |  | ||||||
|                 return await resp.json() |  | ||||||
| 
 |  | ||||||
|     async def _auth_flow(self): |  | ||||||
|         try: |  | ||||||
|             ms_access_token, refresh_token = await self._ms_oauth() |  | ||||||
|             xbl_token, uhs = await self._xbl_auth(ms_access_token) |  | ||||||
|             xsts_token = await self._xsts_auth(xbl_token) |  | ||||||
|             mc_token = await self._mc_auth(uhs, xsts_token) |  | ||||||
|             profile = await self._get_profile(mc_token) |  | ||||||
| 
 |  | ||||||
|             self.access_token_received.emit({ |  | ||||||
|                 'access_token': mc_token, |  | ||||||
|                 'refresh_token': refresh_token, |  | ||||||
|                 'profile': profile |  | ||||||
|             }) |  | ||||||
| 
 |  | ||||||
|         except Exception as e: |  | ||||||
|             self.error_occurred.emit(str(e)) |  | ||||||
| 
 |  | ||||||
|     def run(self): |     def run(self): | ||||||
|         try: |         try: | ||||||
|             loop = asyncio.new_event_loop() |             self.authenticate(self.account) | ||||||
|             asyncio.set_event_loop(loop) |  | ||||||
|             loop.run_until_complete(self._auth_flow()) |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             self.error_occurred.emit(str(e)) |             self.error_occurred.emit(str(e)) | ||||||
|         finally: |  | ||||||
|             self.finished.emit() |             self.finished.emit() | ||||||
| 
 | 
 | ||||||
|  |     def authenticate(self, account): | ||||||
|  |         try: | ||||||
|  |             data = {"client_id": CLIENT_ID, "scope": SCOPE} | ||||||
|  | 
 | ||||||
|  |             # Request device code | ||||||
|  |             resp = requests.post(URL_DEVICE_AUTH, data) | ||||||
|  |             resp.raise_for_status() | ||||||
|  | 
 | ||||||
|  |             j = resp.json() | ||||||
|  |             self.device_code = j["device_code"] | ||||||
|  |             user_code = j["user_code"] | ||||||
|  |             link = j["verification_uri"] | ||||||
|  | 
 | ||||||
|  |             # Format message with colorama | ||||||
|  |             msg = j["message"] | ||||||
|  |             msg = msg.replace( | ||||||
|  |                 user_code, colorama.Fore.RED + user_code + colorama.Fore.RESET | ||||||
|  |             ).replace(link, colorama.Style.BRIGHT + link + colorama.Style.NORMAL) | ||||||
|  | 
 | ||||||
|  |             # Emit auth data received signal | ||||||
|  |             self.auth_data_received.emit({'url': link, 'code': user_code}) | ||||||
|  | 
 | ||||||
|  |         except requests.exceptions.RequestException as e: | ||||||
|  |             logger.error(f"Request failed: {e}") | ||||||
|  |             self.error_occurred.emit(str(e)) | ||||||
|  |             self.finished.emit() | ||||||
|  |      | ||||||
|  |     def poll_for_token(self): | ||||||
|  |         try: | ||||||
|  |             data = {"code": self.device_code, "grant_type": GRANT_TYPE, "client_id": CLIENT_ID} | ||||||
|  |             resp = requests.post(URL_TOKEN, data) | ||||||
|  |             if resp.status_code == 400: | ||||||
|  |                 j = resp.json() | ||||||
|  |                 logger.debug(j) | ||||||
|  |                 if j["error"] == "authorization_pending": | ||||||
|  |                     logger.warning(j["error_description"]) | ||||||
|  |                     self.auth_error_detected.emit(j["error_description"]) | ||||||
|  |                     return | ||||||
|  |                 else: | ||||||
|  |                     raise Exception(j["error_description"]) | ||||||
|  |             resp.raise_for_status() | ||||||
|  |             j = resp.json() | ||||||
|  |             access_token = j["access_token"] | ||||||
|  |             refresh_token = j["refresh_token"] | ||||||
|  |             logger.debug("OAuth device code flow successful") | ||||||
|  |             self.access_token_received.emit(access_token, refresh_token) | ||||||
|  |             self.finished.emit() | ||||||
|  |         except requests.exceptions.RequestException as e: | ||||||
|  |             logger.error(f"Request failed: {e}") | ||||||
|  |             self.error_occurred.emit(str(e)) | ||||||
|  |             self.finished.emit() | ||||||
|  |      | ||||||
|  |     def send_enter(self): | ||||||
|  |         self.poll_for_token() | ||||||
|  | 
 | ||||||
|     def stop(self): |     def stop(self): | ||||||
|         self.is_running = False |         self.is_running = False | ||||||
| 
 | 
 | ||||||
| @ -228,117 +161,54 @@ class MinecraftAuthenticator(QObject): | |||||||
|     def __init__(self, parent=None): |     def __init__(self, parent=None): | ||||||
|         super().__init__(parent) |         super().__init__(parent) | ||||||
|         self.auth_thread = None |         self.auth_thread = None | ||||||
|  |         self.current_auth_data = None | ||||||
|         self.auth_dialog = None |         self.auth_dialog = None | ||||||
|         self.success = False |         self.success = False | ||||||
|         self.username = None |  | ||||||
|          |  | ||||||
|         # Initialize the launcher to get the correct config path |  | ||||||
|         with Launcher.new() as launcher: |  | ||||||
|             self.config_path = launcher.root |  | ||||||
|          |          | ||||||
|     def authenticate(self, username): |     def authenticate(self, username): | ||||||
|         self.username = username |  | ||||||
|         self.success = False |         self.success = False | ||||||
| 
 |  | ||||||
|         # Create accounts.json if it doesn't exist |  | ||||||
|         if not self.save_to_accounts_json(): |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         self.auth_thread = AuthenticationThread(username) |         self.auth_thread = AuthenticationThread(username) | ||||||
|         self.auth_thread.auth_data_received.connect(self.show_auth_dialog) |         self.auth_thread.auth_data_received.connect(self.show_auth_dialog) | ||||||
|  |         self.auth_thread.auth_error_detected.connect(self.handle_auth_error) | ||||||
|         self.auth_thread.error_occurred.connect(self.show_error) |         self.auth_thread.error_occurred.connect(self.show_error) | ||||||
|         self.auth_thread.access_token_received.connect(self.on_access_token_received) |         self.auth_thread.access_token_received.connect(self.on_access_token_received) | ||||||
|         self.auth_thread.finished.connect(self.on_authentication_finished) |         self.auth_thread.finished.connect(self.on_authentication_finished) | ||||||
|         self.auth_thread.start() |         self.auth_thread.start() | ||||||
| 
 | 
 | ||||||
|     def show_auth_dialog(self, auth_data): |     def show_auth_dialog(self, auth_data): | ||||||
|  |         self.current_auth_data = auth_data | ||||||
|  |          | ||||||
|         if self.auth_dialog is not None: |         if self.auth_dialog is not None: | ||||||
|             self.auth_dialog.close() |             self.auth_dialog.close() | ||||||
|  |             self.auth_dialog = None | ||||||
|          |          | ||||||
|         self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code']) |         self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code']) | ||||||
|  |         if self.auth_dialog.exec_() == QDialog.Accepted: | ||||||
|  |             self.auth_thread.send_enter() | ||||||
| 
 | 
 | ||||||
|         result = self.auth_dialog.exec_() |     def handle_auth_error(self, output): | ||||||
|  |         if self.current_auth_data: | ||||||
|  |             if self.auth_dialog is not None: | ||||||
|  |                 self.auth_dialog.close() | ||||||
|  |                 self.auth_dialog = None | ||||||
|              |              | ||||||
|         if result != QDialog.Accepted: |             self.auth_dialog = AuthDialog( | ||||||
|             self.auth_thread.stop() |                 self.current_auth_data['url'], | ||||||
|  |                 self.current_auth_data['code'], | ||||||
|  |                 error_mode=True | ||||||
|  |             ) | ||||||
|  |             if self.auth_dialog.exec_() == QDialog.Accepted: | ||||||
|  |                 self.auth_thread.send_enter() | ||||||
| 
 | 
 | ||||||
|     def show_error(self, error_msg): |     def show_error(self, error_message): | ||||||
|         QMessageBox.critical(None, "Error", error_msg) |         QMessageBox.critical(None, "Error", f"Authentication error: {error_message}") | ||||||
|         self.success = False |         self.success = False | ||||||
|         self.auth_finished.emit(False) |         self.auth_finished.emit(False) | ||||||
| 
 | 
 | ||||||
|     def save_to_accounts_json(self): |     def on_access_token_received(self, access_token, refresh_token): | ||||||
|         try: |         QMessageBox.information(None, "Success", "Authentication successful!") | ||||||
|             accounts_file = Path(self.config_path) / "accounts.json" |  | ||||||
|              |  | ||||||
|             if accounts_file.exists(): |  | ||||||
|                 with open(accounts_file) as f: |  | ||||||
|                     config = json.load(f) |  | ||||||
|             else: |  | ||||||
|                 config = { |  | ||||||
|                     "default": None, |  | ||||||
|                     "accounts": {}, |  | ||||||
|                     "client_token": str(uuid.uuid4()) |  | ||||||
|                 } |  | ||||||
|                 accounts_file.parent.mkdir(parents=True, exist_ok=True) |  | ||||||
|              |  | ||||||
|             # Only create/update if account doesn't exist |  | ||||||
|             if self.username not in config["accounts"]: |  | ||||||
|                 config["accounts"][self.username] = { |  | ||||||
|                     "uuid": "-", |  | ||||||
|                     "online": True, |  | ||||||
|                     "microsoft": True, |  | ||||||
|                     "gname": "-", |  | ||||||
|                     "access_token": "-", |  | ||||||
|                     "refresh_token": "-", |  | ||||||
|                     "is_authenticated": False |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 # Set as default if no default exists |  | ||||||
|                 if config["default"] is None: |  | ||||||
|                     config["default"] = self.username |  | ||||||
|                  |  | ||||||
|                 with open(accounts_file, 'w') as f: |  | ||||||
|                     json.dump(config, f, indent=4) |  | ||||||
|                      |  | ||||||
|             return True |  | ||||||
| 
 |  | ||||||
|         except Exception as e: |  | ||||||
|             logger.error(f"Failed to initialize account data: {str(e)}") |  | ||||||
|             QMessageBox.critical(None, "Error", f"Failed to initialize account data: {str(e)}") |  | ||||||
|             return False |  | ||||||
| 
 |  | ||||||
|     def on_access_token_received(self, data): |  | ||||||
|         try: |  | ||||||
|             accounts_file = Path(self.config_path) / "accounts.json" |  | ||||||
|              |  | ||||||
|             with open(accounts_file) as f: |  | ||||||
|                 config = json.load(f) |  | ||||||
|              |  | ||||||
|             if self.username in config["accounts"]: |  | ||||||
|                 config["accounts"][self.username].update({ |  | ||||||
|                     "access_token": data['access_token'], |  | ||||||
|                     "refresh_token": data['refresh_token'], |  | ||||||
|                     "uuid": data['profile']['id'], |  | ||||||
|                     "gname": data['profile']['name'], |  | ||||||
|                     "is_authenticated": True |  | ||||||
|                 }) |  | ||||||
|                  |  | ||||||
|                 with open(accounts_file, 'w') as f: |  | ||||||
|                     json.dump(config, f, indent=4) |  | ||||||
|                  |  | ||||||
|         self.success = True |         self.success = True | ||||||
|                 QMessageBox.information(None, "Success",  |         self.auth_finished.emit(True) | ||||||
|                                       f"Successfully authenticated account: {self.username}") |  | ||||||
|             else: |  | ||||||
|                 raise Exception("Account not found in configuration") |  | ||||||
|                  |  | ||||||
|         except Exception as e: |  | ||||||
|             logger.error(f"Failed to update account data: {str(e)}") |  | ||||||
|             QMessageBox.critical(None, "Error", f"Failed to update account data: {str(e)}") |  | ||||||
|             self.success = False |  | ||||||
|              |  | ||||||
|         self.auth_finished.emit(self.success) |  | ||||||
| 
 | 
 | ||||||
|     def on_authentication_finished(self): |     def on_authentication_finished(self): | ||||||
|         if self.auth_dialog is not None: |         if self.auth_dialog is not None: | ||||||
| @ -361,6 +231,9 @@ class MinecraftAuthenticator(QObject): | |||||||
|             self.auth_thread.stop() |             self.auth_thread.stop() | ||||||
|             self.auth_thread.wait() |             self.auth_thread.wait() | ||||||
| 
 | 
 | ||||||
| def create_authenticator(): | # Example usage | ||||||
|     """Factory function to create a new MinecraftAuthenticator instance""" | if __name__ == '__main__': | ||||||
|     return MinecraftAuthenticator() |     app = QApplication(sys.argv) | ||||||
|  |     authenticator = MinecraftAuthenticator() | ||||||
|  |     authenticator.authenticate("TestUser") | ||||||
|  |     sys.exit(app.exec_()) | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								picodulce.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								picodulce.py
									
									
									
									
									
								
							| @ -1303,7 +1303,7 @@ class PicomcVersionSelector(QWidget): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def open_mod_loader_and_version_menu(self): |     def open_mod_loader_and_version_menu(self): | ||||||
|         dialog = ModLoaderAndVersionMenu(parent=self) |         dialog = ModLoaderAndVersionMenu() | ||||||
|         dialog.finished.connect(self.populate_installed_versions) |         dialog.finished.connect(self.populate_installed_versions) | ||||||
|         dialog.exec_() |         dialog.exec_() | ||||||
| 
 | 
 | ||||||
| @ -1324,16 +1324,9 @@ class DownloadThread(QThread): | |||||||
|             self.completed.emit(False, error_message) |             self.completed.emit(False, error_message) | ||||||
| 
 | 
 | ||||||
| class ModLoaderAndVersionMenu(QDialog): | class ModLoaderAndVersionMenu(QDialog): | ||||||
|     def __init__(self, parent=None): |     def __init__(self): | ||||||
|         super().__init__(parent) |         super().__init__() | ||||||
|         self.setWindowTitle("Mod Loader and Version Menu") |         self.setWindowTitle("Mod Loader and Version Menu") | ||||||
|         # Set window position relative to parent |  | ||||||
|         if parent: |  | ||||||
|             parent_pos = parent.pos() |  | ||||||
|             x = parent_pos.x() + (parent.width() - 400) // 2 |  | ||||||
|             y = parent_pos.y() + (parent.height() - 300) // 2 |  | ||||||
|             self.setGeometry(x, y, 400, 300) |  | ||||||
|         else: |  | ||||||
|         self.setGeometry(100, 100, 400, 300) |         self.setGeometry(100, 100, 400, 300) | ||||||
| 
 | 
 | ||||||
|         main_layout = QVBoxLayout(self) |         main_layout = QVBoxLayout(self) | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| picomc | picomc | ||||||
| PyQt5 | PyQt5 | ||||||
| requests | requests | ||||||
| aiohttp |  | ||||||
| pypresence | pypresence | ||||||
| tqdm | tqdm | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| { | { | ||||||
|   "version": "0.13.1", |   "version": "0.13", | ||||||
|   "links": [ |   "links": [ | ||||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/version.json", |     "https://raw.githubusercontent.com/nixietab/picodulce/main/version.json", | ||||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py", |     "https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py", | ||||||
| @ -11,5 +11,5 @@ | |||||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py", |     "https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py", | ||||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py" |     "https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py" | ||||||
|   ], |   ], | ||||||
|   "versionBleeding": "0.13.1-202" |   "versionBleeding": "0.13-194" | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user