From f07cd2b44ac782126b7c7f8b8c0fbdd0f8169df5 Mon Sep 17 00:00:00 2001 From: DLmaster Date: Sun, 9 Feb 2025 21:36:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E9=82=AE=E7=AE=B1=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E5=8A=9F=E8=83=BD=E8=B0=83=E6=95=B4=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E7=94=B1=E7=94=A8=E6=88=B7=E6=8F=90=E4=BE=9B=E5=8F=91=E4=BF=A1?= =?UTF-8?q?=E9=82=AE=E7=AE=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 5 +- app/services/notification.py | 144 ++++++++++++++++++++++------------- app/services/security.py | 41 ++++++++-- app/ui/Widget.py | 59 +++++++++++--- app/ui/member_manager.py | 33 ++++---- app/ui/setting.py | 44 ++++++++--- resources/version.json | 4 +- 7 files changed, 229 insertions(+), 101 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index 68244f2..f0bee3f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -562,7 +562,10 @@ class GlobalConfig(QConfig): notify_IfSendErrorOnly = ConfigItem( "Notify", "IfSendErrorOnly", False, BoolValidator() ) - notify_MailAddress = ConfigItem("Notify", "MailAddress", "") + notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "") + notify_AuthorizationCode = ConfigItem("Notify", "AuthorizationCode", "") + notify_FromAddress = ConfigItem("Notify", "FromAddress", "") + notify_ToAddress = ConfigItem("Notify", "ToAddress", "") notify_IfServerChan = ConfigItem("Notify", "IfServerChan", False, BoolValidator()) notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "") diff --git a/app/services/notification.py b/app/services/notification.py index c566cdd..7f373bf 100644 --- a/app/services/notification.py +++ b/app/services/notification.py @@ -34,7 +34,8 @@ from email.utils import formataddr from serverchan_sdk import sc_send -from app.core import Config +from app.core import Config, MainInfoBar +from app.services.security import Crypto class Notification: @@ -57,73 +58,97 @@ class Notification: return True def send_mail(self, title, content): - """使用官方专用邮箱推送邮件通知""" - - # 声明:此邮箱为AUTO_MAA项目组资产,未经授权不得私自使用 - # 注意:此声明注释只有使用者更换发信邮箱时才能删除,本条规则优先级高于GPLv3 + """推送邮件通知""" if Config.global_config.get(Config.global_config.notify_IfSendMail): - # 第三方 SMTP 服务配置 - mail_host = "smtp.163.com" # 设置服务器 - mail_sender = "AUTO_MAA_server@163.com" # 用户名 - mail_key = "SYrq87nDLD4RNB5T" # 授权码 24/11/15 - - # 定义邮件正文 - message = MIMEText(content, "plain", "utf-8") - message["From"] = formataddr( - ( - Header("AUTO_MAA通知服务", "utf-8").encode(), - "AUTO_MAA_server@163.com", - ) - ) # 发件人显示的名字 - message["To"] = formataddr( - ( - Header("AUTO_MAA用户", "utf-8").encode(), - Config.global_config.get(Config.global_config.notify_MailAddress), - ) - ) # 收件人显示的名字 - message["Subject"] = Header(title, "utf-8") - try: - smtpObj = smtplib.SMTP_SSL(mail_host, 465) # 465为SMTP_SSL默认端口 - smtpObj.login(mail_sender, mail_key) + # 定义邮件正文 + message = MIMEText(content, "plain", "utf-8") + message["From"] = formataddr( + ( + Header("AUTO_MAA通知服务", "utf-8").encode(), + "AUTO_MAA_server@163.com", + ) + ) # 发件人显示的名字 + message["To"] = formataddr( + ( + Header("AUTO_MAA用户", "utf-8").encode(), + Config.global_config.get(Config.global_config.notify_ToAddress), + ) + ) # 收件人显示的名字 + message["Subject"] = Header(title, "utf-8") + + smtpObj = smtplib.SMTP_SSL( + Config.global_config.get( + Config.global_config.notify_SMTPServerAddress + ), + 465, + ) + smtpObj.login( + Config.global_config.get(Config.global_config.notify_FromAddress), + Crypto.win_decryptor( + Config.global_config.get( + Config.global_config.notify_AuthorizationCode + ) + ), + ) smtpObj.sendmail( - mail_sender, - Config.global_config.get(Config.global_config.notify_MailAddress), + Config.global_config.get(Config.global_config.notify_FromAddress), + Config.global_config.get(Config.global_config.notify_ToAddress), message.as_string(), ) - return True - except smtplib.SMTPException as e: - return f"发送邮件时出错:\n{e}" - finally: smtpObj.quit() + logger.success("邮件发送成功") + except Exception as e: + logger.error(f"发送邮件时出错:\n{e}") + MainInfoBar.push_info_bar("error", "发送邮件时出错", f"{e}", -1) def ServerChanPush(self, title, content): """使用Server酱推送通知""" if Config.global_config.get(Config.global_config.notify_IfServerChan): - send_key = Config.global_config.get(Config.global_config.notify_ServerChanKey) + send_key = Config.global_config.get( + Config.global_config.notify_ServerChanKey + ) option = {} - is_valid = lambda s: s == "" or (s == '|'.join(s.split('|')) and (s.count('|') == 0 or all(s.split('|')))) + is_valid = lambda s: s == "" or ( + s == "|".join(s.split("|")) and (s.count("|") == 0 or all(s.split("|"))) + ) """ is_valid => True, 如果启用的话需要正确设置Tag和Channel。 允许空的Tag和Channel即不启用,但不允许例如a||b,|a|b,a|b|,|||| """ - send_tag = Config.global_config.get(Config.global_config.notify_ServerChanTag) - send_channel = Config.global_config.get(Config.global_config.notify_ServerChanChannel) + send_tag = Config.global_config.get( + Config.global_config.notify_ServerChanTag + ) + send_channel = Config.global_config.get( + Config.global_config.notify_ServerChanChannel + ) if is_valid(send_tag): - option['tags'] = send_tag + option["tags"] = send_tag else: - option['tags'] = '' - logger.warning('请正确设置Auto_MAA中ServerChan的Tag。') + option["tags"] = "" + logger.warning("请正确设置Auto_MAA中ServerChan的Tag。") + MainInfoBar.push_info_bar( + "warning", + "Server酱通知推送异常", + "请正确设置Auto_MAA中ServerChan的Tag。", + -1, + ) if is_valid(send_channel): - option['channel'] = send_channel + option["channel"] = send_channel else: - option['channel'] = '' - logger.warning('请正确设置Auto_MAA中ServerChan的Channel。') + option["channel"] = "" + logger.warning("请正确设置Auto_MAA中ServerChan的Channel。") + MainInfoBar.push_info_bar( + "warning", + "Server酱通知推送异常", + "请正确设置Auto_MAA中ServerChan的Channel。", + -1, + ) response = sc_send(send_key, title, content, option) if response["code"] == 0: @@ -132,21 +157,24 @@ class Notification: else: logger.info("Server酱推送通知失败") logger.error(response) + MainInfoBar.push_info_bar( + "error", + "Server酱通知推送失败", + f'使用Server酱推送通知时出错:\n{response["data"]['error']}', + -1, + ) return f'使用Server酱推送通知时出错:\n{response["data"]['error']}' def CompanyWebHookBotPush(self, title, content): """使用企业微信群机器人推送通知""" if Config.global_config.get(Config.global_config.notify_IfCompanyWebHookBot): - content = f'{title}\n{content}' - data = { - "msgtype": "text", - "text": { - "content": content - } - } + content = f"{title}\n{content}" + data = {"msgtype": "text", "text": {"content": content}} response = requests.post( - url=Config.global_config.get(Config.global_config.notify_CompanyWebHookBotUrl), - json=data + url=Config.global_config.get( + Config.global_config.notify_CompanyWebHookBotUrl + ), + json=data, ) if response.json()["errcode"] == 0: logger.info("企业微信群机器人推送通知成功") @@ -154,7 +182,15 @@ class Notification: else: logger.info("企业微信群机器人推送通知失败") logger.error(response.json()) - return f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}' + MainInfoBar.push_info_bar( + "error", + "企业微信群机器人通知推送失败", + f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}', + -1, + ) + return ( + f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}' + ) Notify = Notification() diff --git a/app/services/security.py b/app/services/security.py index 719c124..ed7c50e 100644 --- a/app/services/security.py +++ b/app/services/security.py @@ -30,6 +30,8 @@ import sqlite3 import hashlib import random import secrets +import base64 +import win32crypt from pathlib import Path from Crypto.Cipher import AES from Crypto.PublicKey import RSA @@ -83,8 +85,8 @@ class CryptoHandler: private_key_local = AES_key.encrypt(pad(private_key.exportKey(), 32)) (Config.app_path / "data/key/private_key.bin").write_bytes(private_key_local) - def encryptx(self, note: str) -> bytes: - """加密数据""" + def AUTO_encryptor(self, note: str) -> bytes: + """使用AUTO_MAA的算法加密数据""" # 读取RSA公钥 public_key_local = RSA.import_key( @@ -95,8 +97,8 @@ class CryptoHandler: encrypted = cipher.encrypt(note.encode("utf-8")) return encrypted - def decryptx(self, note: bytes, PASSWORD: str) -> str: - """解密数据""" + def AUTO_decryptor(self, note: bytes, PASSWORD: str) -> str: + """使用AUTO_MAA的算法解密数据""" # 读入RSA私钥密文、盐与校验哈希值 private_key_local = ( @@ -150,7 +152,9 @@ class CryptoHandler: # 使用旧管理密钥解密 user_data["Password"] = [] for i in range(len(data)): - user_data["Password"].append(self.decryptx(data[i][12], PASSWORD_old)) + user_data["Password"].append( + self.AUTO_decryptor(data[i][12], PASSWORD_old) + ) cur.close() db.close() @@ -169,7 +173,7 @@ class CryptoHandler: cur.execute( "UPDATE adminx SET password = ? WHERE mode = ? AND uid = ?", ( - self.encryptx(user_data["Password"][i]), + self.AUTO_encryptor(user_data["Password"][i]), data[i][15], data[i][16], ), @@ -181,6 +185,27 @@ class CryptoHandler: cur.close() db.close() + def win_encryptor( + self, note: str, description: str = None, entropy: bytes = None + ) -> str: + """使用Windows DPAPI加密数据""" + + encrypted = win32crypt.CryptProtectData( + note.encode("utf-8"), description, entropy, None, None, 0 + ) + return base64.b64encode(encrypted).decode("utf-8") + + def win_decryptor(self, note: str, entropy: bytes = None) -> str: + """使用Windows DPAPI解密数据""" + + if note == "": + return "" + + decrypted = win32crypt.CryptUnprotectData( + base64.b64decode(note), entropy, None, None, 0 + ) + return decrypted[1].decode("utf-8") + def search_member(self) -> List[Dict[str, Union[Path, list]]]: """搜索所有脚本实例及其用户数据库路径""" @@ -197,7 +222,9 @@ class CryptoHandler: def check_PASSWORD(self, PASSWORD: str) -> bool: """验证管理密钥""" - return bool(self.decryptx(self.encryptx(""), PASSWORD) != "管理密钥错误") + return bool( + self.AUTO_decryptor(self.AUTO_encryptor(""), PASSWORD) != "管理密钥错误" + ) Crypto = CryptoHandler() diff --git a/app/ui/Widget.py b/app/ui/Widget.py index 1ec5a8f..6ac1d6b 100644 --- a/app/ui/Widget.py +++ b/app/ui/Widget.py @@ -44,14 +44,15 @@ from qfluentwidgets import ( TimeEdit, OptionsConfigItem, ) - from typing import Union, List +from app.services import Crypto -class InputMessageBox(MessageBoxBase): + +class LineEditMessageBox(MessageBoxBase): """输入对话框""" - def __init__(self, parent, title: str, content: str, mode: str, list: list = None): + def __init__(self, parent, title: str, content: str, mode: str): super().__init__(parent) self.title = SubtitleLabel(title) @@ -60,10 +61,6 @@ class InputMessageBox(MessageBoxBase): self.input.setClearButtonEnabled(True) elif mode == "密码": self.input = PasswordLineEdit() - elif mode == "选择": - self.input = ComboBox() - self.input.addItems(list) - self.input.setCurrentIndex(-1) self.input.setPlaceholderText(content) @@ -72,8 +69,8 @@ class InputMessageBox(MessageBoxBase): self.viewLayout.addWidget(self.input) -class SetMessageBox(MessageBoxBase): - """输入对话框""" +class ComboBoxMessageBox(MessageBoxBase): + """选择对话框""" def __init__(self, parent, title: str, content: List[str], list: List[List[str]]): super().__init__(parent) @@ -98,7 +95,7 @@ class SetMessageBox(MessageBoxBase): class LineEditSettingCard(SettingCard): - """Setting card with switch button""" + """Setting card with LineEdit""" textChanged = Signal(str) @@ -138,7 +135,49 @@ class LineEditSettingCard(SettingCard): self.LineEdit.setText(content) +class PasswordLineEditSettingCard(SettingCard): + """Setting card with PasswordLineEdit""" + + textChanged = Signal(str) + + def __init__( + self, + text, + icon: Union[str, QIcon, FluentIconBase], + title, + content=None, + configItem: ConfigItem = None, + parent=None, + ): + + super().__init__(icon, title, content, parent) + self.configItem = configItem + self.LineEdit = PasswordLineEdit(self) + self.LineEdit.setMinimumWidth(250) + self.LineEdit.setPlaceholderText(text) + + if configItem: + self.setValue(qconfig.get(configItem)) + configItem.valueChanged.connect(self.setValue) + + self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight) + self.hBoxLayout.addSpacing(16) + + self.LineEdit.textChanged.connect(self.__textChanged) + + def __textChanged(self, content: str): + self.setValue(Crypto.win_encryptor(content)) + self.textChanged.emit(content) + + def setValue(self, content: str): + if self.configItem: + qconfig.set(self.configItem, content) + + self.LineEdit.setText(Crypto.win_decryptor(content)) + + class SpinBoxSettingCard(SettingCard): + """Setting card with SpinBox""" textChanged = Signal(int) diff --git a/app/ui/member_manager.py b/app/ui/member_manager.py index 60d4740..ec7bfe6 100644 --- a/app/ui/member_manager.py +++ b/app/ui/member_manager.py @@ -59,10 +59,10 @@ import shutil from app.core import Config, MainInfoBar, Task_manager from app.services import Crypto from .Widget import ( - InputMessageBox, + LineEditMessageBox, LineEditSettingCard, SpinBoxSettingCard, - SetMessageBox, + ComboBoxMessageBox, ) @@ -123,16 +123,15 @@ class MemberManager(QWidget): def add_setting_box(self): """添加一个脚本实例""" - choice = InputMessageBox( + choice = ComboBoxMessageBox( self.window(), - "选择一个脚本类型并添加相应脚本实例", - "选择脚本类型", - "选择", - ["MAA"], + "选择一个脚本类型以添加相应脚本实例", + ["选择脚本类型"], + [["MAA"]], ) - if choice.exec() and choice.input.currentIndex() != -1: + if choice.exec() and choice.input[0].currentIndex() != -1: - if choice.input.currentText() == "MAA": + if choice.input[0].currentText() == "MAA": index = len(self.member_manager.search_member()) + 1 @@ -295,7 +294,7 @@ class MemberManager(QWidget): def show_password(self): if Config.PASSWORD == "": - choice = InputMessageBox( + choice = LineEditMessageBox( self.window(), "请输入管理密钥", "管理密钥", @@ -694,7 +693,7 @@ class MaaSettingBox(QWidget): user_list = [_[0] for _ in data if _[15] == "simple"] set_list = ["自定义基建"] - choice = SetMessageBox( + choice = ComboBoxMessageBox( self.window(), "用户选项配置", ["选择要配置的用户", "选择要配置的选项"], @@ -734,7 +733,7 @@ class MaaSettingBox(QWidget): user_list = [_[0] for _ in data if _[15] == "beta"] set_list = ["MAA日常配置", "MAA剿灭配置"] - choice = SetMessageBox( + choice = ComboBoxMessageBox( self.window(), "用户选项配置", ["选择要配置的用户", "选择要配置的选项"], @@ -989,7 +988,7 @@ class MaaSettingBox(QWidget): item = QTableWidgetItem("******") item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) else: - result = Crypto.decryptx(value, Config.PASSWORD) + result = Crypto.AUTO_decryptor(value, Config.PASSWORD) item = QTableWidgetItem(result) if result == "管理密钥错误": item.setFlags( @@ -1056,7 +1055,7 @@ class MaaSettingBox(QWidget): item = QTableWidgetItem("******") item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) else: - result = Crypto.decryptx(value, Config.PASSWORD) + result = Crypto.AUTO_decryptor(value, Config.PASSWORD) item = QTableWidgetItem(result) if result == "管理密钥错误": item.setFlags( @@ -1127,7 +1126,7 @@ class MaaSettingBox(QWidget): games[game_in.strip()] = game_out.strip() text = games.get(text, text) if item.column() == 11: # 密码 - text = Crypto.encryptx(text) + text = Crypto.AUTO_encryptor(text) # 保存至本地数据库 if text != "": @@ -1145,7 +1144,7 @@ class MaaSettingBox(QWidget): self.update_user_info("normal") return None if item.column() == 6: # 密码 - text = Crypto.encryptx(text) + text = Crypto.AUTO_encryptor(text) # 保存至本地数据库 if text != "": @@ -1227,7 +1226,7 @@ class MaaSettingBox(QWidget): Config.cur.execute( "INSERT INTO adminx VALUES('新用户','手机号码(官服)/B站ID(B服)','Official',-1,'y','2000-01-01','1-7','-','-','n','n','n',?,'无',0,?,?)", ( - Crypto.encryptx("未设置"), + Crypto.AUTO_encryptor("未设置"), set_book[0], set_book[1], ), diff --git a/app/ui/setting.py b/app/ui/setting.py index e78b9e1..3c4c91f 100644 --- a/app/ui/setting.py +++ b/app/ui/setting.py @@ -53,7 +53,7 @@ import requests from app.core import Config, MainInfoBar from app.services import Crypto, System from app.utils import Updater -from .Widget import InputMessageBox, LineEditSettingCard +from .Widget import LineEditMessageBox, LineEditSettingCard, PasswordLineEditSettingCard class Setting(QWidget): @@ -136,7 +136,7 @@ class Setting(QWidget): while True: - choice = InputMessageBox( + choice = LineEditMessageBox( self.window(), "未检测到管理密钥,请设置您的管理密钥", "管理密钥", @@ -162,7 +162,7 @@ class Setting(QWidget): while if_change: - choice = InputMessageBox( + choice = LineEditMessageBox( self.window(), "请输入旧的管理密钥", "旧管理密钥", @@ -177,7 +177,7 @@ class Setting(QWidget): # 获取新的管理密钥 while True: - choice = InputMessageBox( + choice = LineEditMessageBox( self.window(), "请输入新的管理密钥", "新管理密钥", @@ -557,7 +557,7 @@ class NotifySettingCard(HeaderCardWidget): super().__init__( FluentIcon.SETTING, "推送邮件通知", - "通过AUTO_MAA官方通知服务邮箱推送任务结果", + "通过电子邮箱推送任务结果", parent, ) @@ -567,18 +567,42 @@ class NotifySettingCard(HeaderCardWidget): content="是否启用邮件通知功能", configItem=Config.global_config.notify_IfSendMail, ) - self.card_MailAddress = LineEditSettingCard( - text="请输入邮箱地址", + self.card_SMTPServerAddress = LineEditSettingCard( + text="请输入SMTP服务器地址", icon=FluentIcon.PAGE_RIGHT, - title="邮箱地址", + title="SMTP服务器地址", + content="发信邮箱的SMTP服务器地址", + configItem=Config.global_config.notify_SMTPServerAddress, + ) + self.card_FromAddress = LineEditSettingCard( + text="请输入发信邮箱地址", + icon=FluentIcon.PAGE_RIGHT, + title="发信邮箱地址", + content="发送通知的邮箱地址", + configItem=Config.global_config.notify_FromAddress, + ) + self.card_AuthorizationCode = PasswordLineEditSettingCard( + text="请输入发信邮箱授权码", + icon=FluentIcon.PAGE_RIGHT, + title="发信邮箱授权码", + content="发送通知的邮箱授权码", + configItem=Config.global_config.notify_AuthorizationCode, + ) + self.card_ToAddress = LineEditSettingCard( + text="请输入收信邮箱地址", + icon=FluentIcon.PAGE_RIGHT, + title="收信邮箱地址", content="接收通知的邮箱地址", - configItem=Config.global_config.notify_MailAddress, + configItem=Config.global_config.notify_ToAddress, ) widget = QWidget() Layout = QVBoxLayout(widget) Layout.addWidget(self.card_IfSendMail) - Layout.addWidget(self.card_MailAddress) + Layout.addWidget(self.card_SMTPServerAddress) + Layout.addWidget(self.card_FromAddress) + Layout.addWidget(self.card_AuthorizationCode) + Layout.addWidget(self.card_ToAddress) self.viewLayout.setContentsMargins(0, 0, 0, 0) self.viewLayout.setSpacing(0) self.addGroupWidget(widget) diff --git a/resources/version.json b/resources/version.json index 91658ff..9603433 100644 --- a/resources/version.json +++ b/resources/version.json @@ -1,7 +1,7 @@ { - "main_version": "4.2.2.5", + "main_version": "4.2.3.0", "updater_version": "1.1.1.3", - "announcement": "\n## 新增功能\n- 添加用户每日代理次数上限功能 #15\n- 新增代理成功消息推送渠道Server酱与企业微信群机器人推送\n- 添加更新类别可选项\n- 添加调度队列完成任务后行为选项\n- 初步完成`托管bilibili游戏隐私政策`功能\n## 修复BUG\n- 修复自定义基建无法正常使用的问题\n- 修正人工排查文案\n- 修复高级MAA配置序号错位\n- 修复高级用户列表无法配置问题\n- 修复主调度台选项乱动问题\n- 修复更新器文件夹定位问题\n- 修复窗口记忆功能失效问题\n## 程序优化\n- 优化弹窗逻辑\n- 优化静默判定逻辑\n- 调整MAA设置目录时打开当前已配置的目录位置", + "announcement": "\n## 新增功能\n- 添加用户每日代理次数上限功能 #15\n- 新增代理成功消息推送渠道Server酱与企业微信群机器人推送\n- 添加更新类别可选项\n- 添加调度队列完成任务后行为选项\n- 初步完成`托管bilibili游戏隐私政策`功能\n## 修复BUG\n- 修复自定义基建无法正常使用的问题\n- 修正人工排查文案\n- 修复高级MAA配置序号错位\n- 修复高级用户列表无法配置问题\n- 修复主调度台选项乱动问题\n- 修复更新器文件夹定位问题\n- 修复窗口记忆功能失效问题\n## 程序优化\n- 优化弹窗逻辑\n- 优化静默判定逻辑\n- 调整MAA设置目录时打开当前已配置的目录位置\n- 邮箱推送功能调整,改由用户提供发信邮箱", "proxy_list": [ "", "https://gitproxy.click/",