Skip to content

Commit 3e33670

Browse files
author
Shanshui2024
committed
Update: 提升至最新版本 / 逻辑增加 / 配置项增加 / 文档转移
Signed-off-by: Shanshui2024 <[email protected]>
1 parent b7e2b85 commit 3e33670

22 files changed

+687
-427
lines changed

.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# 连接配置
2+
IP=127.0.0.1 # 监听IP地址 可不填 默认为0.0.0.0
3+
PORT=8443 # 监听端口 可不填 默认为8443
4+
WEBHOOK_PATH=/webhook # 回调路径 可不填 默认为根 /
5+
6+
## SSL配置 可不指定路径 不指定时默认为./data/cert.pem和./data/key.pem
7+
SSL_CERT=./data/cert.pem
8+
SSL_KEY=./data/key.pem
9+
10+
11+
# 机器人配置
12+
APPID=appid # QQ开放平台展示的AppID
13+
BOT_SECRET=appsecret # QQ开放平台展示的AppSecret
14+
15+
16+
# 通用配置
17+
LOG_LEVEL=INFO # 日志等级 可不填 默认为INFO 可选值:INFO DEBUG
18+
19+
20+
DATA_DIR=./data # 数据目录 可不填 默认为./data
21+
LOG_DIR=./logs # 日志目录 可不填 默认为./logs
22+
PLUGINS_DIR=./plugins # 插件目录 可不填 默认为./plugins

Core/Auth.py

Lines changed: 106 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -8,131 +8,113 @@
88
import json
99
import threading
1010
from Core.Logger import logger
11+
from Core.Config import config
1112
from datetime import timedelta
1213

