mirror of
https://github.com/nixietab/picodulce.git
synced 2025-04-19 14:55:30 +01:00
Compare commits
No commits in common. "4f4ff35ee5c2aa77a1e62134de4374de256da931" and "36ff8896efaba0cb23e71d093bf1a0c93c093e9f" have entirely different histories.
4f4ff35ee5
...
36ff8896ef
244
authser.py
244
authser.py
@ -1,244 +0,0 @@
|
|||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,
|
|
||||||
QPushButton, QLineEdit, QMessageBox)
|
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject
|
|
||||||
from PyQt5.QtGui import QDesktopServices
|
|
||||||
|
|
||||||
class AuthenticationParser:
|
|
||||||
@staticmethod
|
|
||||||
def clean_ansi(text):
|
|
||||||
ansi_clean = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
||||||
printable_clean = re.compile(r'[^\x20-\x7E\n]')
|
|
||||||
text = ansi_clean.sub('', text)
|
|
||||||
text = printable_clean.sub('', text)
|
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_auth_error(output):
|
|
||||||
cleaned_output = AuthenticationParser.clean_ansi(output)
|
|
||||||
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):
|
|
||||||
def __init__(self, url, code, parent=None, error_mode=False):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle("Microsoft Authentication")
|
|
||||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
|
||||||
self.setModal(True)
|
|
||||||
self.setup_ui(url, code, error_mode)
|
|
||||||
|
|
||||||
def setup_ui(self, url, code, error_mode):
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
|
|
||||||
if error_mode:
|
|
||||||
error_label = QLabel("Error in Login - Please try again")
|
|
||||||
error_label.setStyleSheet("QLabel { color: red; font-weight: bold; }")
|
|
||||||
layout.addWidget(error_label)
|
|
||||||
|
|
||||||
instructions = QLabel(
|
|
||||||
"To authenticate your Microsoft Account:\n\n"
|
|
||||||
"1. Click 'Open Authentication Page' or visit:\n"
|
|
||||||
"2. Copy the code below\n"
|
|
||||||
"3. Paste the code on the Microsoft website\n"
|
|
||||||
"4. After completing authentication, click 'I've Completed Authentication'"
|
|
||||||
)
|
|
||||||
instructions.setWordWrap(True)
|
|
||||||
layout.addWidget(instructions)
|
|
||||||
|
|
||||||
url_label = QLabel(url)
|
|
||||||
url_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
||||||
url_label.setWordWrap(True)
|
|
||||||
layout.addWidget(url_label)
|
|
||||||
|
|
||||||
self.code_input = QLineEdit(code)
|
|
||||||
self.code_input.setReadOnly(True)
|
|
||||||
self.code_input.setAlignment(Qt.AlignCenter)
|
|
||||||
self.code_input.setStyleSheet("""
|
|
||||||
QLineEdit {
|
|
||||||
font-size: 16pt;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
layout.addWidget(self.code_input)
|
|
||||||
|
|
||||||
copy_button = QPushButton("Copy Code")
|
|
||||||
copy_button.clicked.connect(self.copy_code)
|
|
||||||
layout.addWidget(copy_button)
|
|
||||||
|
|
||||||
open_url_button = QPushButton("Open Authentication Page")
|
|
||||||
open_url_button.clicked.connect(lambda: self.open_url(url))
|
|
||||||
layout.addWidget(open_url_button)
|
|
||||||
|
|
||||||
continue_button = QPushButton("I've Completed Authentication")
|
|
||||||
continue_button.clicked.connect(self.accept)
|
|
||||||
layout.addWidget(continue_button)
|
|
||||||
|
|
||||||
def copy_code(self):
|
|
||||||
clipboard = QApplication.clipboard()
|
|
||||||
clipboard.setText(self.code_input.text())
|
|
||||||
|
|
||||||
def open_url(self, url):
|
|
||||||
QDesktopServices.openUrl(QUrl(url))
|
|
||||||
|
|
||||||
class AuthenticationThread(QThread):
|
|
||||||
auth_data_received = pyqtSignal(dict)
|
|
||||||
error_occurred = pyqtSignal(str)
|
|
||||||
auth_error_detected = pyqtSignal(str)
|
|
||||||
finished = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, account):
|
|
||||||
super().__init__()
|
|
||||||
self.account = account
|
|
||||||
self.process = None
|
|
||||||
self.is_running = True
|
|
||||||
self.current_output = ""
|
|
||||||
self.waiting_for_auth = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
command = f'picomc account authenticate {self.account}'
|
|
||||||
|
|
||||||
self.process = subprocess.Popen(
|
|
||||||
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:
|
|
||||||
self.error_occurred.emit(str(e))
|
|
||||||
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):
|
|
||||||
self.is_running = False
|
|
||||||
if self.process:
|
|
||||||
self.process.terminate()
|
|
||||||
|
|
||||||
class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
|
|
||||||
auth_finished = pyqtSignal(bool) # Add signal for completion
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.auth_thread = None
|
|
||||||
self.current_auth_data = None
|
|
||||||
self.auth_dialog = None
|
|
||||||
self.success = False
|
|
||||||
|
|
||||||
def authenticate(self, username):
|
|
||||||
"""
|
|
||||||
Start the authentication process for the given username
|
|
||||||
Returns immediately, authentication result will be emitted via auth_finished signal
|
|
||||||
"""
|
|
||||||
self.success = False
|
|
||||||
self.auth_thread = AuthenticationThread(username)
|
|
||||||
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.finished.connect(self.on_authentication_finished)
|
|
||||||
self.auth_thread.start()
|
|
||||||
|
|
||||||
def show_auth_dialog(self, auth_data):
|
|
||||||
self.current_auth_data = auth_data
|
|
||||||
|
|
||||||
if self.auth_dialog is not None:
|
|
||||||
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.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_message):
|
|
||||||
QMessageBox.critical(None, "Error", f"Authentication error: {error_message}")
|
|
||||||
self.success = False
|
|
||||||
self.auth_finished.emit(False)
|
|
||||||
|
|
||||||
def on_authentication_finished(self):
|
|
||||||
if self.auth_dialog is not None:
|
|
||||||
self.auth_dialog.close()
|
|
||||||
self.auth_dialog = None
|
|
||||||
|
|
||||||
if self.auth_thread:
|
|
||||||
self.auth_thread.stop()
|
|
||||||
self.auth_thread = None
|
|
||||||
|
|
||||||
self.success = True
|
|
||||||
self.auth_finished.emit(True)
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
if self.auth_dialog is not None:
|
|
||||||
self.auth_dialog.close()
|
|
||||||
self.auth_dialog = None
|
|
||||||
|
|
||||||
if self.auth_thread and self.auth_thread.isRunning():
|
|
||||||
self.auth_thread.stop()
|
|
||||||
self.auth_thread.wait()
|
|
||||||
|
|
||||||
# Example usage
|
|
||||||
if __name__ == '__main__':
|
|
||||||
authenticator = MinecraftAuthenticator()
|
|
||||||
authenticator.authenticate("TestUser")
|
|
64
picodulce.py
64
picodulce.py
@ -10,7 +10,6 @@ import requests
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from authser import MinecraftAuthenticator
|
|
||||||
from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu
|
from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu
|
||||||
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QMovie, QPixmap, QDesktopServices, QBrush
|
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QMovie, QPixmap, QDesktopServices, QBrush
|
||||||
from PyQt5.QtCore import Qt, QObject, pyqtSignal, QThread, QUrl, QMetaObject, Q_ARG, QByteArray, QSize
|
from PyQt5.QtCore import Qt, QObject, pyqtSignal, QThread, QUrl, QMetaObject, Q_ARG, QByteArray, QSize
|
||||||
@ -52,10 +51,6 @@ class PicomcVersionSelector(QWidget):
|
|||||||
if self.config.get("IsFirstLaunch", False):
|
if self.config.get("IsFirstLaunch", False):
|
||||||
self.FirstLaunch()
|
self.FirstLaunch()
|
||||||
|
|
||||||
self.authenticator = MinecraftAuthenticator(self)
|
|
||||||
self.authenticator.auth_finished.connect(self._on_auth_finished)
|
|
||||||
|
|
||||||
|
|
||||||
def load_theme_from_file(self, file_path, app):
|
def load_theme_from_file(self, file_path, app):
|
||||||
self.theme = {}
|
self.theme = {}
|
||||||
# Check if the file exists, else load 'Dark.json'
|
# Check if the file exists, else load 'Dark.json'
|
||||||
@ -450,27 +445,24 @@ class PicomcVersionSelector(QWidget):
|
|||||||
|
|
||||||
# QListWidget to display available themes
|
# QListWidget to display available themes
|
||||||
json_files_label = QLabel('Installed Themes:')
|
json_files_label = QLabel('Installed Themes:')
|
||||||
self.json_files_list_widget = QListWidget()
|
json_files_list_widget = QListWidget()
|
||||||
|
|
||||||
# Track selected theme
|
# Track selected theme
|
||||||
self.selected_theme = theme_filename # Default to current theme
|
self.selected_theme = theme_filename # Default to current theme
|
||||||
|
|
||||||
# Build the list of themes
|
|
||||||
themes_list = self.build_themes_list()
|
|
||||||
|
|
||||||
# Populate themes initially
|
# Populate themes initially
|
||||||
self.populate_themes(self.json_files_list_widget, themes_list)
|
self.populate_themes(json_files_list_widget)
|
||||||
|
|
||||||
# Update current theme label when a theme is selected
|
# Update current theme label when a theme is selected
|
||||||
self.json_files_list_widget.itemClicked.connect(
|
json_files_list_widget.itemClicked.connect(
|
||||||
lambda: self.on_theme_selected(self.json_files_list_widget, current_theme_label)
|
lambda: self.on_theme_selected(json_files_list_widget, current_theme_label)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add widgets to the layout
|
# Add widgets to the layout
|
||||||
customization_layout.addWidget(theme_background_checkbox)
|
customization_layout.addWidget(theme_background_checkbox)
|
||||||
customization_layout.addWidget(current_theme_label)
|
customization_layout.addWidget(current_theme_label)
|
||||||
customization_layout.addWidget(json_files_label)
|
customization_layout.addWidget(json_files_label)
|
||||||
customization_layout.addWidget(self.json_files_list_widget)
|
customization_layout.addWidget(json_files_list_widget)
|
||||||
|
|
||||||
# Button to download themes
|
# Button to download themes
|
||||||
download_themes_button = QPushButton("Download More Themes")
|
download_themes_button = QPushButton("Download More Themes")
|
||||||
@ -515,9 +507,9 @@ class PicomcVersionSelector(QWidget):
|
|||||||
if response == QMessageBox.No:
|
if response == QMessageBox.No:
|
||||||
checkbox.setChecked(False)
|
checkbox.setChecked(False)
|
||||||
|
|
||||||
def build_themes_list(self):
|
def populate_themes(self, json_files_list_widget):
|
||||||
themes_folder = os.path.join(os.getcwd(), "themes")
|
themes_folder = os.path.join(os.getcwd(), "themes")
|
||||||
themes_list = []
|
json_files_list_widget.clear()
|
||||||
if os.path.exists(themes_folder):
|
if os.path.exists(themes_folder):
|
||||||
json_files = [f for f in os.listdir(themes_folder) if f.endswith('.json')]
|
json_files = [f for f in os.listdir(themes_folder) if f.endswith('.json')]
|
||||||
for json_file in json_files:
|
for json_file in json_files:
|
||||||
@ -533,12 +525,6 @@ class PicomcVersionSelector(QWidget):
|
|||||||
|
|
||||||
# Create display text and list item
|
# Create display text and list item
|
||||||
display_text = f"{name}\n{description}\nBy: {author}"
|
display_text = f"{name}\n{description}\nBy: {author}"
|
||||||
themes_list.append((display_text, json_file))
|
|
||||||
return themes_list
|
|
||||||
|
|
||||||
def populate_themes(self, json_files_list_widget, themes_list):
|
|
||||||
json_files_list_widget.clear()
|
|
||||||
for display_text, json_file in themes_list:
|
|
||||||
list_item = QListWidgetItem(display_text)
|
list_item = QListWidgetItem(display_text)
|
||||||
list_item.setData(Qt.UserRole, json_file) # Store the JSON filename as metadata
|
list_item.setData(Qt.UserRole, json_file) # Store the JSON filename as metadata
|
||||||
|
|
||||||
@ -603,16 +589,9 @@ class PicomcVersionSelector(QWidget):
|
|||||||
main_layout.addLayout(right_layout)
|
main_layout.addLayout(right_layout)
|
||||||
dialog.setLayout(main_layout)
|
dialog.setLayout(main_layout)
|
||||||
|
|
||||||
dialog.finished.connect(lambda: self.update_themes_list())
|
|
||||||
|
|
||||||
|
|
||||||
self.load_themes()
|
self.load_themes()
|
||||||
dialog.exec_()
|
dialog.exec_()
|
||||||
|
|
||||||
def update_themes_list(self):
|
|
||||||
themes_list = self.build_themes_list()
|
|
||||||
self.populate_themes(self.json_files_list_widget, themes_list)
|
|
||||||
|
|
||||||
def fetch_themes(self):
|
def fetch_themes(self):
|
||||||
try:
|
try:
|
||||||
with open("config.json", "r") as config_file:
|
with open("config.json", "r") as config_file:
|
||||||
@ -1084,36 +1063,19 @@ class PicomcVersionSelector(QWidget):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def authenticate_account(self, dialog, account_name):
|
def authenticate_account(self, dialog, account_name):
|
||||||
# Clean up the account name
|
# Authenticate a selected account
|
||||||
account_name = account_name.strip().lstrip(" * ")
|
account_name = account_name.strip().lstrip(" * ")
|
||||||
if not account_name:
|
if not account_name:
|
||||||
QMessageBox.warning(dialog, "Warning", "Please select an account to authenticate.")
|
QMessageBox.warning(dialog, "Warning", "Please select an account to authenticate.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create authenticator instance if it doesn't exist
|
subprocess.run(['picomc', 'account', 'authenticate', account_name], check=True)
|
||||||
if self.authenticator is None:
|
QMessageBox.information(self, "Success", f"Account '{account_name}' authenticated successfully!")
|
||||||
self.authenticator = MinecraftAuthenticator(self)
|
except subprocess.CalledProcessError as e:
|
||||||
self.authenticator.auth_finished.connect(self._on_auth_finished)
|
error_message = f"Error authenticating account '{account_name}': {e.stderr.decode()}"
|
||||||
|
|
||||||
# Start authentication process
|
|
||||||
self.authenticator.authenticate(account_name)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_message = f"Error authenticating account '{account_name}': {str(e)}"
|
|
||||||
logging.error(error_message)
|
logging.error(error_message)
|
||||||
QMessageBox.critical(dialog, "Error", error_message)
|
QMessageBox.critical(self, "Error", error_message)
|
||||||
|
|
||||||
def _on_auth_finished(self, success):
|
|
||||||
if success:
|
|
||||||
QMessageBox.information(self, "Success", "Account authenticated successfully!")
|
|
||||||
else:
|
|
||||||
QMessageBox.critical(self, "Error", "Failed to authenticate account")
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
if self.authenticator:
|
|
||||||
self.authenticator.cleanup()
|
|
||||||
self.authenticator = None
|
|
||||||
|
|
||||||
def remove_account(self, dialog, username):
|
def remove_account(self, dialog, username):
|
||||||
# Remove a selected account
|
# Remove a selected account
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.12",
|
"version": "0.11.9",
|
||||||
"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",
|
||||||
"https://raw.githubusercontent.com/nixietab/picodulce/main/requirements.txt",
|
"https://raw.githubusercontent.com/nixietab/picodulce/main/requirements.txt",
|
||||||
"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"
|
|
||||||
],
|
],
|
||||||
"versionBleeding": "0.12-168"
|
"versionBleeding": "0.11.9-156"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user