Skip to content

Feat: Ntfy integration (Draft/Future Use)#8818

Draft
CooperWang0912 wants to merge 6 commits into
AstrBotDevs:masterfrom
CooperWang0912:feat/Ntfy-Integration
Draft

Feat: Ntfy integration (Draft/Future Use)#8818
CooperWang0912 wants to merge 6 commits into
AstrBotDevs:masterfrom
CooperWang0912:feat/Ntfy-Integration

Conversation

@CooperWang0912

@CooperWang0912 CooperWang0912 commented Jun 16, 2026

Copy link
Copy Markdown

The feature is currently not needed for AstrBot

AstrBot目前不需要该功能

This is a draft PR for community feedback and future reference

该PR为草稿PR,主要目的为整合社区反馈和作为未来参考

I can mark the PR as ready if notified

如有需要可以通知我将PR改为正式PR

Summary / 总结

Added Ntfy as a lightweight messaging platform for easy interaction with the AI agent.
加入了更加轻量化的Ntfy作为与Agent交互的平台

Motivation / 改动原因

  • Easier Setup for Agents / 更轻量化的设置
  • Easier Integration Across Programs / 和其他程序更容易的整合

See Issue 8720 for more details
具体请见Issue 8720

Modifications / 改动点

  • Added Ntfy Platform Adapter, API, and Event Handler
  • 增加了Ntfy平台适配器,API转接口,以及事件处理
  • Configured Ntfy selector for the dashboard
  • 在控制面板上增加了Ntfy的适配
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

Manual Testing / 手动测试

  • Conversation and Memory Test / 普通对话与记忆测试
Conversation and Memory Test
  • Image Recognition Test / 视觉识别测试
IMG_6868
  • Successful MCP Server Tools Test / MCP服务与工具测试成功

Automated Testing Sequence / 自动测试

Passed / 通过

ruff format .: 438 files left unchanged
ruff check .: All checks passed!
uv run pytest -q tests/test_neo_sync.py: 2 passed in 6.37s
uv run pytest -q tests/test_neo_skill_tools.py: 1 passed in 3.76s
uv run pytest -q tests/test_skill_manager_sandbox_cache.py: 3 passed in 0.15s
uv run pytest -q tests/test_dashboard.py:test_neo_skills_routes: 1 passed in 12.24s

Failed / 失败

uv run pytest -q tests/test_computer_skill_sync.py: 2 failed, 3 passed in 4.20s

#####Cause / 原因

AssertionError: assert 'skills\skills.zip' == 'skills/skills.zip'
The error is caused by OS configuration / 报错由OS设置导致,与主程序无关


Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the ntfy platform adapter, enabling message notifications and streaming. It adds the core adapter, API client, and event handling logic, along with default configurations, i18n resources, and frontend assets. Feedback on the implementation highlights several critical improvements: encoding non-ASCII HTTP headers in send_file to prevent UnicodeEncodeError, prefixing configuration metadata keys with ntfy_ to resolve mismatches, safely handling None values during configuration retrieval, correcting a typo in the default server URL, and offloading synchronous file I/O to a thread pool to prevent blocking the async event loop.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +102 to +117
async def send_file(
self,
file_bytes: bytes,
filename: str,
message: str | None = None,
) -> bool:
"""Uploads a rich attachment asset via PUT binary stream."""
headers = {
**self.headers,
"X-Title": "AstrBot",
"X-Tags": "robot",
"X-Filename": filename,
}
if message:
headers["X-Message"] = message

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

send_file 中,X-FilenameX-Message 头部直接被赋予了可能包含非 ASCII 字符(如中文)的 filenamemessage。由于 aiohttp 默认使用 latin-1 编码 HTTP 头部,这会导致 UnicodeEncodeError 异常,从而使发送带中文文件名或中文消息的附件时彻底失败。根据 ntfy 官方文档,非 ASCII 头部必须使用 RFC 2047 进行 Base64 编码。建议在此处实现一个本地的 encode_header 辅助函数来处理这些头部。此外,作为新增加的附件处理功能,请确保为其编写相应的单元测试。

    async def send_file(
        self,
        file_bytes: bytes,
        filename: str,
        message: str | None = None,
    ) -> bool:
        """Uploads a rich attachment asset via PUT binary stream."""
        import base64
        def encode_header(val: str) -> str:
            try:
                val.encode('ascii')
                return val
            except UnicodeEncodeError:
                return f"=?utf-8?B?{base64.b64encode(val.encode('utf-8')).decode('ascii')}?="

        headers = {
            **self.headers,
            "X-Title": "AstrBot",
            "X-Tags": "robot",
            "X-Filename": encode_header(filename),
        }
        if message:
            headers["X-Message"] = encode_header(message)
References
  1. New functionality, such as handling attachments, should be accompanied by corresponding unit tests.

