diff --git a/NOTICE b/NOTICE index 6832859..1a08965 100644 --- a/NOTICE +++ b/NOTICE @@ -6,6 +6,7 @@ This product includes software developed by AptS-1547 (https://github.com/AptS-1 This software uses the following third-party libraries that are distributed under their own licenses: +- APScheduler (https://github.com/agronholm/apscheduler) - MIT License - FastAPI (https://fastapi.tiangolo.com/) - MIT License - Pydantic (https://pydantic-docs.helpmanual.io/) - MIT License - Pydantic-settings (https://github.com/pydantic/pydantic-settings) - MIT License diff --git a/app/api/github_webhook.py b/app/api/github_webhook.py index 4653e9f..505f633 100644 --- a/app/api/github_webhook.py +++ b/app/api/github_webhook.py @@ -24,7 +24,8 @@ from fastapi import APIRouter, Request, HTTPException from app.core import GitHubWebhookHandler -from app.onebot import text, get_onebot_client +from app.botclient import BotClient +from app.models import MessageSegment from app.models.config import get_settings router = APIRouter() @@ -33,12 +34,11 @@ config = get_settings() @router.post("") -async def github_webhook( - request: Request, -): # pylint: disable=too-many-return-statements +async def github_webhook(request: Request): """处理 GitHub webhook 请求""" + # TODO: 为了后期使用自定义模版,作为临时过渡,需要完全重构 - onebot_client = get_onebot_client() + onebot_client = BotClient.get_client("onebot") if not onebot_client: logger.error("OneBot 客户端未初始化,无法处理请求") @@ -54,6 +54,11 @@ async def github_webhook( logger.info("缺少 X-GitHub-Event 头,忽略") return {"status": "ignored", "message": "缺少 X-GitHub-Event 头"} + payload = await request.json() + if not payload: + logger.info("请求体为空,忽略") + return {"status": "ignored", "message": "请求体为空"} + try: await GitHubWebhookHandler.verify_signature( request, @@ -64,26 +69,27 @@ async def github_webhook( logger.info("签名验证失败: %s", e.detail) return {"status": "ignored", "message": f"签名验证失败: {e.detail}"} - payload = await request.json() - if not payload: - logger.info("请求体为空,忽略") - return {"status": "ignored", "message": "请求体为空"} - repo_name = payload.get("repository", {}).get("full_name") - branch = payload.get("ref", "").replace("refs/heads/", "") + + # 根据事件类型获取分支/PR/Issue信息用于匹配 + match_info = {} + if event_type == "push": + match_info["branch"] = payload.get("ref", "").replace("refs/heads/", "") matched_webhook = GitHubWebhookHandler.find_matching_webhook( repo_name, - branch, + match_info.get("branch", ""), event_type, config.GITHUB_WEBHOOK ) if not matched_webhook: - logger.info("找不到匹配的 webhook 配置: 仓库 %s, 分支 %s, 事件类型 %s", repo_name, branch, event_type) + logger.info("找不到匹配的 webhook 配置: 仓库 %s, 事件类型 %s", repo_name, event_type) return {"status": "ignored", "message": "找不到匹配的 webhook 配置"} - # 处理不同类型的事件,暂时只支持 push 事件 + message = None + + # 处理不同类型的事件 if event_type == "push": push_data = GitHubWebhookHandler.extract_push_data(payload) @@ -93,37 +99,103 @@ async def github_webhook( push_data["pusher"], push_data["commit_count"]) + message = format_github_push_message( + repo_name=push_data["repo_name"], + branch=push_data["branch"], + pusher=push_data["pusher"], + commit_count=push_data["commit_count"], + commits=push_data["commits"] + ) + + elif event_type == "pull_request": + pr_data = GitHubWebhookHandler.extract_pull_request_data(payload) + + logger.info("发现新的 pull_request 事件,来自 %s 仓库", pr_data["repo_name"]) + logger.info("PR #%s, 操作: %s, 用户: %s", + pr_data["pull_request_number"], + pr_data["action"], + pr_data["user"]) + + message = format_github_pull_request_message( + repo_name=pr_data["repo_name"], + action=pr_data["action"], + pull_request=pr_data["pull_request"], + user=pr_data["user"] + ) + + elif event_type == "issues": + issue_data = GitHubWebhookHandler.extract_issue_data(payload) + + logger.info("发现新的 issue 事件,来自 %s 仓库", issue_data["repo_name"]) + logger.info("Issue #%s, 操作: %s, 用户: %s", + issue_data["issue_number"], + issue_data["action"], + issue_data["user"]) + + message = format_github_issue_message( + repo_name=issue_data["repo_name"], + action=issue_data["action"], + issue=issue_data["issue"], + user=issue_data["user"] + ) + + elif event_type == "release": + release_data = GitHubWebhookHandler.extract_release_data(payload) + + logger.info("发现新的 release 事件,来自 %s 仓库", release_data["repo_name"]) + logger.info("版本: %s, 操作: %s, 用户: %s", + release_data["release_tag"], + release_data["action"], + release_data["user"]) + + message = format_github_release_message( + repo_name=release_data["repo_name"], + action=release_data["action"], + release=release_data["release"], + user=release_data["user"] + ) + + elif event_type == "issue_comment": + comment_data = GitHubWebhookHandler.extract_issue_comment_data(payload) + + logger.info("发现新的 issue_comment 事件,来自 %s 仓库", comment_data["repo_name"]) + logger.info("Issue #%s, 操作: %s, 用户: %s", + comment_data["issue_number"], + comment_data["action"], + comment_data["user"]) + + message = format_github_issue_comment_message( + repo_name=comment_data["repo_name"], + action=comment_data["action"], + comment=comment_data["comment"], + issue_number=comment_data["issue_number"], + user=comment_data["user"] + ) + else: + logger.info("收到 %s 事件,但尚未实现处理逻辑", event_type) + return {"status": "ignored", "message": f"暂不处理 {event_type} 类型的事件"} + + if message: # 向配置的所有 OneBot 目标发送通知 for target in matched_webhook.ONEBOT: logger.info("正在发送消息到 QQ %s %s", target.type, target.id) - - message = format_github_push_message( - repo_name=push_data["repo_name"], - branch=push_data["branch"], - pusher=push_data["pusher"], - commit_count=push_data["commit_count"], - commits=push_data["commits"] - ) - - # 使用已有的客户端发送消息 await onebot_client.send_message(target.type, target.id, message) - return {"status": "success", "message": "处理 push 事件成功"} + return {"status": "success", "message": f"处理 {event_type} 事件成功"} - logger.info("收到 %s 事件,但尚未实现处理逻辑", event_type) - return {"status": "ignored", "message": f"暂不处理 {event_type} 类型的事件"} + return {"status": "failed", "message": "处理事件时发生错误"} def format_github_push_message(repo_name, branch, pusher, commit_count, commits): """格式化 GitHub 推送消息""" message = [ - text("📢 GitHub 推送通知\n"), - text(f"仓库:{repo_name}\n"), - text(f"分支:{branch}\n"), - text(f"推送者:{pusher}\n"), - text(f"提交数量:{commit_count}\n\n"), - text("最新提交:\n") + MessageSegment.text("📢 GitHub 推送通知\n"), + MessageSegment.text(f"仓库:{repo_name}\n"), + MessageSegment.text(f"分支:{branch}\n"), + MessageSegment.text(f"推送者:{pusher}\n"), + MessageSegment.text(f"提交数量:{commit_count}\n\n"), + MessageSegment.text("最新提交:\n") ] # 最多展示3条最新提交 @@ -132,6 +204,149 @@ def format_github_push_message(repo_name, branch, pusher, commit_count, commits) commit_message = commit["message"].split("\n")[0] # 只取第一行 author = commit.get("author", {}).get("name", "未知") - message.append(text(f"[{short_id}] {commit_message} (by {author})\n")) + message.append(MessageSegment.text(f"[{short_id}] {commit_message} (by {author})\n")) + + return message + +def format_github_pull_request_message(repo_name, action, pull_request, user): + """格式化 GitHub Pull Request 消息""" + + # 针对不同动作定制消息内容 + action_text = { + "opened": "创建了", + "closed": "关闭了" if not pull_request.get("merged") else "合并了", + "reopened": "重新打开了", + "assigned": "被分配了", + "unassigned": "被取消分配了", + "review_requested": "请求审核", + "review_request_removed": "取消审核请求", + "labeled": "被添加了标签", + "unlabeled": "被移除了标签", + "synchronize": "同步了", + }.get(action, action) + + message = [ + MessageSegment.text(f"📢 GitHub Pull Request {action_text}\n"), + MessageSegment.text(f"仓库:{repo_name}\n"), + MessageSegment.text(f"PR #{pull_request.get('number')}: {pull_request.get('title')}\n"), + MessageSegment.text(f"用户:{user}\n"), + MessageSegment.text(f"状态:{pull_request.get('state')}\n") + ] + + # 添加分支信息 + base = pull_request.get("base", {}).get("ref", "") + head = pull_request.get("head", {}).get("ref", "") + if base and head: + message.append(MessageSegment.text(f"目标分支:{base} ← {head}\n")) + + # 添加链接 + if pull_request.get("html_url"): + message.append(MessageSegment.text(f"链接:{pull_request.get('html_url')}\n")) + + return message + +def format_github_issue_message(repo_name, action, issue, user): + """格式化 GitHub Issue 消息""" + + # 针对不同动作定制消息内容 + action_text = { + "opened": "创建了", + "closed": "关闭了", + "reopened": "重新打开了", + "assigned": "被分配了", + "unassigned": "被取消分配了", + "labeled": "被添加了标签", + "unlabeled": "被移除了标签", + }.get(action, action) + + message = [ + MessageSegment.text(f"📢 GitHub Issue {action_text}\n"), + MessageSegment.text(f"仓库:{repo_name}\n"), + MessageSegment.text(f"Issue #{issue.get('number')}: {issue.get('title')}\n"), + MessageSegment.text(f"用户:{user}\n"), + MessageSegment.text(f"状态:{issue.get('state')}\n") + ] + + # 添加标签信息 + labels = issue.get("labels", []) + if labels: + label_names = [label.get("name", "") for label in labels] + message.append(MessageSegment.text(f"标签:{', '.join(label_names)}\n")) + + # 添加链接 + if issue.get("html_url"): + message.append(MessageSegment.text(f"链接:{issue.get('html_url')}\n")) + + return message + +def format_github_release_message(repo_name, action, release, user): + """格式化 GitHub Release 消息""" + + # 针对不同动作定制消息内容 + action_text = { + "published": "发布了", + "created": "创建了", + "edited": "编辑了", + "deleted": "删除了", + "prereleased": "预发布了", + "released": "正式发布了", + }.get(action, action) + + tag_name = release.get("tag_name", "") + name = release.get("name", tag_name) if release.get("name") else tag_name + + message = [ + MessageSegment.text(f"📢 GitHub Release {action_text}\n"), + MessageSegment.text(f"仓库:{repo_name}\n"), + MessageSegment.text(f"版本:{name} ({tag_name})\n"), + MessageSegment.text(f"发布者:{user}\n") + ] + + # 添加预发布信息 + if release.get("prerelease"): + message.append(MessageSegment.text("类型:预发布\n")) + + # 添加发布时间 + if release.get("published_at"): + published_time = release.get("published_at") + message.append(MessageSegment.text(f"发布时间:{published_time}\n")) + + # 添加链接 + if release.get("html_url"): + message.append(MessageSegment.text(f"链接:{release.get('html_url')}\n")) + + return message + +def format_github_issue_comment_message(repo_name, action, comment, issue_number, user): + """格式化 GitHub Issue/PR 评论消息""" + + # 针对不同动作定制消息内容 + action_text = { + "created": "发表了", + "edited": "编辑了", + "deleted": "删除了", + }.get(action, action) + + # 判断是PR还是Issue + issue_type = "PR" if comment.get("pull_request") else "Issue" + + message = [ + MessageSegment.text(f"📢 GitHub {issue_type}评论 {action_text}\n"), + MessageSegment.text(f"仓库:{repo_name}\n"), + MessageSegment.text(f"{issue_type} #{issue_number}\n"), + MessageSegment.text(f"用户:{user}\n") + ] + + # 添加评论内容预览 + body = comment.get("body", "") + if body and action != "deleted": + # 截取评论内容,最多显示100个字符 + preview = body[:100] + "..." if len(body) > 100 else body + preview = preview.replace("\n", " ") + message.append(MessageSegment.text(f"内容:{preview}\n")) + + # 添加链接 + if comment.get("html_url"): + message.append(MessageSegment.text(f"链接:{comment.get('html_url')}\n")) return message diff --git a/app/botclient/__init__.py b/app/botclient/__init__.py new file mode 100644 index 0000000..b7724af --- /dev/null +++ b/app/botclient/__init__.py @@ -0,0 +1,83 @@ +# Copyright 2025 AptS-1547 +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +OneBot GitHub Webhook botclient 模块 +本模块用于各类 Bot 客户端的实现,包括 Onebot、Rocket.Chat、Telegram 等。 +本模块的设计目标是提供一个统一的接口,以便于在不同的 Bot 客户端之间进行切换和扩展。 +作者:AptS:1547 +版本:0.1.0-alpha +日期:2025-04-17 +本程序遵循 Apache License 2.0 许可证 +""" + +import logging +from typing import Dict, Optional, Any + +from .onebot import ( + init_onebot_client, + shutdown_onebot_client +) + +_logger = logging.getLogger(__name__) + +class BotClient: + """统一的 Bot 客户端接口""" + + _clients_registry: Dict[str, Any] = {} + + @classmethod + async def init_client(cls, client_type: str, *args, **kwargs): + """ + 初始化 Bot 客户端 + 根据 client_type 的不同,初始化不同的 Bot 客户端 + """ + + _logger.info("Initializing client of type: %s", client_type) + + if client_type == "onebot": + client = await init_onebot_client(*args, **kwargs) + elif client_type == "rocketchat": + raise NotImplementedError("Rocket.Chat client is not implemented yet.") + elif client_type == "telegram": + raise NotImplementedError("Telegram client is not implemented yet.") + else: + raise ValueError(f"Unsupported client type: {client_type}") + + cls._clients_registry[client_type] = client + return client + + @classmethod + def get_client(cls, client_type: str) -> Optional[Any]: + """ + 获取指定类型的客户端实例 + + Args: + client_type: 客户端类型 + + Returns: + 客户端实例,如果不存在则返回None + """ + return cls._clients_registry.get(client_type) + + @classmethod + def shutdown_client(cls, *args, **kwargs): + """ + 关闭 Bot 客户端 + 关闭全部的 Bot 客户端 + """ + return shutdown_onebot_client(*args, **kwargs) + +__all__ = [ + "BotClient", +] diff --git a/app/onebot/__init__.py b/app/botclient/onebot/__init__.py similarity index 98% rename from app/onebot/__init__.py rename to app/botclient/onebot/__init__.py index 90e4f9c..c580d36 100644 --- a/app/onebot/__init__.py +++ b/app/botclient/onebot/__init__.py @@ -23,7 +23,6 @@ from .onebot import ( OneBotWebSocketClient, OneBotHTTPClient, - text, init_onebot_client, get_onebot_client, shutdown_onebot_client @@ -32,7 +31,6 @@ __all__ = [ "OneBotWebSocketClient", "OneBotHTTPClient", - "text", "init_onebot_client", "get_onebot_client", "shutdown_onebot_client" diff --git a/app/onebot/onebot.py b/app/botclient/onebot/onebot.py similarity index 97% rename from app/onebot/onebot.py rename to app/botclient/onebot/onebot.py index 85acb89..ee3e11d 100644 --- a/app/onebot/onebot.py +++ b/app/botclient/onebot/onebot.py @@ -410,16 +410,11 @@ async def send_message( logger.error("发送消息时出错: %s", e) raise -# 消息段工具函数 -def text(content: str) -> Dict[str, Any]: - """纯文本消息""" - return {"type": "text", "data": {"text": content}} - # 全局客户端实例 _ONEBOT_CLIENT = None async def init_onebot_client( - client_type: str, + protocol_type: str, url: str, access_token: str = "", max_retries: int = 5, @@ -429,7 +424,7 @@ async def init_onebot_client( 初始化全局 OneBot 客户端 参数: - client_type: 客户端类型,"ws" 或 "http" + protocol_type: 客户端类型,"ws" 或 "http" url: OneBot 实现的 URL 地址 access_token: 鉴权 token,如果有的话 max_retries: 最大重试次数 (仅用于 WebSocket) @@ -444,7 +439,7 @@ async def init_onebot_client( logger.warning("OneBot 客户端已经初始化,将返回现有实例") return _ONEBOT_CLIENT - if client_type == "ws": + if protocol_type == "ws": _ONEBOT_CLIENT = OneBotWebSocketClient(url, access_token) # 启动 WebSocket 连接 logger.info("正在初始化 WebSocket 连接...") @@ -454,10 +449,10 @@ async def init_onebot_client( except Exception as e: # pylint: disable=broad-except logger.error("建立 WebSocket 连接失败: %s", e) raise - elif client_type == "http": + elif protocol_type == "http": _ONEBOT_CLIENT = OneBotHTTPClient(url, access_token) else: - raise ValueError(f"不支持的客户端类型: {client_type}") + raise ValueError(f"不支持的协议类型: {protocol_type}") return _ONEBOT_CLIENT diff --git a/app/core/github/__init__.py b/app/core/github/__init__.py new file mode 100644 index 0000000..9444c46 --- /dev/null +++ b/app/core/github/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2025 AptS-1547 +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +GitHub 处理模块 +本模块用于处理 GitHub Webhook 事件以及轮训 GitHub 仓库的相关操作。 +作者:AptS:1547 +版本:0.1.0-alpha +日期:2025-04-17 +本程序遵循 Apache License 2.0 许可证 +""" + +from .webhook import GitHubWebhookHandler +from .polling import GitHubPollingHandler + +__all__ = [ + "GitHubWebhookHandler", + "GitHubPollingHandler", +] diff --git a/app/core/github/polling.py b/app/core/github/polling.py new file mode 100644 index 0000000..15ba122 --- /dev/null +++ b/app/core/github/polling.py @@ -0,0 +1,27 @@ +# Copyright 2025 AptS-1547 +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +GitHub Polling 模块 +本模块用于处理 GitHub 仓库的轮训操作。 +作者:AptS:1547 +版本:0.1.0-alpha +日期:2025-04-17 +本程序遵循 Apache License 2.0 许可证 +""" + + + +class GitHubPollingHandler: + """GitHub Polling 处理类""" + pass diff --git a/app/core/github.py b/app/core/github/webhook.py similarity index 64% rename from app/core/github.py rename to app/core/github/webhook.py index 7059d9d..6d2a88d 100644 --- a/app/core/github.py +++ b/app/core/github/webhook.py @@ -132,3 +132,79 @@ def extract_push_data(payload: Dict[str, Any]) -> Dict[str, Any]: "commits": payload.get("commits", []), "commit_count": len(payload.get("commits", [])) } + + @staticmethod + def extract_pull_request_data(payload: Dict[str, Any]) -> Dict[str, Any]: + """ + 从 pull request 事件中提取相关数据 + + Args: + payload: GitHub webhook 负载 + + Returns: + 包含提取数据的字典 + """ + return { + "repo_name": payload.get("repository", {}).get("full_name"), + "pull_request_number": payload.get("number"), + "action": payload.get("action"), + "pull_request": payload.get("pull_request", {}), + "user": payload.get("sender", {}).get("login") + } + + @staticmethod + def extract_issue_data(payload: Dict[str, Any]) -> Dict[str, Any]: + """ + 从 issue 事件中提取相关数据 + + Args: + payload: GitHub webhook 负载 + + Returns: + 包含提取数据的字典 + """ + return { + "repo_name": payload.get("repository", {}).get("full_name"), + "issue_number": payload.get("issue", {}).get("number"), + "action": payload.get("action"), + "issue": payload.get("issue", {}), + "user": payload.get("sender", {}).get("login") + } + + @staticmethod + def extract_release_data(payload: Dict[str, Any]) -> Dict[str, Any]: + """ + 从 release 事件中提取相关数据 + + Args: + payload: GitHub webhook 负载 + + Returns: + 包含提取数据的字典 + """ + return { + "repo_name": payload.get("repository", {}).get("full_name"), + "release_tag": payload.get("release", {}).get("tag_name"), + "action": payload.get("action"), + "release": payload.get("release", {}), + "user": payload.get("sender", {}).get("login") + } + + @staticmethod + def extract_issue_comment_data(payload: Dict[str, Any]) -> Dict[str, Any]: + """ + 从 issue comment 事件中提取相关数据 + + Args: + payload: GitHub webhook 负载 + + Returns: + 包含提取数据的字典 + """ + return { + "repo_name": payload.get("repository", {}).get("full_name"), + "issue_number": payload.get("issue", {}).get("number"), + "action": payload.get("action"), + "comment": payload.get("comment", {}), + "user": payload.get("sender", {}).get("login") + } diff --git a/app/models/__init__.py b/app/models/__init__.py index 1e6f54c..41b7ba5 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -20,7 +20,9 @@ """ from .config import get_settings +from .message import MessageSegment __all__ = [ "get_settings", + "MessageSegment" ] diff --git a/app/models/config.py b/app/models/config.py index 60b639a..19ffad5 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -49,7 +49,7 @@ class Config(BaseModel): ENV: str = "production" ONEBOT_URL: str = "" - ONEBOT_TYPE: Literal["http", "ws"] = "ws" + ONEBOT_PROTOCOL_TYPE: Literal["http", "ws"] = "ws" ONEBOT_ACCESS_TOKEN: str = "" GITHUB_WEBHOOK: List[WebhookConfig] = [] @@ -57,9 +57,9 @@ class Config(BaseModel): def validate_onebot_url(self) -> 'Config': """验证 ONEBOT_URL 的格式是否与 ONEBOT_TYPE 匹配""" if self.ONEBOT_URL: - if self.ONEBOT_TYPE == "ws" and not (self.ONEBOT_URL.startswith("ws://") or self.ONEBOT_URL.startswith("wss://")): # pylint: disable=line-too-long + if self.ONEBOT_PROTOCOL_TYPE == "ws" and not (self.ONEBOT_URL.startswith("ws://") or self.ONEBOT_URL.startswith("wss://")): # pylint: disable=line-too-long raise ValueError("当 ONEBOT_TYPE 为 ws 时,ONEBOT_URL 必须以 'ws://' 或 'wss://' 开头") - if self.ONEBOT_TYPE == "http" and not (self.ONEBOT_URL.startswith("http://") or self.ONEBOT_URL.startswith("https://")): # pylint: disable=line-too-long + if self.ONEBOT_PROTOCOL_TYPE == "http" and not (self.ONEBOT_URL.startswith("http://") or self.ONEBOT_URL.startswith("https://")): # pylint: disable=line-too-long raise ValueError("当 ONEBOT_TYPE 为 http 时,ONEBOT_URL 必须以 'http://' 或 'https://' 开头") return self @@ -104,7 +104,7 @@ def from_yaml(cls, yaml_file: str = "config.yaml"): default_config = { "ENV": "production", "ONEBOT_URL": "", - "ONEBOT_TYPE": "ws", + "ONEBOT_PROTOCOL_TYPE": "ws", "ONEBOT_ACCESS_TOKEN": "", "GITHUB_WEBHOOK": [ { diff --git a/app/models/message.py b/app/models/message.py new file mode 100644 index 0000000..e1a6b29 --- /dev/null +++ b/app/models/message.py @@ -0,0 +1,35 @@ +# Copyright 2025 AptS-1547 +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Onebot Github Webhook 消息模型 +本文件用于定义 Onebot Github Webhook 的消息模型。 +作者:AptS:1547 +版本:0.1.0-alpha +日期:2025-04-17 +本程序遵循 Apache License 2.0 许可证 +""" + +from typing import Dict, Any + +class MessageSegment: + """ + 消息段类 + 用于定义消息段的类型和数据 + """ + + # 消息段工具函数 + @staticmethod + def text(content: str) -> Dict[str, Any]: + """纯文本消息""" + return {"type": "text", "data": {"text": content}} diff --git a/config.example.yaml b/config.example.yaml index 9a5b60f..1497436 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,6 +1,6 @@ ENV: production ONEBOT_URL: '' -ONEBOT_TYPE: 'ws' +ONEBOT_PORTOL_TYPE: 'ws' ONEBOT_ACCESS_TOKEN: '' GITHUB_WEBHOOK: - NAME: github diff --git a/main.py b/main.py index 24bdb77..5cfd7f9 100644 --- a/main.py +++ b/main.py @@ -29,7 +29,7 @@ from app.api import api_router from app.models import get_settings -from app.onebot import init_onebot_client, shutdown_onebot_client +from app.botclient import BotClient from app.utils.exceptions import InitializationError @@ -46,8 +46,9 @@ async def lifespan(_: FastAPI): """ try: - await init_onebot_client( - client_type=config.ONEBOT_TYPE, + await BotClient.init_client( + client_type="onebot", + protocol_type=config.ONEBOT_PROTOCOL_TYPE, url=config.ONEBOT_URL, access_token=config.ONEBOT_ACCESS_TOKEN ) @@ -63,7 +64,7 @@ async def lifespan(_: FastAPI): yield - await shutdown_onebot_client() + await BotClient.shutdown_client() app = FastAPI(lifespan=lifespan) diff --git a/poetry.lock b/poetry.lock index 73888ac..dd863ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -189,6 +189,39 @@ type = "legacy" url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" reference = "tsinghua" +[[package]] +name = "apscheduler" +version = "3.11.0" +description = "In-process task scheduler with Cron-like capabilities" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da"}, + {file = "apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133"}, +] + +[package.dependencies] +tzlocal = ">=3.0" + +[package.extras] +doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"] +etcd = ["etcd3", "protobuf (<=3.21.0)"] +gevent = ["gevent"] +mongodb = ["pymongo (>=3.0)"] +redis = ["redis (>=3.0)"] +rethinkdb = ["rethinkdb (>=2.4.0)"] +sqlalchemy = ["sqlalchemy (>=1.4)"] +test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6 ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "anyio (>=4.5.2)", "gevent ; python_version < \"3.14\"", "pytest", "pytz", "twisted ; python_version < \"3.14\""] +tornado = ["tornado (>=4.3)"] +twisted = ["twisted"] +zookeeper = ["kazoo"] + +[package.source] +type = "legacy" +url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" +reference = "tsinghua" + [[package]] name = "astroid" version = "3.3.9" @@ -257,12 +290,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "test", "unittest"] +groups = ["main", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\"", unittest = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [package.source] type = "legacy" @@ -478,7 +511,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" -groups = ["test", "unittest"] +groups = ["test"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -751,7 +784,7 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["test", "unittest"] +groups = ["test"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -790,7 +823,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" -groups = ["test", "unittest"] +groups = ["test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1127,7 +1160,7 @@ version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" -groups = ["test", "unittest"] +groups = ["test"] files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, @@ -1353,6 +1386,47 @@ type = "legacy" url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" reference = "tsinghua" +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" +reference = "tsinghua" + +[[package]] +name = "tzlocal" +version = "5.3.1" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, + {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[package.source] +type = "legacy" +url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" +reference = "tsinghua" + [[package]] name = "uvicorn" version = "0.34.2" @@ -1504,4 +1578,4 @@ reference = "tsinghua" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "e9ae6138753201327e7912765757cbc9d383c94d3efccbc522bd950c79627c3e" +content-hash = "4abe508cfcf687a225fa5b8394784ceddd6033f4c80f4b11827d6aa0d45cfc10" diff --git a/pyproject.toml b/pyproject.toml index 8731f13..bc61107 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,9 +15,14 @@ dependencies = [ "pydantic-settings (>=2.9.1,<3.0.0)", "pyyaml (>=6.0.2,<7.0.0)", "uvicorn (>=0.34.2,<0.35.0)", - "jinja2 (>=3.1.6,<4.0.0)" + "jinja2 (>=3.1.6,<4.0.0)", + "apscheduler (>=3.11.0,<4.0.0)" ] +[tool.poetry] +packages = [{include = "app"}] +package-mode = false + [[tool.poetry.source]] name = "tsinghua" url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" @@ -30,9 +35,6 @@ pytest-asyncio = "^0.26.0" pylint = "^3.3.7" -[tool.poetry.group.unittest.dependencies] -pytest = "^8.3.5" - [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api"