13-
# 从 QQ 机器人控制台获取的 Bot Secret
14-
BOT_SECRET = "您的BOTSecret"
15-
16-
# 从 QQ 机器人控制台获取的 App ID 和 Client Secret
17-
APP_ID = "您的APPID"
18-
# 全局变量存储 access_token 和过期时间
19-
access_token = None
20-
expires_at = 0
21-
22-
start_time = None
23-
24-
# 验证签名
25-
def verify_signature(bot_secret: str, signature_hex: str, timestamp: str, http_body: bytes) -> bool:
26-
seed = bot_secret.encode("utf-8")
27-
while len(seed) < nacl.bindings.crypto_sign_SEEDBYTES:
28-
seed += seed
29-
seed = seed[:nacl.bindings.crypto_sign_SEEDBYTES]
30-
signing_key = nacl.signing.SigningKey(seed)
31-
verify_key = signing_key.verify_key
32-
33-
msg = timestamp.encode("utf-8") + http_body
34-
try:
35-
signature = nacl.encoding.HexEncoder.decode(signature_hex)
36-
verify_key.verify(msg, signature)
37-
return True
38-
except nacl.exceptions.BadSignatureError:
39-
return False
40-
41-
# 生成签名
42-
def generate_signature(bot_secret: str, plain_token: str, event_ts: str) -> str:
43-
seed = bot_secret.encode("utf-8")
44-
while len(seed) < nacl.bindings.crypto_sign_SEEDBYTES:
45-
seed += seed
46-
seed = seed[:nacl.bindings.crypto_sign_SEEDBYTES]
47-
signing_key = nacl.signing.SigningKey(seed)
48-
49-
msg = event_ts.encode("utf-8") + plain_token.encode("utf-8")
50-
signed_msg = signing_key.sign(msg)
51-
signature = signed_msg.signature.hex()
52-
logger.debug(f"框架 >>> 生成签名: {signature}")
53-
return signature
54-
55-
# 验证回调请求
56-
async def validate_callback(request: Request):
57-
signature_hex = request.headers.get("X-Signature-Ed25519")
58-
timestamp = request.headers.get("X-Signature-Timestamp")
59-
http_body = await request.body()
60-
61-
if not verify_signature(BOT_SECRET, signature_hex, timestamp, http_body):
62-
raise HTTPException(status_code=401, detail="Invalid signature")
63-
# 获取 access_token 的函数
64-
def get_access_token(app_id: str, client_secret: str) -> dict:
65-
url = "https://bots.qq.com/app/getAppAccessToken"
66-
headers = {"Content-Type": "application/json"}
67-
data = {"appId": app_id, "clientSecret": client_secret}
68-
response = requests.post(url, headers=headers, data=json.dumps(data))
69-
if response.status_code == 200:
70-
return response.json()
71-
else:
72-
logger.error(f"框架 >>> 获取 access token 失败: {response.text}")
73-
raise Exception(f"Failed to get access token: {response.text}")
74-
75-
# 守护线程函数
76-
def token_refresh_thread(app_id: str, client_secret: str):
77-
global access_token, expires_at, start_time
78-
start_time = time.time()
79-
while True:
14+
15+
class Auth:
16+
def __init__(self, Bot_Secret: str, AppId: str):
17+
self.BOTSECRET = Bot_Secret
18+
self.APPID = AppId
19+
self.access_token = None
20+
self.expires_at = 0
21+
self.start_time = None
22+
23+
def verify_signature(self, signature_hex: str, timestamp: str, http_body: bytes) -> bool:
24+
seed = self.BOTSECRET.encode("utf-8")
25+
while len(seed) < nacl.bindings.crypto_sign_SEEDBYTES:
26+
seed += seed
27+
seed = seed[:nacl.bindings.crypto_sign_SEEDBYTES]
28+
signing_key = nacl.signing.SigningKey(seed)
29+
verify_key = signing_key.verify_key
30+
31+
msg = timestamp.encode("utf-8") + http_body
8032
try:
81-
# 获取新的 access_token
82-
response = get_access_token(app_id, client_secret)
83-
access_token = response["access_token"]
84-
expires_in = int(response["expires_in"])
85-
expires_at = time.time() + int(expires_in) - 45 # 提前45s刷新 / 详见:https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/api-use.html#%E8%8E%B7%E5%8F%96%E8%B0%83%E7%94%A8%E5%87%AD%E8%AF%81
86-
logger.debug(f"框架 >>> Access token 已刷新: {access_token}, 将于 {expires_in} s 后过期")
87-
except Exception as e:
88-
logger.error(f"框架 >>> 刷新 access token 失败: {e}")
89-
raise e
90-
wait_time = expires_at - time.time() # 等待时间
91-
if wait_time > 0:
92-
time.sleep(wait_time)
33+
signature = nacl.encoding.HexEncoder.decode(signature_hex)
34+
verify_key.verify(msg, signature)
35+
return True
36+
except nacl.exceptions.BadSignatureError:
37+
return False
38+
39+
def generate_signature(self, plain_token: str, event_ts: str) -> str:
40+
seed = self.BOTSECRET.encode("utf-8")
41+
while len(seed) < nacl.bindings.crypto_sign_SEEDBYTES:
42+
seed +=seed
43+
seed = seed[:nacl.bindings.crypto_sign_SEEDBYTES]
44+
signing_key = nacl.signing.SigningKey(seed)
45+
46+
msg = event_ts.encode("utf-8") + plain_token.encode("utf-8")
47+
signed_msg = signing_key.sign(msg)
48+
signature = signed_msg.signature.hex()
49+
logger.debug(f"框架 >>> 生成签名: {signature}")
50+
return signature
51+
52+
async def validate_callback(self, request: Request):
53+
signature_hex = request.headers.get("X-Signature-Ed25519")
54+
timestamp = request.headers.get("X-Signature-Timestamp")
55+
http_body = await request.body()
56+
57+
if not self.verify_signature(signature_hex, timestamp, http_body):
58+
raise HTTPException(status_code=401, detail="Invalid signature")
59+
60+
def get_access_token(self) -> dict:
61+
url = "https://bots.qq.com/app/getAppAccessToken"
62+
headers = {"Content-Type": "application/json"}
63+
data = {"appId": self.APPID, "clientSecret": self.BOTSECRET}
64+
response = requests.post(url, headers=headers, data=json.dumps(data))
65+
if response.status_code == 200:
66+
return response.json()
9367
else:
94-
logger.warning("框架 >>> Access token 已失效...立即获取新token")
95-
pass
96-
97-
def start_token_refresh(app_id: str, client_secret: str):
98-
thread = threading.Thread(target=token_refresh_thread, args=(app_id, client_secret))
99-
thread.daemon = True # 设置为守护线程
100-
thread.start()
101-
logger.debug("框架 >>> Access Token刷新线程启动")
102-
103-
def get_current_access_token():
104-
if access_token is None or time.time() >= expires_at:
105-
raise Exception("Access token is not available or has expired.")
106-
return access_token
107-
108-
109-
def format_timedelta(delta: timedelta) -> str:
110-
"""
111-
将 timedelta 对象格式化为 'XX天 XX时 XX分 XX秒' 的字符串
112-
"""
113-
days = delta.days
114-
hours, remainder = divmod(delta.seconds, 3600)
115-
minutes, seconds = divmod(remainder, 60)
116-
return f"{days}{hours:02d}{minutes:02d}{seconds:02d}秒"
117-
118-
def get_current_run_time(string_out: bool = True) -> timedelta or str:
119-
"""
120-
获取当前运行时间
121-
122-
Args:
123-
string_out (bool): 是否返回格式化后的字符串。默认为 True,返回格式化后的字符串。
124-
如果设置为 False,则返回 timedelta 对象。
125-
126-
Returns:
127-
timedelta or str: 根据 string_out 参数返回 timedelta 对象或格式化后的字符串。
128-
"""
129-
global start_time
130-
if start_time is None:
131-
start_time = time.time()
132-
now = time.time()
133-
elapsed_time = timedelta(seconds=int(now - start_time))
134-
135-
if string_out:
136-
return format_timedelta(elapsed_time)
137-
else:
138-
return elapsed_time
68+
logger.error(f"框架 >>> 获取 access token 失败: {response.text}")
69+
raise Exception(f"Failed to access get token: {response.text}")
70+
71+
def token_refresh_thread(self):
72+
self.start_time = time.time()
73+
while True:
74+
try:
75+
response = self.get_access_token()
76+
logger.debug(f'框架 >>> 获得返回内容:{response}')
77+
self.access_token = response["access_token"]
78+
expires_in = int(response["expires_in"])
79+
self.expires_at = time.time() + int(expires_in) - 45
80+
logger.debug(f"框架 >>> Access token 已刷新: {self.access_token}, 将于 {expires_in} s 后过期")
81+
except Exception as e:
82+
logger.error(f"框架 >>> 刷新 access token 失败: {e}")
83+
raise e
84+
wait_time = self.expires_at - time.time()
85+
if wait_time > 0:
86+
time.sleep(wait_time)
87+
else:
88+
logger.warning("框架 >>> Access token 已失效...立即获取新token")
89+
90+
def start_token_refresh(self):
91+
thread = threading.Thread(target=self.token_refresh_thread)
92+
thread.daemon = True
93+
thread.start()
94+
logger.debug("框架 >>> Access Token刷新线程启动")
95+
96+
def get_current_access_token(self):
97+
if self.access_token is None or time.time() >= self.expires_at:
98+
raise Exception("Access token is not available or has expired.")
99+
return self.access_token
100+
101+
def format_timedelta(self, delta: timedelta) -> str:
102+
days = delta.days
103+
hours, remainder = divmod(delta.seconds, 3600)
104+
minutes, seconds = divmod(remainder, 60)
105+
return f"{days}{hours:02d}{minutes:02d}{seconds:02d}秒"
106+
107+
def get_current_run_time(self, string_out: bool = True):
108+
if self.start_time is None:
109+
self.start_time = time.time()
110+
now = time.time()
111+
elapsed_time = timedelta(seconds=int(now - self.start_time))
112+
113+
if string_out:
114+
return self.format_timedelta(elapsed_time)
115+
else:
116+
return elapsed_time
117+
118+
119+
# 实例化 Auth 类
120+
auth = Auth(config.bot_secret, str(config.appid))