Comment thread astrbot/core/config/default.py Outdated
Comment on lines +1005 to +1019
"server_url": {
"description": "ntfy Server URL",
"type": "string",
"hint": "ntfy 服务器地址,例如 https://ntfy.sh 或您的自建实例地址。",
},
"topic": {
"description": "ntfy Topic",
"type": "string",
"hint": "用于收发消息的唯一订阅主题名称 (请确保其足够私密)。",
},
"access_token": {
"description": "Access Token (Optional)",
"type": "string",
"hint": "如果您的 ntfy 服务器开启了身份验证,请在此输入 Bearer Token。",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Ntfy 平台的配置元数据键名(server_urltopicaccess_token)与默认配置模板及适配器中实际读取的键名(ntfy_server_urlntfy_topicntfy_access_token)不一致。这会导致管理面板(Dashboard)无法正确绑定和保存这些配置项。建议将元数据中的键名统一加上 ntfy_ 前缀。

Suggested change
"server_url": {
"description": "ntfy Server URL",
"type": "string",
"hint": "ntfy 服务器地址,例如 https://ntfy.sh 或您的自建实例地址。",
},
"topic": {
"description": "ntfy Topic",
"type": "string",
"hint": "用于收发消息的唯一订阅主题名称 (请确保其足够私密)。",
},
"access_token": {
"description": "Access Token (Optional)",
"type": "string",
"hint": "如果您的 ntfy 服务器开启了身份验证,请在此输入 Bearer Token。",
},
"ntfy_server_url": {
"description": "ntfy Server URL",
"type": "string",
"hint": "ntfy 服务器地址,例如 https://ntfy.sh 或您的自建实例地址。",
},
"ntfy_topic": {
"description": "ntfy Topic",
"type": "string",
"hint": "用于收发消息的唯一订阅主题名称 (请确保其足够私密)。",
},
"ntfy_access_token": {
"description": "Access Token (Optional)",
"type": "string",
"hint": "如果您的 ntfy 服务器开启了身份验证,请在此输入 Bearer Token。",
},

Comment on lines +22 to +38
NTFY_CONFIG_METADATA = {
"server_url": {
"description": "ntfy Server URL",
"type": "string",
"hint": "ntfy 服务器地址,例如 https://ntfy.sh 或您的自建实例地址。",
},
"topic": {
"description": "ntfy Topic",
"type": "string",
"hint": "用于收发消息的唯一订阅主题名称 (请确保其足够私密)。",
},
"access_token": {
"description": "Access Token (Optional)",
"type": "string",
"hint": "如果您的 ntfy 服务器开启了身份验证,请在此输入 Bearer Token。",
},
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

同样地,NtfyPlatformAdapter 内部定义的 NTFY_CONFIG_METADATA 键名也缺少 ntfy_ 前缀,与实际读取的配置键名不符。建议在此处也统一加上 ntfy_ 前缀以保持一致。

Suggested change
NTFY_CONFIG_METADATA = {
"server_url": {
"description": "ntfy Server URL",
"type": "string",
"hint": "ntfy 服务器地址,例如 https://ntfy.sh 或您的自建实例地址。",
},
"topic": {
"description": "ntfy Topic",
"type": "string",
"hint": "用于收发消息的唯一订阅主题名称 (请确保其足够私密)。",
},
"access_token": {
"description": "Access Token (Optional)",
"type": "string",
"hint": "如果您的 ntfy 服务器开启了身份验证,请在此输入 Bearer Token。",
},
}
NTFY_CONFIG_METADATA = {
"ntfy_server_url": {
"description": "ntfy Server URL",
"type": "string",
"hint": "ntfy 服务器地址,例如 https://ntfy.sh 或您的自建实例地址。",
},
"ntfy_topic": {
"description": "ntfy Topic",
"type": "string",
"hint": "用于收发消息的唯一订阅主题名称 (请确保其足够私密)。",
},
"ntfy_access_token": {
"description": "Access Token (Optional)",
"type": "string",
"hint": "如果您的 ntfy 服务器开启了身份验证,请在此输入 Bearer Token。",
},
}

Comment on lines +91 to +95
server_url = str(
platform_config.get("ntfy_server_url", "https://ntfy.sh")
).strip()
topic = str(platform_config.get("ntfy_topic", "")).strip()
access_token = str(platform_config.get("ntfy_access_token", "")).strip()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

如果 platform_config 中对应的配置项值为 None(例如在 JSON 配置中显式设为 null),直接使用 str(platform_config.get(..., default)) 会将 None 转换为字符串 "None",从而导致错误的请求 URL 或 Token。建议使用 or 运算符进行安全的空值兜底。

Suggested change
server_url = str(
platform_config.get("ntfy_server_url", "https://ntfy.sh")
).strip()
topic = str(platform_config.get("ntfy_topic", "")).strip()
access_token = str(platform_config.get("ntfy_access_token", "")).strip()
server_url = str(
platform_config.get("ntfy_server_url") or "https://ntfy.sh"
).strip()
topic = str(platform_config.get("ntfy_topic") or "").strip()
access_token = str(platform_config.get("ntfy_access_token") or "").strip()

Comment thread astrbot/core/config/default.py Outdated
"id": "ntfy",
"type": "ntfy",
"enable": True,
"ntfy_server_url": "https:ntfy.sh",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

"ntfy_server_url" 的默认值 "https:ntfy.sh" 缺少双斜杠 "//",应修正为 "https://ntfy.sh"。

Suggested change
"ntfy_server_url": "https:ntfy.sh",
"ntfy_server_url": "https://ntfy.sh",

Comment on lines +53 to +57
filename = getattr(
file_component, "name", None
) or os.path.basename(file_path)
with open(file_path, "rb") as f:
file_bytes = f.read()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在异步事件处理函数中,使用原生的 open().read() 进行同步 file I/O 读取会阻塞整个 asyncio 事件循环,导致其他并发任务(如其他用户的消息处理)出现明显的延迟。建议使用 asyncio.to_thread 将文件读取操作放到线程池中执行。

                    filename = getattr(
                        file_component, "name", None
                    ) or os.path.basename(file_path)
                    def read_file():
                        with open(file_path, "rb") as f:
                            return f.read()
                    file_bytes = await asyncio.to_thread(read_file)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant