Compare commits

..

32 Commits
0.13 ... main

Author SHA1 Message Date
github-actions[bot]
9523f1ab0c Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-04-15 06:23:51 +00:00
Nix
103927328a
little fix to the version managing window 2025-04-15 03:23:40 -03:00
github-actions[bot]
faefc09aad Update version.json with commit count 2025-04-15 06:13:47 +00:00
Nix
dfc2dac3f5
Update version.json 2025-04-15 03:13:37 -03:00
github-actions[bot]
7942c09082 Update version.json with commit count 2025-04-15 06:13:15 +00:00
Nix
d85cfe2de6
Update requirements.txt 2025-04-15 03:13:08 -03:00
github-actions[bot]
d145fb2df1 Update version.json with commit count 2025-04-15 06:13:02 +00:00
OmeletGit
dedc59d09c
Fix the authentication system dont saving tokens (#13) 2025-04-15 03:12:53 -03:00
github-actions[bot]
32e4783218 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-04-07 21:05:29 +00:00
Nix
502e64df83
Update version.json 2025-04-07 18:05:20 -03:00
github-actions[bot]
1cfb6ffcb6 Update version.json with commit count 2025-04-07 20:53:17 +00:00
Nix
77291ad89e
Update version.json 2025-04-07 17:53:08 -03:00
github-actions[bot]
3baf6e0b1d Update version.json with commit count 2025-04-07 20:52:52 +00:00
Nix
0768897706
stoped parsing commands (#12)
* Add files via upload

* auth done even better!
2025-04-07 17:52:43 -03:00
github-actions[bot]
ba8072c669 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-03-03 04:12:50 +00:00
Nix
785e9be9f9
Update version.json 2025-03-03 01:12:37 -03:00
github-actions[bot]
0cbd000be4 Update version.json with commit count 2025-03-03 04:11:40 +00:00
Nixietab
52b635285e Moved the health checks to a OOP method 2025-03-03 01:10:53 -03:00
github-actions[bot]
67a16c008a Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-03-01 08:34:21 +00:00
Nix
a4bd707461
Delete healtcheck.py 2025-03-01 05:34:12 -03:00
github-actions[bot]
1b27fffc96 Update version.json with commit count 2025-03-01 08:34:00 +00:00
Nix
fade5f86b7
Update picodulce.py 2025-03-01 05:33:51 -03:00
github-actions[bot]
823b438840 Update version.json with commit count 2025-03-01 08:33:36 +00:00
Nix
9a8c3f44d0
Update version.json 2025-03-01 05:33:25 -03:00
github-actions[bot]
6b65fb0d1e Update version.json with commit count 2025-03-01 08:33:18 +00:00
Nix
8247009d60
Delete locales directory 2025-03-01 05:33:07 -03:00
github-actions[bot]
e5c395d031 Update version.json with commit count 2025-03-01 08:06:58 +00:00
Nix
263e6eae07
Added rudimentary translations (#11)
* Update picodulce.py

* Update picodulce.py

* Update picodulce.py

* Update picodulce.py

* Create healtcheck.py

* Update picodulce.py

* Update version.json

* Create locales-go-here

* Add files via upload

* Update version.json

* Update picodulce.py

* Update healtcheck.py

* Update picodulce.py

* Added more locales

* Update version.json

* Update picodulce.py
2025-03-01 05:06:48 -03:00
github-actions[bot]
ec99488326 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-02-23 04:26:44 +00:00
Nix
61cd427beb
Delete locales directory 2025-02-23 01:26:35 -03:00
github-actions[bot]
cb2f5b52b3 Update version.json with commit count 2025-02-23 04:26:29 +00:00
Nix
ba40354a5d
Create bogosbinted.json 2025-02-23 01:26:19 -03:00
10 changed files with 554 additions and 574 deletions

View File

@ -1,43 +1,29 @@
import sys import sys
import subprocess import json
import re import os
import uuid
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
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from picomc.logging import logger
from picomc.launcher import get_default_root, Launcher
class AuthenticationParser: # Constants for Microsoft Authentication
@staticmethod URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"
def clean_ansi(text): URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
ansi_clean = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') URL_XBL = "https://user.auth.xboxlive.com/user/authenticate"
printable_clean = re.compile(r'[^\x20-\x7E\n]') URL_XSTS = "https://xsts.auth.xboxlive.com/xsts/authorize"
text = ansi_clean.sub('', text) URL_MC = "https://api.minecraftservices.com/authentication/login_with_xbox"
text = printable_clean.sub('', text) URL_PROFILE = "https://api.minecraftservices.com/minecraft/profile"
return text.strip()
@staticmethod CLIENT_ID = "c52aed44-3b4d-4215-99c5-824033d2bc0f"
def is_auth_error(output): SCOPE = "XboxLive.signin offline_access"
cleaned_output = AuthenticationParser.clean_ansi(output) GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
return "AADSTS70016" in cleaned_output and "not yet been authorized" in cleaned_output
@staticmethod
def parse_auth_output(output):
cleaned_output = AuthenticationParser.clean_ansi(output)
if AuthenticationParser.is_auth_error(cleaned_output):
return None
pattern = r"https://[^\s]+"
code_pattern = r"code\s+([A-Z0-9]+)"
url_match = re.search(pattern, cleaned_output)
code_match = re.search(code_pattern, cleaned_output, re.IGNORECASE)
if url_match and code_match:
return {
'url': url_match.group(0),
'code': code_match.group(1)
}
return None
class AuthDialog(QDialog): class AuthDialog(QDialog):
def __init__(self, url, code, parent=None, error_mode=False): def __init__(self, url, code, parent=None, error_mode=False):
@ -46,10 +32,10 @@ class AuthDialog(QDialog):
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setModal(True) self.setModal(True)
self.setup_ui(url, code, error_mode) self.setup_ui(url, code, error_mode)
def setup_ui(self, url, code, error_mode): def setup_ui(self, url, code, error_mode):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
if error_mode: if error_mode:
error_label = QLabel("Error in Login - Please try again") error_label = QLabel("Error in Login - Please try again")
error_label.setStyleSheet("QLabel { color: red; font-weight: bold; }") error_label.setStyleSheet("QLabel { color: red; font-weight: bold; }")
@ -106,117 +92,254 @@ 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)
def __init__(self, account): def __init__(self, username):
super().__init__() super().__init__()
self.account = account self.username = username
self.process = None self.device_code = None
self.is_running = True self.is_running = True
self.current_output = ""
self.waiting_for_auth = False 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:
command = f'picomc account authenticate {self.account}' loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.process = subprocess.Popen( loop.run_until_complete(self._auth_flow())
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
self.current_output = ""
while self.is_running and self.process.poll() is None:
line = self.process.stdout.readline()
if line:
self.current_output += line
if not self.waiting_for_auth:
parsed_data = AuthenticationParser.parse_auth_output(self.current_output)
if parsed_data:
self.auth_data_received.emit(parsed_data)
self.waiting_for_auth = True
self.current_output = ""
elif AuthenticationParser.is_auth_error(self.current_output):
self.auth_error_detected.emit(self.current_output)
self.waiting_for_auth = False
self.current_output = ""
self.process.wait()
self.finished.emit()
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 send_enter(self):
if self.process and self.process.poll() is None:
self.process.stdin.write("\n")
self.process.stdin.flush()
def stop(self): def stop(self):
self.is_running = False self.is_running = False
if self.process:
self.process.terminate()
class MinecraftAuthenticator(QObject): # Changed to inherit from QObject class MinecraftAuthenticator(QObject):
auth_finished = pyqtSignal(bool) # Add signal for completion auth_finished = pyqtSignal(bool)
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
Start the authentication process for the given username
Returns immediately, authentication result will be emitted via auth_finished signal
"""
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.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'])
if self.auth_dialog.exec_() == QDialog.Accepted:
self.auth_thread.send_enter()
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
self.auth_dialog = AuthDialog( self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code'])
self.current_auth_data['url'],
self.current_auth_data['code'], result = self.auth_dialog.exec_()
error_mode=True
) if result != QDialog.Accepted:
if self.auth_dialog.exec_() == QDialog.Accepted: self.auth_thread.stop()
self.auth_thread.send_enter()
def show_error(self, error_message): def show_error(self, error_msg):
QMessageBox.critical(None, "Error", f"Authentication error: {error_message}") QMessageBox.critical(None, "Error", error_msg)
self.success = False self.success = False
self.auth_finished.emit(False) self.auth_finished.emit(False)
def save_to_accounts_json(self):
try:
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
QMessageBox.information(None, "Success",
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:
self.auth_dialog.close() self.auth_dialog.close()
@ -226,8 +349,8 @@ class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
self.auth_thread.stop() self.auth_thread.stop()
self.auth_thread = None self.auth_thread = None
self.success = True if not self.success:
self.auth_finished.emit(True) self.auth_finished.emit(False)
def cleanup(self): def cleanup(self):
if self.auth_dialog is not None: if self.auth_dialog is not None:
@ -238,7 +361,6 @@ class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
self.auth_thread.stop() self.auth_thread.stop()
self.auth_thread.wait() self.auth_thread.wait()
# Example usage def create_authenticator():
if __name__ == '__main__': """Factory function to create a new MinecraftAuthenticator instance"""
authenticator = MinecraftAuthenticator() return MinecraftAuthenticator()
authenticator.authenticate("TestUser")

View File

@ -116,37 +116,4 @@ class HealthCheck:
# Check if both files exist and print OK message # Check if both files exist and print OK message
if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file): if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file):
print("Theme Integrity OK") print("Theme Integrity OK")
def locales_integrity(self):
# Define the locales folder path
locales_folder = "locales"
version_url = "https://raw.githubusercontent.com/nixietab/picodulce/main/version.json"
# Step 1: Ensure the locales folder exists
if not os.path.exists(locales_folder):
print(f"Creating folder: {locales_folder}")
os.makedirs(locales_folder)
self.download_locales(version_url)
else:
print("Locales folder already exists.")
def download_locales(self, url):
response = requests.get(url)
if response.status_code == 200:
data = response.json()
locales_links = data.get("locales", [])
for link in locales_links:
locale_name = os.path.basename(link)
locale_path = os.path.join("locales", locale_name)
locale_response = requests.get(link)
if locale_response.status_code == 200:
with open(locale_path, "w", encoding="utf-8") as locale_file:
locale_file.write(locale_response.text)
print(f"Downloaded and created file: {locale_path}")
else:
print(f"Failed to download {link}")
else:
print("Failed to fetch version.json")

View File

@ -1 +0,0 @@

Binary file not shown.

Binary file not shown.

Binary file not shown.

30
modulecli.py Normal file
View File

@ -0,0 +1,30 @@
import click
from picomc.cli.main import picomc_cli
from io import StringIO
import sys
def run_command(command="picomc"):
# Redirect stdout and stderr to capture the command output
old_stdout, old_stderr = sys.stdout, sys.stderr
sys.stdout = mystdout = StringIO()
sys.stderr = mystderr = StringIO()
try:
picomc_cli.main(args=command.split())
except SystemExit as e:
if e.code != 0:
print(f"Command exited with code {e.code}", file=sys.stderr)
except Exception as e:
print(f"Unexpected error: {e}", file=sys.stderr)
finally:
# Restore stdout and stderr
sys.stdout = old_stdout
sys.stderr = old_stderr
output = mystdout.getvalue().strip()
error = mystderr.getvalue().strip()
if not output:
return f"Error: No output from command. Stderr: {error}"
return output

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
picomc picomc
PyQt5 PyQt5
requests requests
aiohttp
pypresence pypresence
tqdm tqdm

View File

@ -1,5 +1,5 @@
{ {
"version": "0.13", "version": "0.13.1",
"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",
@ -7,12 +7,9 @@
"https://raw.githubusercontent.com/nixietab/picodulce/main/drums.gif", "https://raw.githubusercontent.com/nixietab/picodulce/main/drums.gif",
"https://raw.githubusercontent.com/nixietab/picodulce/main/marroc.py", "https://raw.githubusercontent.com/nixietab/picodulce/main/marroc.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico", "https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico",
"https://raw.githubusercontent.com/nixietab/picodulce/main/authser.py" "https://raw.githubusercontent.com/nixietab/picodulce/main/authser.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py"
], ],
"locales": [ "versionBleeding": "0.13.1-202"
"https://raw.githubusercontent.com/nixietab/picodulce/main/locales/picodulce_es.qm",
"https://raw.githubusercontent.com/nixietab/picodulce/main/locales/picodulce_it.qm",
"https://raw.githubusercontent.com/nixietab/picodulce/main/locales/picodulce_pt.qm"
],
"versionBleeding": "0.12-174"
} }