Core/Config.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import os
2+
from dotenv import load_dotenv
3+
4+
# 加载 .env 文件中的环境变量
5+
load_dotenv(".env")
6+
7+
class Config:
8+
"""
9+
配置类,用于管理应用程序的配置参数。
10+
11+
参数可以通过构造函数传入,或者从环境变量中读取。如果未提供参数且环境变量也未设置,
12+
则使用默认值。
13+
14+
Attributes:
15+
ip (str): Webhook 监听IP地址,默认 '127.0.0.1'。
16+
port (int): Webhook 监听端口,默认 8443。
17+
path (str): Webhook 监听路径,默认 '/webhook'。
18+
appid (int): 机器人AppID。无默认值,用于登录机器人、发送消息。
19+
ssl_cert (str): SSL证书路径,默认 './data/cert.pem'。
20+
ssl_key (str): SSL证书私钥路径,默认 './data/key.pem'。
21+
bot_secret (str): 机器人AppSecret。无默认值,用于登录机器人、发送消息。
22+
log_level (str): 日志级别,默认 'INFO'。
23+
log_dir (str): 日志文件存储目录,默认 './logs'。
24+
data_dir (str): 数据文件存储目录,默认 './data'。
25+
plugins_dir (str): 插件文件存储目录,默认 './plugins'。
26+
"""
27+
28+
def __init__(self, ip=None, port=None, path=None, appid=None, bot_secret=None, ssl_cert=None, ssl_key=None, log_level=None, log_dir=None, data_dir=None, plugins_dir=None):
29+
"""
30+
初始化配置实例。
31+
32+
Args:
33+
ip (str, optional): IP地址。默认为环境变量中的 'IP' 或 '0.0.0.0'。
34+
port (int, optional): 端口号。默认为环境变量中的 'PORT' 或 8443。
35+
path (str, optional): Webhook 监听路径,默认为环境变量中的 'WEBHOOK_PATH' 或 '/'。
36+
appid (int): 机器人AppID。无默认值,用于登录机器人、发送消息。
37+
ssl_cert (str): SSL证书路径,默认 './data/cert.pem'。
38+
ssl_key (str): SSL证书私钥路径,默认 './data/key.pem'。
39+
bot_secret (str): 机器人AppSecret。无默认值,用于登录机器人、发送消息。
40+
log_level (str, optional): 日志级别。默认为环境变量中的 'LOG_LEVEL' 或 'INFO'。
41+
log_dir (str, optional): 日志目录。默认为环境变量中的 'LOG_DIR' 或 './logs'。
42+
data_dir (str, optional): 数据目录。默认为环境变量中的 'DATA_DIR' 或 './data'。
43+
plugins_dir (str, optional): 插件目录。默认为环境变量中的 'PLUGINS_DIR' 或 './plugins'。
44+
"""
45+
self.ip = os.getenv('IP', ip or '0.0.0.0')
46+
try:
47+
self.port = int(os.getenv('PORT', port or 8443))
48+
except ValueError:
49+
raise ValueError("无效的端口号,请确保 PORT 是一个有效的整数")
50+
self.path = os.getenv('WEBHOOK_PATH', path or '/')
51+
self.appid = str(os.getenv('APPID', appid))
52+
self.bot_secret = os.getenv('BOT_SECRET', bot_secret)
53+
self.ssl_cert = os.getenv('SSL_CERT', ssl_cert or './data/cert.pem')
54+
self.ssl_key = os.getenv('SSL_KEY', ssl_key or './data/key.pem')
55+
self.log_level = os.getenv('LOG_LEVEL', log_level or 'INFO')
56+
self.log_dir = os.getenv('LOG_DIR', log_dir or './logs')
57+
self.data_dir = os.getenv('DATA_DIR', data_dir or './data')
58+
self.plugins_dir = os.getenv('PLUGINS_DIR', plugins_dir or './plugins')
59+
60+
# 检查必填项
61+
required_fields = {
62+
'appid': self.appid,
63+
'bot_secret': self.bot_secret,
64+
'ssl_cert': self.ssl_cert,
65+
'ssl_key': self.ssl_key
66+
}
67+
missing_fields = [k for k, v in required_fields.items() if v is None]
68+
if missing_fields:
69+
raise ValueError(f"配置项丢失!以下配置项必须有值: {', '.join(missing_fields)}")
70+
71+
def __repr__(self):
72+
"""
73+
返回配置实例的字符串表示。
74+
75+
Returns:
76+
str: 配置实例的字符串表示。
77+
"""
78+
return f"Config(ip={self.ip}, port={self.port}, path={self.path}, appid={self.appid}, bot_secret={self.bot_secret}, ssl_cert={self.ssl_cert}, ssl_key={self.ssl_key}, log_level={self.log_level}, log_dir={self.log_dir}, data_dir={self.data_dir}, plugins_dir={self.plugins_dir})"
79+
80+
# 创建 Config 实例
81+
config = Config()

0 commit comments

Comments
 (0)