commit 9f1ee9ffe63cb202800285a14e7a218645ad3c9f Author: Hermes Agent Date: Fri May 8 23:54:03 2026 +0800 feat: publish dbussmsforward refactor diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f664c4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Python +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ +venv/ +dist/ +build/ +*.egg-info/ + +# .NET +bin/ +obj/ +TestResults/ +*.user +*.suo + +# Runtime/config secrets +*.log +.env +appsettings.local.json +appsettings.*.local.json +run-records/ +.runtime/ + +# macOS +.DS_Store diff --git a/.hermes/plans/2026-04-24_192822-dbussmsforward-debian-status-and-checklist.md b/.hermes/plans/2026-04-24_192822-dbussmsforward-debian-status-and-checklist.md new file mode 100644 index 0000000..8c7f97c --- /dev/null +++ b/.hermes/plans/2026-04-24_192822-dbussmsforward-debian-status-and-checklist.md @@ -0,0 +1,531 @@ +# DbusSmsForward Debian 重构状态报告与下一步落地清单 + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** 基于当前仓库实际状态,给出一份 Debian-first 的真实重构进度报告,并列出后续最小可落地的执行清单,避免再把方向带回 OpenWRT。 + +**Architecture:** 当前仓库处于双轨状态:保留 C# Worker 主线,同时新增 Python 重构线。Python 线已经形成可测试的运行时核心,但 Web 管理层和 Debian 交付闭环尚未完成;C# 线则有 Debian 友好的部分配置改造,但还不是完整的 Web 化交付形态。 + +**Tech Stack:** Python 3.11、unittest、CLI runtime、ModemManager/DBus 抽象、C# .NET 8 Worker、Systemd-oriented configuration + +--- + +## 一、结论摘要 + +### 1. 当前方向应明确为 Debian-first +本项目当前应围绕: +- Debian 运行 +- ModemManager/DBus 短信接收 +- Web 管理 +- 不要求在 Debian 目标机上编译源码 + +不应再把“下一步推进目标”描述为 OpenWRT 部署。 + +### 2. 当前重构不是没进展,但也还没到 Debian Web 成品 +仓库里的 Python 重构线已经完成了“核心运行层”的大量工作: +- 配置模型 +- 短信来源抽象 +- 文件源/ModemManager 源 +- sender 分发 +- AutmanPush 错误结构化 +- 轮询、去重、fetch issue 统计 +- CLI 基本闭环 +- 自动化测试覆盖 + +但 **Debian Web 管理版本身尚未完成**,因为还缺: +- HTTP 服务入口 +- Web API +- 前端页面 +- 配置读写 API +- 状态 API +- systemd/service 交付文件 +- Debian 部署说明与目录规范 + +### 3. C# 主线仍存在,但并非当前可直接交付的 Debian Web 版本 +C# 线保留了较多历史功能与 Debian 友好的配置思路,但从当前文件状态看,它仍然是 Worker 应用,不是完整 Web 项目,且存在引用类与当前可见文件状态不完全一致的问题,因此不适合作为“已完成的 Debian Web 版”来对外描述。 + +--- + +## 二、实际检查到的仓库状态 + +仓库路径:`/Users/chick/.Hermes/workspace/DbusSmsForward` + +### 目录结构观察 +- C# 主线目录:`DbusTest/` +- Python 重构目录:`src/dbussmsforward/` +- Python 测试目录:`tests/` +- README:`README.md` + +### 粗略文件数量 +- `DbusTest/` 文件数:35 +- `src/dbussmsforward/` 文件数:14 +- `tests/` 文件数:9 + +这说明仓库现在不是“只剩一个新实现”,而是明确的双轨并存状态。 + +--- + +## 三、Python 重构线:已完成内容 + +### 1. 配置模型已成型 +已检查文件: +- `src/dbussmsforward/config.py` + +已具备: +- `DaemonConfig` +- `ForwardingConfig` +- `AutmanPushConfig` +- `SmsSourceConfig` +- `NotificationChannels` +- `AppConfig` +- legacy `appSettings/appsettings` 到新模型的兼容映射 + +说明:Python 线已经不是零散脚本,而是具备明确配置边界的可持续实现。 + +### 2. 短信源抽象已成型 +已检查文件: +- `src/dbussmsforward/source.py` + +已具备: +- `FileSmsSource` +- `ModemManagerSmsSource` +- `PythonDbusModemManagerProxy` +- `FetchIssue` + +说明: +- `modemmanager` source 是 Debian-first 能力,不是 OpenWRT 专属。 +- 当前 Python 线已经具备在 Debian + ModemManager 环境继续推进的正确技术基础。 + +### 3. 通知分发核心已成型 +已检查文件: +- `src/dbussmsforward/notifications.py` +- `src/dbussmsforward/app.py` + +已具备: +- console sender +- AutmanPush sender +- `DispatchOutcome` +- `SenderDispatchResult` +- sender 级别失败记录 +- HTTP/timeout/network/unknown 分类 +- HTTP 响应片段摘录 +- partial success 保留 +- dedupe +- 轮询统计 +- fetch issue 聚合 + +### 4. CLI 基本闭环已经存在 +已检查文件: +- `src/dbussmsforward/cli.py` + +当前命令: +- `run` +- `send` +- `validate-config` +- `print-sample-config` + +说明: +- 这已经满足“核心运行原型”的最低闭环。 +- 但还不是 Web 管理闭环。 + +--- + +## 四、Python 重构线:验证状态 + +### 1. CLI 测试 +命令: +```bash +PYTHONPATH=src python3 -m unittest tests.test_cli -v +``` +结果:10/10 通过 + +### 2. 运行时流程测试 +命令: +```bash +PYTHONPATH=src python3 -m unittest tests.test_runtime_flow -v +``` +结果:16/16 通过 + +### 3. 配置模型测试 +命令: +```bash +PYTHONPATH=src python3 -m unittest tests.test_config_model -v +``` +结果:4/4 通过 + +### 4. 关于“全量测试”口径的注意事项 +如果直接运行: +```bash +python3 -m unittest discover -s tests -v +``` +会失败,原因不是测试逻辑本身坏了,而是当前仓库还没有把 `PYTHONPATH=src` 或等价安装流程固化到默认测试入口里,导致: +- `test_config_model` +- `test_runtime_flow` +在 discover 模式下导入 `dbussmsforward` 失败 + +这说明: +- **测试内容本身大体可用** +- **但仓库级测试入口还没标准化** + +这属于 Debian 交付前必须补齐的一环。 + +--- + +## 五、C# 主线:当前真实状态 + +### 1. 仍是 Worker,不是 Web 项目 +已检查文件: +- `DbusTest/DbusSmsForward.csproj` + +当前项目 SDK: +```xml + +``` + +说明: +- 这不是 `Microsoft.NET.Sdk.Web` +- 当前 C# 主线不能被称为“已完成 Web 化” + +### 2. 有 Debian 友好的配置痕迹 +已检查文件: +- `DbusTest/Helper/ConfigHelper.cs` + +确认存在: +- `/etc/dbussmsforward/appsettings.json` 的优先加载路径 +- `DBUSSMSFORWARD_CONFIG_PATH` 环境变量支持 +- `SaveOptions` / `LoadOptions` +- 旧配置与新配置转换 + +说明: +- C# 线并非完全无效,已经有面向 Debian 服务运行的配置设计。 +- 但这只是“服务化/配置化能力”,不是完整 Web 管理版。 + +### 3. Program.cs 依赖的若干类当前未直接看到实现文件 +在 `DbusTest/Program.cs` 中可见引用: +- `SampleConfigFactory` +- `LegacyConfigurationBridge` +- `CommandLineConfigurationApplier` +- `ConfigurationValidator` + +但本轮目录巡检中未直接发现对应文件,说明至少有以下一种情况: +- 文件缺失 +- 尚未提交完整 +- 放在当前未显式识别的位置 +- 或重构过程中断过 + +因此,**当前不应把 C# 主线描述为“已稳定可构建的 Debian Web 版”。** + +### 4. README 仍是旧交付叙事 +已检查文件: +- `README.md` + +当前 README 仍主要描述: +- 直接运行旧二进制 +- `-fE/-fP/-fW/...` 旧参数模式 +- Native AOT 交叉编译 + +没有覆盖: +- Python 重构线的运行方式 +- Debian-first Web 管理目标 +- 统一部署方式 +- systemd 落地方式 + +这意味着: +- 仓库文档叙事仍停留在旧时代 +- 这会持续误导后续判断与交付方向 + +--- + +## 六、当前最准确的项目分层判断 + +### 已完成层 +1. Python 核心配置模型 +2. Python 短信源抽象(含 ModemManager) +3. Python 通知分发运行时 +4. Python CLI 基本闭环 +5. 单元/流程级测试覆盖的大部分核心行为 + +### 半完成层 +1. C# Debian 服务化配置思路 +2. legacy -> 新配置兼容桥接思路 +3. 多通知渠道的旧实现保留 + +### 明显未完成层 +1. Debian Web API +2. Debian Web 前端 +3. Web 管理配置读写 +4. 服务状态查看接口 +5. 消息查看/发送 API +6. systemd/service 交付物 +7. Debian 安装/升级/目录规范 +8. 统一测试入口 +9. README/部署文档重写 + +--- + +## 七、建议的方向收敛 + +### 建议主路线 +**以 Python 线作为 Debian-first 主实现继续推进。** + +原因: +1. Python 线目前已经具备可测试、可迭代的核心运行时 +2. 已有 ModemManager 抽象,技术方向与 Debian 目标一致 +3. 当前 Web 化缺口更适合作为 Python 层增量补齐 +4. C# 线当前更适合做“参考/兼容来源”,不适合作为当前主交付叙事 + +### C# 线建议定位 +短期内将 C# 线定义为: +- 历史主线 +- 兼容配置与行为参考来源 +- 功能映射对照对象 + +而不是当前 Debian Web 交付主线。 + +--- + +## 八、下一步落地清单(按优先级排序) + +下面这份清单按“最小可落地 Debian Web 版”来排,不再发散。 + +### 阶段 0:先把工程入口和叙事统一(高优先级,半天内可做) + +#### Task 0.1:补统一测试入口 +**Objective:** 让仓库级测试命令不依赖手工写 `PYTHONPATH=src` + +**Files:** +- Create: `pyproject.toml` 或 `pytest.ini` / `tox.ini`(择一) +- Optionally modify: `README.md` + +**Expected outcome:** +以下命令之一可直接工作: +```bash +python3 -m unittest discover -s tests -v +``` +或 +```bash +python3 -m pytest -q +``` + +#### Task 0.2:补 Python 运行入口说明 +**Objective:** 把当前 Python 线的实际运行方式写清楚 + +**Files:** +- Modify: `README.md` + +**Must include:** +- Python CLI 启动方式 +- `PYTHONPATH`/安装方式 +- `validate-config` / `run` / `send` +- file source 与 modemmanager source 的示例配置 + +#### Task 0.3:明确仓库主线状态说明 +**Objective:** 在 README 顶部写明当前仓库是 C# 历史主线 + Python Debian 重构线并存 + +**Files:** +- Modify: `README.md` + +**Must include:** +- 当前推荐继续投入的是 Python Debian-first 线 +- C# 线主要用于兼容参考,不宣称已完成 Web 化 + +--- + +### 阶段 1:补最小 Debian Web API(最高业务优先级) + +#### Task 1.1:新增 Web 服务入口模块 +**Objective:** 在 Python 线增加最小 HTTP 服务,而不破坏 CLI + +**Files:** +- Create: `src/dbussmsforward/web.py` +- Modify: `src/dbussmsforward/cli.py` +- Modify: `src/dbussmsforward/config.py` + +**API baseline:** +- `GET /api/health` +- `GET /api/system/status` +- `GET /api/settings` +- `POST /api/settings` +- `POST /api/sms/send` + +#### Task 1.2:补 Web 层测试 +**Files:** +- Create: `tests/test_web.py` + +**Validation:** +- 健康检查返回 200 +- 设置读取/写入生效 +- send API 能触发 dispatcher +- 错误配置返回结构化 4xx + +#### Task 1.3:定义配置文件持久化位置 +**Objective:** 对齐 Debian 运行约定 + +**Files:** +- Modify: `src/dbussmsforward/cli.py` +- Modify: `src/dbussmsforward/web.py` +- Create: `appsettings.example.json` + +**Recommended path order:** +1. `--config` +2. `DBUSSMSFORWARD_CONFIG_PATH` +3. `/etc/dbussmsforward/appsettings.json` +4. 项目内默认示例路径 + +--- + +### 阶段 2:补最小前端管理页(次高优先级) + +#### Task 2.1:加纯静态前端 +**Files:** +- Create: `wwwroot/index.html` +- Create: `wwwroot/app.js` +- Create: `wwwroot/app.css` + +**UI minimum scope:** +- 当前运行状态 +- 当前配置查看 +- 基础配置编辑保存 +- 手工发送短信/测试通知 + +#### Task 2.2:把前端挂到 Web 服务 +**Files:** +- Modify: `src/dbussmsforward/web.py` + +**Validation:** +- 浏览器可直接打开管理页 +- 前端能调用 `settings` / `status` / `send` + +--- + +### 阶段 3:补 Debian 部署闭环 + +#### Task 3.1:新增 systemd service 文件 +**Files:** +- Create: `deploy/dbussmsforward.service` + +**Must include:** +- `Environment=DBUSSMSFORWARD_CONFIG_PATH=/etc/dbussmsforward/appsettings.json` +- `WorkingDirectory=/opt/dbussmsforward` +- `ExecStart=` 指向 Python 启动方式或打包后的启动方式 +- `Restart=always` + +#### Task 3.2:新增 Debian 部署说明 +**Files:** +- Modify: `README.md` +- Optionally create: `docs/debian-deploy.md` + +**Must include:** +- 依赖安装 +- 目录布局 +- 配置文件位置 +- systemd 安装与启动 +- 日志查看方式 +- 升级步骤 + +#### Task 3.3:明确“不在 Debian 目标机编译”的交付方式 +**推荐选项二选一:** +1. Python wheel / source bundle + venv 安装 +2. 打包好的独立运行目录(含依赖) + +**建议优先:** Python venv + requirements/pip install 方案,先求稳。 + +--- + +### 阶段 4:补运维与可观察性 + +#### Task 4.1:统一日志输出格式 +**Files:** +- Modify: `src/dbussmsforward/cli.py` +- Modify: `src/dbussmsforward/app.py` +- Modify: `src/dbussmsforward/web.py` + +**Need:** +- 标准 INFO/WARN/ERROR +- sender failure / fetch issue 结构化输出 +- Web API 错误统一 JSON + +#### Task 4.2:补状态页指标 +**API suggested:** +- 最近一次 poll 时间 +- 最近 dispatch failures 数量 +- 最近 fetch issues +- 启用的通知渠道 +- 当前 source kind + +#### Task 4.3:补消息历史持久化(可选) +这项不是第一阶段必须,但如果要做 Web 查看“最近短信”,就需要: +- SQLite 或轻量 JSONL 存储 +- 最近 N 条消息与失败记录查询 + +建议放到 Web API 稳定之后再做。 + +--- + +## 九、推荐执行顺序(最务实版本) + +### 最小 4 步路线 +1. **统一测试入口 + README 重写** +2. **补 Python Web API 最小闭环** +3. **补静态前端最小页面** +4. **补 systemd + Debian 部署说明** + +这样完成后,项目才能第一次比较诚实地对外说: + +> 已具备 Debian-first 的可运行 Web 管理版本雏形。 + +--- + +## 十、风险与注意事项 + +### 风险 1:双轨并存会继续制造误判 +如果不尽快在 README 和目录说明里明确: +- 哪条线是当前主线 +- 哪条线只是历史参考 + +后续任何人都可能再次把方向带偏。 + +### 风险 2:测试入口不统一会导致“看起来坏了” +目前 `unittest discover` 直接跑会误报导入失败,这会让人误以为 Python 重构质量不稳定。 + +### 风险 3:如果先补太多高级功能,会拖慢最小交付 +比如: +- 历史消息数据库 +- 权限系统 +- 多用户管理 +- 完整前端框架重构 + +这些都不该排在 Debian 最小可交付之前。 + +--- + +## 十一、最终建议 + +### 推荐的下一动作 +直接进入: + +**Phase 0 + Phase 1** +- 统一测试入口 +- 重写 README 的 Debian-first 叙事 +- 增加最小 Web API + +这是最短路径,也最能避免继续空转。 + +### 不建议的动作 +- 继续把目标描述为 OpenWRT 部署 +- 先做复杂前端重构 +- 先追求完整数据库历史功能 +- 继续把 C# Worker 当成当前 Web 主交付线 + +--- + +## 十二、执行准备说明 + +如果下一步进入实现,建议采用以下节奏: + +1. 先修测试入口与 README +2. 再加 `web.py` 和 `tests/test_web.py` +3. API 稳定后再加 `wwwroot/` +4. 最后补 `deploy/dbussmsforward.service` + +这样每一步都可验证,也不会再跑偏。 diff --git a/DbusSmsForward.sln b/DbusSmsForward.sln new file mode 100644 index 0000000..1547266 --- /dev/null +++ b/DbusSmsForward.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33530.505 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DbusSmsForward", "DbusTest\DbusSmsForward.csproj", "{FDF431F9-11F0-4EEC-AE1A-72A875087986}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FDF431F9-11F0-4EEC-AE1A-72A875087986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDF431F9-11F0-4EEC-AE1A-72A875087986}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDF431F9-11F0-4EEC-AE1A-72A875087986}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDF431F9-11F0-4EEC-AE1A-72A875087986}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {98BDE03E-0B50-4652-8111-3B1076B339A2} + EndGlobalSection +EndGlobal diff --git a/DbusTest/DbusSmsForward.csproj b/DbusTest/DbusSmsForward.csproj new file mode 100644 index 0000000..f415a59 --- /dev/null +++ b/DbusTest/DbusSmsForward.csproj @@ -0,0 +1,34 @@ + + + + Exe + net8.0 + enable + enable + true + DbusSmsForward + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/DbusTest/Helper/ConfigHelper.cs b/DbusTest/Helper/ConfigHelper.cs new file mode 100644 index 0000000..23690ce --- /dev/null +++ b/DbusTest/Helper/ConfigHelper.cs @@ -0,0 +1,208 @@ +using DbusSmsForward.SettingModel; +using Microsoft.Extensions.Configuration; +using System.Net; +using System.Text.Json; + +namespace DbusSmsForward.Helper; + +public static class ConfigHelper +{ + private static readonly JsonSerializerOptions JsonOptions = new(SourceGenerationContext.Default.Options) + { + PropertyNameCaseInsensitive = true, + WriteIndented = true + }; + + public static string DefaultSettingsFileName => Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + + public static string ResolveConfigPath(IConfiguration? configuration = null) + { + string[] candidates = + [ + configuration?["DBUSSMSFORWARD_CONFIG_PATH"] ?? string.Empty, + Environment.GetEnvironmentVariable("DBUSSMSFORWARD_CONFIG_PATH") ?? string.Empty, + "/etc/dbussmsforward/appsettings.json", + DefaultSettingsFileName + ]; + + foreach (string candidate in candidates) + { + if (!string.IsNullOrWhiteSpace(candidate) && File.Exists(candidate)) + { + return candidate; + } + } + + return candidates.Last(static c => !string.IsNullOrWhiteSpace(c)); + } + + public static DbusSmsForwardOptions LoadOptions(IConfiguration? configuration = null) + { + DbusSmsForwardOptions options = new(); + string configPath = ResolveConfigPath(configuration); + if (File.Exists(configPath)) + { + string json = File.ReadAllText(configPath); + options = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.DbusSmsForwardOptions) ?? new DbusSmsForwardOptions(); + } + + if (configuration is not null) + { + configuration.GetSection("DbusSmsForward").Bind(options); + configuration.GetSection("Daemon").Bind(options.Daemon); + configuration.GetSection("Modem").Bind(options.Modem); + configuration.GetSection("Forwarding").Bind(options.Forwarding); + configuration.GetSection("Notifications").Bind(options.Notifications); + configuration.GetSection("LegacyCompatibility").Bind(options.LegacyCompatibility); + } + + return options; + } + + public static void SaveOptions(DbusSmsForwardOptions options, string? path = null) + { + string target = path ?? ResolveConfigPath(); + Directory.CreateDirectory(Path.GetDirectoryName(target) ?? AppContext.BaseDirectory); + string updatedJson = JsonSerializer.Serialize(options, SourceGenerationContext.Default.DbusSmsForwardOptions); + File.WriteAllText(target, updatedJson); + } + + public static void SaveLegacySettings(appsettingsModel result, string? path = null) + { + string target = path ?? DefaultSettingsFileName; + Directory.CreateDirectory(Path.GetDirectoryName(target) ?? AppContext.BaseDirectory); + string updatedJson = JsonSerializer.Serialize(result, SourceGenerationContext.Default.appsettingsModel); + File.WriteAllText(target, updatedJson); + } + + public static void GetSettings(ref appsettingsModel result) + { + string settingsFileName = ResolveConfigPath(); + if (!File.Exists(settingsFileName)) + { + result ??= new appsettingsModel(); + return; + } + + string config = File.ReadAllText(settingsFileName); + + try + { + if (LooksLikeNewConfig(config)) + { + DbusSmsForwardOptions options = JsonSerializer.Deserialize(config, SourceGenerationContext.Default.DbusSmsForwardOptions) ?? new DbusSmsForwardOptions(); + result = ConvertToLegacy(options); + return; + } + + result = JsonSerializer.Deserialize(config, SourceGenerationContext.Default.appsettingsModel) ?? new appsettingsModel(); + } + catch + { + result ??= new appsettingsModel(); + } + } + + public static void UpdateSettings(ref appsettingsModel result) + { + SaveLegacySettings(result); + } + + public static bool JudgeIsForwardIgnore(uint storage) + { + appsettingsModel result = new(); + GetSettings(ref result); + string forwardStorageType = result.appSettings.ForwardIgnoreStorageType?.Trim().ToLowerInvariant() ?? string.Empty; + + return (forwardStorageType, storage) switch + { + ("unknown", 0) => true, + ("sm", 1) => true, + ("me", 2) => true, + ("mt", 3) => true, + ("sr", 4) => true, + ("bm", 5) => true, + ("ta", 6) => true, + _ => false + }; + } + + public static string GetDeviceHostName() + { + try + { + return Dns.GetHostName(); + } + catch + { + try + { + return Environment.MachineName; + } + catch + { + return string.Empty; + } + } + } + + public static string ResolveDeviceName(DbusSmsForwardOptions options) + { + if (!string.IsNullOrWhiteSpace(options.Forwarding.DeviceName)) + { + return options.Forwarding.DeviceName; + } + + return GetDeviceHostName(); + } + + public static appsettingsModel ConvertToLegacy(DbusSmsForwardOptions options) + { + return new appsettingsModel + { + appSettings = new LegacyAppSettings + { + EmailConfig = options.Notifications.Email, + PushPlusConfig = options.Notifications.PushPlus, + DingTalkConfig = options.Notifications.DingTalk, + BarkConfig = options.Notifications.Bark, + WeComApplicationConfig = options.Notifications.WeComApplication, + TGBotConfig = options.Notifications.TelegramBot, + ShellConfig = options.Notifications.Shell, + SmsCodeKey = options.Forwarding.SmsCodeKey, + ForwardIgnoreStorageType = options.Forwarding.IgnoreStorageType, + DeviceName = options.Forwarding.DeviceName + } + }; + } + + public static DbusSmsForwardOptions ConvertFromLegacy(appsettingsModel legacy) + { + return new DbusSmsForwardOptions + { + Forwarding = new ForwardingConfig + { + DeviceName = legacy.appSettings.DeviceName, + SmsCodeKey = legacy.appSettings.SmsCodeKey, + IgnoreStorageType = legacy.appSettings.ForwardIgnoreStorageType + }, + Notifications = new NotificationChannels + { + Email = legacy.appSettings.EmailConfig, + PushPlus = legacy.appSettings.PushPlusConfig, + DingTalk = legacy.appSettings.DingTalkConfig, + Bark = legacy.appSettings.BarkConfig, + WeComApplication = legacy.appSettings.WeComApplicationConfig, + TelegramBot = legacy.appSettings.TGBotConfig, + Shell = legacy.appSettings.ShellConfig + } + }; + } + + private static bool LooksLikeNewConfig(string json) + { + using JsonDocument document = JsonDocument.Parse(json); + JsonElement root = document.RootElement; + return root.TryGetProperty("Daemon", out _) || root.TryGetProperty("Notifications", out _) || root.TryGetProperty("Forwarding", out _); + } +} diff --git a/DbusTest/Helper/HttpHelper.cs b/DbusTest/Helper/HttpHelper.cs new file mode 100644 index 0000000..3e640a0 --- /dev/null +++ b/DbusTest/Helper/HttpHelper.cs @@ -0,0 +1,45 @@ +using System.Net.Http.Json; +using System.Text; +using System.Text.Json.Nodes; + +namespace DbusSmsForward.Helper; + +public static class HttpHelper +{ + public static string HttpGet(string url) + => HttpGetAsync(url).GetAwaiter().GetResult(); + + public static async Task HttpGetAsync(string url, bool skipTlsValidation = false, CancellationToken cancellationToken = default) + { + using HttpClient client = CreateClient(skipTlsValidation); + using HttpResponseMessage response = await client.GetAsync(url, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(cancellationToken); + } + + public static string Post(string url, JsonObject obj) + => PostAsync(url, obj).GetAwaiter().GetResult(); + + public static async Task PostAsync(string url, JsonObject obj, bool skipTlsValidation = false, CancellationToken cancellationToken = default) + { + using HttpClient client = CreateClient(skipTlsValidation); + using StringContent content = new(obj.ToJsonString(), Encoding.UTF8, "application/json"); + using HttpResponseMessage response = await client.PostAsync(url, content, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(cancellationToken); + } + + private static HttpClient CreateClient(bool skipTlsValidation) + { + HttpClientHandler handler = new(); + if (skipTlsValidation) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + + return new HttpClient(handler) + { + Timeout = TimeSpan.FromSeconds(30) + }; + } +} diff --git a/DbusTest/Models/AppCommand.cs b/DbusTest/Models/AppCommand.cs new file mode 100644 index 0000000..ff71ebc --- /dev/null +++ b/DbusTest/Models/AppCommand.cs @@ -0,0 +1,11 @@ +namespace DbusSmsForward.Models; + +public sealed class AppCommand +{ + public string Name { get; init; } = "run"; + public bool PrintSampleConfig { get; init; } + public bool ValidateConfig { get; init; } + public string PhoneNumber { get; init; } = string.Empty; + public string Message { get; init; } = string.Empty; + public string[] EnabledChannels { get; init; } = []; +} diff --git a/DbusTest/ModemManager1.DBus.cs b/DbusTest/ModemManager1.DBus.cs new file mode 100644 index 0000000..824ae82 --- /dev/null +++ b/DbusTest/ModemManager1.DBus.cs @@ -0,0 +1,5837 @@ +namespace ModemManager1.DBus +{ + using System; + using Tmds.DBus.Protocol; + using System.Collections.Generic; + using System.Threading.Tasks; + using Tmds.DBus; + + partial class ObjectManager : ModemManager1Object + { + private const string __Interface = "org.freedesktop.DBus.ObjectManager"; + public ObjectManager(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task>>> GetManagedObjectsAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_aeoaesaesv(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "GetManagedObjects"); + return writer.CreateMessage(); + } + } + public ValueTask WatchInterfacesAddedAsync(Action> InterfacesAndProperties)> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "InterfacesAdded", (Message m, object? s) => ReadMessage_oaesaesv(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + public ValueTask WatchInterfacesRemovedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "InterfacesRemoved", (Message m, object? s) => ReadMessage_oas(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + } + record ModemManager1Properties + { + public string Version { get; set; } = default!; + } + partial class ModemManager1 : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1"; + public ModemManager1(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task ScanDevicesAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "ScanDevices"); + return writer.CreateMessage(); + } + } + public Task SetLoggingAsync(string level) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "s", + member: "SetLogging"); + writer.WriteString(level); + return writer.CreateMessage(); + } + } + public Task ReportKernelEventAsync(Dictionary properties) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "ReportKernelEvent"); + writer.WriteDictionary(properties); + return writer.CreateMessage(); + } + } + public Task InhibitDeviceAsync(string uid, bool inhibit) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "sb", + member: "InhibitDevice"); + writer.WriteString(uid); + writer.WriteBool(inhibit); + return writer.CreateMessage(); + } + } + public Task SetVersionAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Version"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task GetVersionAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Version"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static ModemManager1Properties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Version": invalidated.Add("Version"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static ModemManager1Properties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new ModemManager1Properties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Version": + reader.ReadSignature("s"); + props.Version = reader.ReadString(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record LocationProperties + { + public uint Capabilities { get; set; } = default!; + public uint SupportedAssistanceData { get; set; } = default!; + public uint Enabled { get; set; } = default!; + public bool SignalsLocation { get; set; } = default!; + public Dictionary Location { get; set; } = default!; + public string SuplServer { get; set; } = default!; + public string[] AssistanceDataServers { get; set; } = default!; + public uint GpsRefreshRate { get; set; } = default!; + } + partial class Location : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Location"; + public Location(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task SetupAsync(uint sources, bool signalLocation) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "ub", + member: "Setup"); + writer.WriteUInt32(sources); + writer.WriteBool(signalLocation); + return writer.CreateMessage(); + } + } + public Task> GetLocationAsyncMethod1() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_aeuv(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "GetLocation"); + return writer.CreateMessage(); + } + } + public Task SetSuplServerAsyncMethod1(string supl) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "s", + member: "SetSuplServer"); + writer.WriteString(supl); + return writer.CreateMessage(); + } + } + public Task InjectAssistanceDataAsync(byte[] data) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "ay", + member: "InjectAssistanceData"); + writer.WriteArray(data); + return writer.CreateMessage(); + } + } + public Task SetGpsRefreshRateAsyncMethod1(uint rate) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "SetGpsRefreshRate"); + writer.WriteUInt32(rate); + return writer.CreateMessage(); + } + } + public Task SetCapabilitiesAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Capabilities"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetSupportedAssistanceDataAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SupportedAssistanceData"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetEnabledAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Enabled"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetSignalsLocationAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SignalsLocation"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetLocationAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Location"); + writer.WriteSignature("a{uv}"); + WriteType_aeuv(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetSuplServerAsyncMethod(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SuplServer"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetAssistanceDataServersAsync(string[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("AssistanceDataServers"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetGpsRefreshRateAsyncMethod2(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("GpsRefreshRate"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task GetCapabilitiesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Capabilities"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetSupportedAssistanceDataAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SupportedAssistanceData"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetEnabledAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Enabled"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetSignalsLocationAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SignalsLocation"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task> GetLocationAsyncMethod2() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Location"), (Message m, object? s) => ReadMessage_v_aeuv(m, (ModemManager1Object)s!), this); + public Task GetSuplServerAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SuplServer"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetAssistanceDataServersAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "AssistanceDataServers"), (Message m, object? s) => ReadMessage_v_as(m, (ModemManager1Object)s!), this); + public Task GetGpsRefreshRateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "GpsRefreshRate"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static LocationProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Capabilities": invalidated.Add("Capabilities"); break; + case "SupportedAssistanceData": invalidated.Add("SupportedAssistanceData"); break; + case "Enabled": invalidated.Add("Enabled"); break; + case "SignalsLocation": invalidated.Add("SignalsLocation"); break; + case "Location": invalidated.Add("Location"); break; + case "SuplServer": invalidated.Add("SuplServer"); break; + case "AssistanceDataServers": invalidated.Add("AssistanceDataServers"); break; + case "GpsRefreshRate": invalidated.Add("GpsRefreshRate"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static LocationProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new LocationProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Capabilities": + reader.ReadSignature("u"); + props.Capabilities = reader.ReadUInt32(); + changedList?.Add("Capabilities"); + break; + case "SupportedAssistanceData": + reader.ReadSignature("u"); + props.SupportedAssistanceData = reader.ReadUInt32(); + changedList?.Add("SupportedAssistanceData"); + break; + case "Enabled": + reader.ReadSignature("u"); + props.Enabled = reader.ReadUInt32(); + changedList?.Add("Enabled"); + break; + case "SignalsLocation": + reader.ReadSignature("b"); + props.SignalsLocation = reader.ReadBool(); + changedList?.Add("SignalsLocation"); + break; + case "Location": + reader.ReadSignature("a{uv}"); + props.Location = ReadType_aeuv(ref reader); + changedList?.Add("Location"); + break; + case "SuplServer": + reader.ReadSignature("s"); + props.SuplServer = reader.ReadString(); + changedList?.Add("SuplServer"); + break; + case "AssistanceDataServers": + reader.ReadSignature("as"); + props.AssistanceDataServers = reader.ReadArrayOfString(); + changedList?.Add("AssistanceDataServers"); + break; + case "GpsRefreshRate": + reader.ReadSignature("u"); + props.GpsRefreshRate = reader.ReadUInt32(); + changedList?.Add("GpsRefreshRate"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record SignalProperties + { + public uint Rate { get; set; } = default!; + public uint RssiThreshold { get; set; } = default!; + public bool ErrorRateThreshold { get; set; } = default!; + public Dictionary Cdma { get; set; } = default!; + public Dictionary Evdo { get; set; } = default!; + public Dictionary Gsm { get; set; } = default!; + public Dictionary Umts { get; set; } = default!; + public Dictionary Lte { get; set; } = default!; + public Dictionary Nr5g { get; set; } = default!; + } + partial class Signal : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Signal"; + public Signal(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task SetupAsync(uint rate) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "Setup"); + writer.WriteUInt32(rate); + return writer.CreateMessage(); + } + } + public Task SetupThresholdsAsync(Dictionary settings) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "SetupThresholds"); + writer.WriteDictionary(settings); + return writer.CreateMessage(); + } + } + public Task SetRateAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Rate"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetRssiThresholdAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("RssiThreshold"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetErrorRateThresholdAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("ErrorRateThreshold"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetCdmaAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Cdma"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetEvdoAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Evdo"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetGsmAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Gsm"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetUmtsAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Umts"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetLteAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Lte"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetNr5gAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Nr5g"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task GetRateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Rate"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetRssiThresholdAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "RssiThreshold"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetErrorRateThresholdAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "ErrorRateThreshold"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task> GetCdmaAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Cdma"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task> GetEvdoAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Evdo"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task> GetGsmAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Gsm"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task> GetUmtsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Umts"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task> GetLteAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Lte"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task> GetNr5gAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Nr5g"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static SignalProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Rate": invalidated.Add("Rate"); break; + case "RssiThreshold": invalidated.Add("RssiThreshold"); break; + case "ErrorRateThreshold": invalidated.Add("ErrorRateThreshold"); break; + case "Cdma": invalidated.Add("Cdma"); break; + case "Evdo": invalidated.Add("Evdo"); break; + case "Gsm": invalidated.Add("Gsm"); break; + case "Umts": invalidated.Add("Umts"); break; + case "Lte": invalidated.Add("Lte"); break; + case "Nr5g": invalidated.Add("Nr5g"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static SignalProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new SignalProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Rate": + reader.ReadSignature("u"); + props.Rate = reader.ReadUInt32(); + changedList?.Add("Rate"); + break; + case "RssiThreshold": + reader.ReadSignature("u"); + props.RssiThreshold = reader.ReadUInt32(); + changedList?.Add("RssiThreshold"); + break; + case "ErrorRateThreshold": + reader.ReadSignature("b"); + props.ErrorRateThreshold = reader.ReadBool(); + changedList?.Add("ErrorRateThreshold"); + break; + case "Cdma": + reader.ReadSignature("a{sv}"); + props.Cdma = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Cdma"); + break; + case "Evdo": + reader.ReadSignature("a{sv}"); + props.Evdo = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Evdo"); + break; + case "Gsm": + reader.ReadSignature("a{sv}"); + props.Gsm = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Gsm"); + break; + case "Umts": + reader.ReadSignature("a{sv}"); + props.Umts = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Umts"); + break; + case "Lte": + reader.ReadSignature("a{sv}"); + props.Lte = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Lte"); + break; + case "Nr5g": + reader.ReadSignature("a{sv}"); + props.Nr5g = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Nr5g"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record UssdProperties + { + public uint State { get; set; } = default!; + public string NetworkNotification { get; set; } = default!; + public string NetworkRequest { get; set; } = default!; + } + partial class Ussd : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Modem3gpp.Ussd"; + public Ussd(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task InitiateAsync(string command) + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_s(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "s", + member: "Initiate"); + writer.WriteString(command); + return writer.CreateMessage(); + } + } + public Task RespondAsync(string response) + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_s(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "s", + member: "Respond"); + writer.WriteString(response); + return writer.CreateMessage(); + } + } + public Task CancelAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "Cancel"); + return writer.CreateMessage(); + } + } + public Task SetStateAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("State"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetNetworkNotificationAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("NetworkNotification"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetNetworkRequestAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("NetworkRequest"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task GetStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "State"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetNetworkNotificationAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "NetworkNotification"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetNetworkRequestAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "NetworkRequest"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static UssdProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "State": invalidated.Add("State"); break; + case "NetworkNotification": invalidated.Add("NetworkNotification"); break; + case "NetworkRequest": invalidated.Add("NetworkRequest"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static UssdProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new UssdProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "State": + reader.ReadSignature("u"); + props.State = reader.ReadUInt32(); + changedList?.Add("State"); + break; + case "NetworkNotification": + reader.ReadSignature("s"); + props.NetworkNotification = reader.ReadString(); + changedList?.Add("NetworkNotification"); + break; + case "NetworkRequest": + reader.ReadSignature("s"); + props.NetworkRequest = reader.ReadString(); + changedList?.Add("NetworkRequest"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record MessagingProperties + { + public ObjectPath[] Messages { get; set; } = default!; + public uint[] SupportedStorages { get; set; } = default!; + public uint DefaultStorage { get; set; } = default!; + } + partial class Messaging : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Messaging"; + public Messaging(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task ListAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_ao(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "List"); + return writer.CreateMessage(); + } + } + public Task DeleteAsync(ObjectPath path) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "o", + member: "Delete"); + writer.WriteObjectPath(path); + return writer.CreateMessage(); + } + } + public Task CreateAsync(Dictionary properties) + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_o(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "Create"); + writer.WriteDictionary(properties); + return writer.CreateMessage(); + } + } + public ValueTask WatchAddedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "Added", (Message m, object? s) => ReadMessage_ob(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + public ValueTask WatchDeletedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "Deleted", (Message m, object? s) => ReadMessage_o(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + public Task SetMessagesAsync(ObjectPath[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Messages"); + writer.WriteSignature("ao"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetSupportedStoragesAsync(uint[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SupportedStorages"); + writer.WriteSignature("au"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetDefaultStorageAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("DefaultStorage"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task GetMessagesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Messages"), (Message m, object? s) => ReadMessage_v_ao(m, (ModemManager1Object)s!), this); + public Task GetSupportedStoragesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SupportedStorages"), (Message m, object? s) => ReadMessage_v_au(m, (ModemManager1Object)s!), this); + public Task GetDefaultStorageAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "DefaultStorage"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static MessagingProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Messages": invalidated.Add("Messages"); break; + case "SupportedStorages": invalidated.Add("SupportedStorages"); break; + case "DefaultStorage": invalidated.Add("DefaultStorage"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static MessagingProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new MessagingProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Messages": + reader.ReadSignature("ao"); + props.Messages = reader.ReadArrayOfObjectPath(); + changedList?.Add("Messages"); + break; + case "SupportedStorages": + reader.ReadSignature("au"); + props.SupportedStorages = reader.ReadArrayOfUInt32(); + changedList?.Add("SupportedStorages"); + break; + case "DefaultStorage": + reader.ReadSignature("u"); + props.DefaultStorage = reader.ReadUInt32(); + changedList?.Add("DefaultStorage"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record ModemProperties + { + public ObjectPath Sim { get; set; } = default!; + public ObjectPath[] SimSlots { get; set; } = default!; + public uint PrimarySimSlot { get; set; } = default!; + public ObjectPath[] Bearers { get; set; } = default!; + public uint[] SupportedCapabilities { get; set; } = default!; + public uint CurrentCapabilities { get; set; } = default!; + public uint MaxBearers { get; set; } = default!; + public uint MaxActiveBearers { get; set; } = default!; + public uint MaxActiveMultiplexedBearers { get; set; } = default!; + public string Manufacturer { get; set; } = default!; + public string Model { get; set; } = default!; + public string Revision { get; set; } = default!; + public string CarrierConfiguration { get; set; } = default!; + public string CarrierConfigurationRevision { get; set; } = default!; + public string HardwareRevision { get; set; } = default!; + public string DeviceIdentifier { get; set; } = default!; + public string Device { get; set; } = default!; + public string[] Drivers { get; set; } = default!; + public string Plugin { get; set; } = default!; + public string PrimaryPort { get; set; } = default!; + public (string, uint)[] Ports { get; set; } = default!; + public string EquipmentIdentifier { get; set; } = default!; + public uint UnlockRequired { get; set; } = default!; + public Dictionary UnlockRetries { get; set; } = default!; + public int State { get; set; } = default!; + public uint StateFailedReason { get; set; } = default!; + public uint AccessTechnologies { get; set; } = default!; + public (uint, bool) SignalQuality { get; set; } = default!; + public string[] OwnNumbers { get; set; } = default!; + public uint PowerState { get; set; } = default!; + public (uint, uint)[] SupportedModes { get; set; } = default!; + public (uint, uint) CurrentModes { get; set; } = default!; + public uint[] SupportedBands { get; set; } = default!; + public uint[] CurrentBands { get; set; } = default!; + public uint SupportedIpFamilies { get; set; } = default!; + } + partial class Modem : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem"; + public Modem(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task EnableAsync(bool enable) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "b", + member: "Enable"); + writer.WriteBool(enable); + return writer.CreateMessage(); + } + } + public Task ListBearersAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_ao(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "ListBearers"); + return writer.CreateMessage(); + } + } + public Task CreateBearerAsync(Dictionary properties) + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_o(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "CreateBearer"); + writer.WriteDictionary(properties); + return writer.CreateMessage(); + } + } + public Task DeleteBearerAsync(ObjectPath bearer) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "o", + member: "DeleteBearer"); + writer.WriteObjectPath(bearer); + return writer.CreateMessage(); + } + } + public Task ResetAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "Reset"); + return writer.CreateMessage(); + } + } + public Task FactoryResetAsync(string code) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "s", + member: "FactoryReset"); + writer.WriteString(code); + return writer.CreateMessage(); + } + } + public Task SetPowerStateAsyncMethod1(uint state) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "SetPowerState"); + writer.WriteUInt32(state); + return writer.CreateMessage(); + } + } + public Task SetCurrentCapabilitiesAsyncMethod1(uint capabilities) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "SetCurrentCapabilities"); + writer.WriteUInt32(capabilities); + return writer.CreateMessage(); + } + } + public Task SetCurrentModesAsyncMethod1((uint, uint) modes) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "(uu)", + member: "SetCurrentModes"); + WriteType_ruuz(ref writer, modes); + return writer.CreateMessage(); + } + } + public Task SetCurrentBandsAsyncMethod1(uint[] bands) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "au", + member: "SetCurrentBands"); + writer.WriteArray(bands); + return writer.CreateMessage(); + } + } + public Task SetPrimarySimSlotAsyncMethod1(uint simSlot) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "SetPrimarySimSlot"); + writer.WriteUInt32(simSlot); + return writer.CreateMessage(); + } + } + public Task[]> GetCellInfoAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_aaesv(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "GetCellInfo"); + return writer.CreateMessage(); + } + } + public Task CommandAsync(string cmd, uint timeout) + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_s(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "su", + member: "Command"); + writer.WriteString(cmd); + writer.WriteUInt32(timeout); + return writer.CreateMessage(); + } + } + public ValueTask WatchStateChangedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "StateChanged", (Message m, object? s) => ReadMessage_iiu(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + public Task SetSimAsync(ObjectPath value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Sim"); + writer.WriteSignature("o"); + writer.WriteObjectPath(value); + return writer.CreateMessage(); + } + } + public Task SetSimSlotsAsync(ObjectPath[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SimSlots"); + writer.WriteSignature("ao"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetPrimarySimSlotAsyncMethod2(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("PrimarySimSlot"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetBearersAsync(ObjectPath[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Bearers"); + writer.WriteSignature("ao"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetSupportedCapabilitiesAsync(uint[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SupportedCapabilities"); + writer.WriteSignature("au"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetCurrentCapabilitiesAsyncMethod2(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("CurrentCapabilities"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetMaxBearersAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("MaxBearers"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetMaxActiveBearersAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("MaxActiveBearers"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetMaxActiveMultiplexedBearersAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("MaxActiveMultiplexedBearers"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetManufacturerAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Manufacturer"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetModelAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Model"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetRevisionAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Revision"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetCarrierConfigurationAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("CarrierConfiguration"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetCarrierConfigurationRevisionAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("CarrierConfigurationRevision"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetHardwareRevisionAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("HardwareRevision"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetDeviceIdentifierAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("DeviceIdentifier"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetDeviceAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Device"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetDriversAsync(string[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Drivers"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetPluginAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Plugin"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetPrimaryPortAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("PrimaryPort"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetPortsAsync((string, uint)[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Ports"); + writer.WriteSignature("a(su)"); + WriteType_arsuz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetEquipmentIdentifierAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("EquipmentIdentifier"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetUnlockRequiredAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("UnlockRequired"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetUnlockRetriesAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("UnlockRetries"); + writer.WriteSignature("a{uu}"); + WriteType_aeuu(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetStateAsync(int value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("State"); + writer.WriteSignature("i"); + writer.WriteInt32(value); + return writer.CreateMessage(); + } + } + public Task SetStateFailedReasonAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("StateFailedReason"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetAccessTechnologiesAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("AccessTechnologies"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetSignalQualityAsync((uint, bool) value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SignalQuality"); + writer.WriteSignature("(ub)"); + WriteType_rubz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetOwnNumbersAsync(string[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("OwnNumbers"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetPowerStateAsyncMethod2(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("PowerState"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetSupportedModesAsync((uint, uint)[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SupportedModes"); + writer.WriteSignature("a(uu)"); + WriteType_aruuz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetCurrentModesAsyncMethod2((uint, uint) value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("CurrentModes"); + writer.WriteSignature("(uu)"); + WriteType_ruuz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetSupportedBandsAsync(uint[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SupportedBands"); + writer.WriteSignature("au"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetCurrentBandsAsyncMethod2(uint[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("CurrentBands"); + writer.WriteSignature("au"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetSupportedIpFamiliesAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SupportedIpFamilies"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task GetSimAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Sim"), (Message m, object? s) => ReadMessage_v_o(m, (ModemManager1Object)s!), this); + public Task GetSimSlotsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SimSlots"), (Message m, object? s) => ReadMessage_v_ao(m, (ModemManager1Object)s!), this); + public Task GetPrimarySimSlotAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "PrimarySimSlot"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetBearersAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Bearers"), (Message m, object? s) => ReadMessage_v_ao(m, (ModemManager1Object)s!), this); + public Task GetSupportedCapabilitiesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SupportedCapabilities"), (Message m, object? s) => ReadMessage_v_au(m, (ModemManager1Object)s!), this); + public Task GetCurrentCapabilitiesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "CurrentCapabilities"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetMaxBearersAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "MaxBearers"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetMaxActiveBearersAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "MaxActiveBearers"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetMaxActiveMultiplexedBearersAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "MaxActiveMultiplexedBearers"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetManufacturerAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Manufacturer"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetModelAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Model"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetRevisionAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Revision"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetCarrierConfigurationAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "CarrierConfiguration"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetCarrierConfigurationRevisionAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "CarrierConfigurationRevision"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetHardwareRevisionAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "HardwareRevision"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetDeviceIdentifierAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "DeviceIdentifier"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetDeviceAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Device"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetDriversAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Drivers"), (Message m, object? s) => ReadMessage_v_as(m, (ModemManager1Object)s!), this); + public Task GetPluginAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Plugin"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetPrimaryPortAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "PrimaryPort"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task<(string, uint)[]> GetPortsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Ports"), (Message m, object? s) => ReadMessage_v_arsuz(m, (ModemManager1Object)s!), this); + public Task GetEquipmentIdentifierAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "EquipmentIdentifier"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetUnlockRequiredAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "UnlockRequired"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task> GetUnlockRetriesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "UnlockRetries"), (Message m, object? s) => ReadMessage_v_aeuu(m, (ModemManager1Object)s!), this); + public Task GetStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "State"), (Message m, object? s) => ReadMessage_v_i(m, (ModemManager1Object)s!), this); + public Task GetStateFailedReasonAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "StateFailedReason"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetAccessTechnologiesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "AccessTechnologies"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task<(uint, bool)> GetSignalQualityAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SignalQuality"), (Message m, object? s) => ReadMessage_v_rubz(m, (ModemManager1Object)s!), this); + public Task GetOwnNumbersAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "OwnNumbers"), (Message m, object? s) => ReadMessage_v_as(m, (ModemManager1Object)s!), this); + public Task GetPowerStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "PowerState"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task<(uint, uint)[]> GetSupportedModesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SupportedModes"), (Message m, object? s) => ReadMessage_v_aruuz(m, (ModemManager1Object)s!), this); + public Task<(uint, uint)> GetCurrentModesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "CurrentModes"), (Message m, object? s) => ReadMessage_v_ruuz(m, (ModemManager1Object)s!), this); + public Task GetSupportedBandsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SupportedBands"), (Message m, object? s) => ReadMessage_v_au(m, (ModemManager1Object)s!), this); + public Task GetCurrentBandsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "CurrentBands"), (Message m, object? s) => ReadMessage_v_au(m, (ModemManager1Object)s!), this); + public Task GetSupportedIpFamiliesAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SupportedIpFamilies"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static ModemProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Sim": invalidated.Add("Sim"); break; + case "SimSlots": invalidated.Add("SimSlots"); break; + case "PrimarySimSlot": invalidated.Add("PrimarySimSlot"); break; + case "Bearers": invalidated.Add("Bearers"); break; + case "SupportedCapabilities": invalidated.Add("SupportedCapabilities"); break; + case "CurrentCapabilities": invalidated.Add("CurrentCapabilities"); break; + case "MaxBearers": invalidated.Add("MaxBearers"); break; + case "MaxActiveBearers": invalidated.Add("MaxActiveBearers"); break; + case "MaxActiveMultiplexedBearers": invalidated.Add("MaxActiveMultiplexedBearers"); break; + case "Manufacturer": invalidated.Add("Manufacturer"); break; + case "Model": invalidated.Add("Model"); break; + case "Revision": invalidated.Add("Revision"); break; + case "CarrierConfiguration": invalidated.Add("CarrierConfiguration"); break; + case "CarrierConfigurationRevision": invalidated.Add("CarrierConfigurationRevision"); break; + case "HardwareRevision": invalidated.Add("HardwareRevision"); break; + case "DeviceIdentifier": invalidated.Add("DeviceIdentifier"); break; + case "Device": invalidated.Add("Device"); break; + case "Drivers": invalidated.Add("Drivers"); break; + case "Plugin": invalidated.Add("Plugin"); break; + case "PrimaryPort": invalidated.Add("PrimaryPort"); break; + case "Ports": invalidated.Add("Ports"); break; + case "EquipmentIdentifier": invalidated.Add("EquipmentIdentifier"); break; + case "UnlockRequired": invalidated.Add("UnlockRequired"); break; + case "UnlockRetries": invalidated.Add("UnlockRetries"); break; + case "State": invalidated.Add("State"); break; + case "StateFailedReason": invalidated.Add("StateFailedReason"); break; + case "AccessTechnologies": invalidated.Add("AccessTechnologies"); break; + case "SignalQuality": invalidated.Add("SignalQuality"); break; + case "OwnNumbers": invalidated.Add("OwnNumbers"); break; + case "PowerState": invalidated.Add("PowerState"); break; + case "SupportedModes": invalidated.Add("SupportedModes"); break; + case "CurrentModes": invalidated.Add("CurrentModes"); break; + case "SupportedBands": invalidated.Add("SupportedBands"); break; + case "CurrentBands": invalidated.Add("CurrentBands"); break; + case "SupportedIpFamilies": invalidated.Add("SupportedIpFamilies"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static ModemProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new ModemProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Sim": + reader.ReadSignature("o"); + props.Sim = reader.ReadObjectPath(); + changedList?.Add("Sim"); + break; + case "SimSlots": + reader.ReadSignature("ao"); + props.SimSlots = reader.ReadArrayOfObjectPath(); + changedList?.Add("SimSlots"); + break; + case "PrimarySimSlot": + reader.ReadSignature("u"); + props.PrimarySimSlot = reader.ReadUInt32(); + changedList?.Add("PrimarySimSlot"); + break; + case "Bearers": + reader.ReadSignature("ao"); + props.Bearers = reader.ReadArrayOfObjectPath(); + changedList?.Add("Bearers"); + break; + case "SupportedCapabilities": + reader.ReadSignature("au"); + props.SupportedCapabilities = reader.ReadArrayOfUInt32(); + changedList?.Add("SupportedCapabilities"); + break; + case "CurrentCapabilities": + reader.ReadSignature("u"); + props.CurrentCapabilities = reader.ReadUInt32(); + changedList?.Add("CurrentCapabilities"); + break; + case "MaxBearers": + reader.ReadSignature("u"); + props.MaxBearers = reader.ReadUInt32(); + changedList?.Add("MaxBearers"); + break; + case "MaxActiveBearers": + reader.ReadSignature("u"); + props.MaxActiveBearers = reader.ReadUInt32(); + changedList?.Add("MaxActiveBearers"); + break; + case "MaxActiveMultiplexedBearers": + reader.ReadSignature("u"); + props.MaxActiveMultiplexedBearers = reader.ReadUInt32(); + changedList?.Add("MaxActiveMultiplexedBearers"); + break; + case "Manufacturer": + reader.ReadSignature("s"); + props.Manufacturer = reader.ReadString(); + changedList?.Add("Manufacturer"); + break; + case "Model": + reader.ReadSignature("s"); + props.Model = reader.ReadString(); + changedList?.Add("Model"); + break; + case "Revision": + reader.ReadSignature("s"); + props.Revision = reader.ReadString(); + changedList?.Add("Revision"); + break; + case "CarrierConfiguration": + reader.ReadSignature("s"); + props.CarrierConfiguration = reader.ReadString(); + changedList?.Add("CarrierConfiguration"); + break; + case "CarrierConfigurationRevision": + reader.ReadSignature("s"); + props.CarrierConfigurationRevision = reader.ReadString(); + changedList?.Add("CarrierConfigurationRevision"); + break; + case "HardwareRevision": + reader.ReadSignature("s"); + props.HardwareRevision = reader.ReadString(); + changedList?.Add("HardwareRevision"); + break; + case "DeviceIdentifier": + reader.ReadSignature("s"); + props.DeviceIdentifier = reader.ReadString(); + changedList?.Add("DeviceIdentifier"); + break; + case "Device": + reader.ReadSignature("s"); + props.Device = reader.ReadString(); + changedList?.Add("Device"); + break; + case "Drivers": + reader.ReadSignature("as"); + props.Drivers = reader.ReadArrayOfString(); + changedList?.Add("Drivers"); + break; + case "Plugin": + reader.ReadSignature("s"); + props.Plugin = reader.ReadString(); + changedList?.Add("Plugin"); + break; + case "PrimaryPort": + reader.ReadSignature("s"); + props.PrimaryPort = reader.ReadString(); + changedList?.Add("PrimaryPort"); + break; + case "Ports": + reader.ReadSignature("a(su)"); + props.Ports = ReadType_arsuz(ref reader); + changedList?.Add("Ports"); + break; + case "EquipmentIdentifier": + reader.ReadSignature("s"); + props.EquipmentIdentifier = reader.ReadString(); + changedList?.Add("EquipmentIdentifier"); + break; + case "UnlockRequired": + reader.ReadSignature("u"); + props.UnlockRequired = reader.ReadUInt32(); + changedList?.Add("UnlockRequired"); + break; + case "UnlockRetries": + reader.ReadSignature("a{uu}"); + props.UnlockRetries = ReadType_aeuu(ref reader); + changedList?.Add("UnlockRetries"); + break; + case "State": + reader.ReadSignature("i"); + props.State = reader.ReadInt32(); + changedList?.Add("State"); + break; + case "StateFailedReason": + reader.ReadSignature("u"); + props.StateFailedReason = reader.ReadUInt32(); + changedList?.Add("StateFailedReason"); + break; + case "AccessTechnologies": + reader.ReadSignature("u"); + props.AccessTechnologies = reader.ReadUInt32(); + changedList?.Add("AccessTechnologies"); + break; + case "SignalQuality": + reader.ReadSignature("(ub)"); + props.SignalQuality = ReadType_rubz(ref reader); + changedList?.Add("SignalQuality"); + break; + case "OwnNumbers": + reader.ReadSignature("as"); + props.OwnNumbers = reader.ReadArrayOfString(); + changedList?.Add("OwnNumbers"); + break; + case "PowerState": + reader.ReadSignature("u"); + props.PowerState = reader.ReadUInt32(); + changedList?.Add("PowerState"); + break; + case "SupportedModes": + reader.ReadSignature("a(uu)"); + props.SupportedModes = ReadType_aruuz(ref reader); + changedList?.Add("SupportedModes"); + break; + case "CurrentModes": + reader.ReadSignature("(uu)"); + props.CurrentModes = ReadType_ruuz(ref reader); + changedList?.Add("CurrentModes"); + break; + case "SupportedBands": + reader.ReadSignature("au"); + props.SupportedBands = reader.ReadArrayOfUInt32(); + changedList?.Add("SupportedBands"); + break; + case "CurrentBands": + reader.ReadSignature("au"); + props.CurrentBands = reader.ReadArrayOfUInt32(); + changedList?.Add("CurrentBands"); + break; + case "SupportedIpFamilies": + reader.ReadSignature("u"); + props.SupportedIpFamilies = reader.ReadUInt32(); + changedList?.Add("SupportedIpFamilies"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record TimeProperties + { + public Dictionary NetworkTimezone { get; set; } = default!; + } + partial class Time : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Time"; + public Time(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task GetNetworkTimeAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_s(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "GetNetworkTime"); + return writer.CreateMessage(); + } + } + public ValueTask WatchNetworkTimeChangedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "NetworkTimeChanged", (Message m, object? s) => ReadMessage_s(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + public Task SetNetworkTimezoneAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("NetworkTimezone"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task> GetNetworkTimezoneAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "NetworkTimezone"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static TimeProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "NetworkTimezone": invalidated.Add("NetworkTimezone"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static TimeProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new TimeProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "NetworkTimezone": + reader.ReadSignature("a{sv}"); + props.NetworkTimezone = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("NetworkTimezone"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record FirmwareProperties + { + public (uint, Dictionary) UpdateSettings { get; set; } = default!; + } + partial class Firmware : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Firmware"; + public Firmware(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task<(string Selected, Dictionary[] Installed)> ListAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_saaesv(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "List"); + return writer.CreateMessage(); + } + } + public Task SelectAsync(string uniqueid) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "s", + member: "Select"); + writer.WriteString(uniqueid); + return writer.CreateMessage(); + } + } + public Task SetUpdateSettingsAsync((uint, Dictionary) value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("UpdateSettings"); + writer.WriteSignature("(ua{sv})"); + WriteType_ruaesvz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task<(uint, Dictionary)> GetUpdateSettingsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "UpdateSettings"), (Message m, object? s) => ReadMessage_v_ruaesvz(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static FirmwareProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "UpdateSettings": invalidated.Add("UpdateSettings"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static FirmwareProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new FirmwareProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "UpdateSettings": + reader.ReadSignature("(ua{sv})"); + props.UpdateSettings = ReadType_ruaesvz(ref reader); + changedList?.Add("UpdateSettings"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record ProfileManagerProperties + { + public string IndexField { get; set; } = default!; + } + partial class ProfileManager : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Modem3gpp.ProfileManager"; + public ProfileManager(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task[]> ListAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_aaesv(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "List"); + return writer.CreateMessage(); + } + } + public Task> SetAsync(Dictionary requestedProperties) + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_aesv(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "Set"); + writer.WriteDictionary(requestedProperties); + return writer.CreateMessage(); + } + } + public Task DeleteAsync(Dictionary properties) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "Delete"); + writer.WriteDictionary(properties); + return writer.CreateMessage(); + } + } + public ValueTask WatchUpdatedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "Updated", handler, emitOnCapturedContext, flags); + public Task SetIndexFieldAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("IndexField"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task GetIndexFieldAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "IndexField"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static ProfileManagerProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "IndexField": invalidated.Add("IndexField"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static ProfileManagerProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new ProfileManagerProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "IndexField": + reader.ReadSignature("s"); + props.IndexField = reader.ReadString(); + changedList?.Add("IndexField"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record SarProperties + { + public bool State { get; set; } = default!; + public uint PowerLevel { get; set; } = default!; + } + partial class Sar : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Sar"; + public Sar(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task EnableAsync(bool enable) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "b", + member: "Enable"); + writer.WriteBool(enable); + return writer.CreateMessage(); + } + } + public Task SetPowerLevelAsyncMethod1(uint level) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "SetPowerLevel"); + writer.WriteUInt32(level); + return writer.CreateMessage(); + } + } + public Task SetStateAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("State"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetPowerLevelAsyncMethod2(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("PowerLevel"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task GetStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "State"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task GetPowerLevelAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "PowerLevel"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static SarProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "State": invalidated.Add("State"); break; + case "PowerLevel": invalidated.Add("PowerLevel"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static SarProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new SarProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "State": + reader.ReadSignature("b"); + props.State = reader.ReadBool(); + changedList?.Add("State"); + break; + case "PowerLevel": + reader.ReadSignature("u"); + props.PowerLevel = reader.ReadUInt32(); + changedList?.Add("PowerLevel"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + partial class Simple : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Simple"; + public Simple(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task ConnectAsync(Dictionary properties) + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_o(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "Connect"); + writer.WriteDictionary(properties); + return writer.CreateMessage(); + } + } + public Task DisconnectAsync(ObjectPath bearer) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "o", + member: "Disconnect"); + writer.WriteObjectPath(bearer); + return writer.CreateMessage(); + } + } + public Task> GetStatusAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_aesv(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "GetStatus"); + return writer.CreateMessage(); + } + } + } + record Modem3gppProperties + { + public string Imei { get; set; } = default!; + public uint RegistrationState { get; set; } = default!; + public string OperatorCode { get; set; } = default!; + public string OperatorName { get; set; } = default!; + public uint EnabledFacilityLocks { get; set; } = default!; + public uint SubscriptionState { get; set; } = default!; + public uint EpsUeModeOperation { get; set; } = default!; + public (uint, bool, byte[])[] Pco { get; set; } = default!; + public ObjectPath InitialEpsBearer { get; set; } = default!; + public Dictionary InitialEpsBearerSettings { get; set; } = default!; + public uint PacketServiceState { get; set; } = default!; + public Dictionary Nr5gRegistrationSettings { get; set; } = default!; + } + partial class Modem3gpp : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Modem3gpp"; + public Modem3gpp(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task RegisterAsync(string operatorId) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "s", + member: "Register"); + writer.WriteString(operatorId); + return writer.CreateMessage(); + } + } + public Task[]> ScanAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_aaesv(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "Scan"); + return writer.CreateMessage(); + } + } + public Task SetEpsUeModeOperationAsyncMethod1(uint mode) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "SetEpsUeModeOperation"); + writer.WriteUInt32(mode); + return writer.CreateMessage(); + } + } + public Task SetInitialEpsBearerSettingsAsyncMethod1(Dictionary settings) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "SetInitialEpsBearerSettings"); + writer.WriteDictionary(settings); + return writer.CreateMessage(); + } + } + public Task SetNr5gRegistrationSettingsAsyncMethod1(Dictionary properties) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "SetNr5gRegistrationSettings"); + writer.WriteDictionary(properties); + return writer.CreateMessage(); + } + } + public Task DisableFacilityLockAsync((uint, string) properties) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "(us)", + member: "DisableFacilityLock"); + WriteType_rusz(ref writer, properties); + return writer.CreateMessage(); + } + } + public Task SetPacketServiceStateAsyncMethod1(uint state) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "SetPacketServiceState"); + writer.WriteUInt32(state); + return writer.CreateMessage(); + } + } + public Task SetImeiAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Imei"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetRegistrationStateAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("RegistrationState"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetOperatorCodeAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("OperatorCode"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetOperatorNameAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("OperatorName"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetEnabledFacilityLocksAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("EnabledFacilityLocks"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetSubscriptionStateAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SubscriptionState"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetEpsUeModeOperationAsyncMethod2(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("EpsUeModeOperation"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetPcoAsync((uint, bool, byte[])[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Pco"); + writer.WriteSignature("a(ubay)"); + WriteType_arubayz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetInitialEpsBearerAsync(ObjectPath value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("InitialEpsBearer"); + writer.WriteSignature("o"); + writer.WriteObjectPath(value); + return writer.CreateMessage(); + } + } + public Task SetInitialEpsBearerSettingsAsyncMethod2(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("InitialEpsBearerSettings"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetPacketServiceStateAsyncMethod2(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("PacketServiceState"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetNr5gRegistrationSettingsAsyncMethod2(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Nr5gRegistrationSettings"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task GetImeiAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Imei"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetRegistrationStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "RegistrationState"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetOperatorCodeAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "OperatorCode"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetOperatorNameAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "OperatorName"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetEnabledFacilityLocksAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "EnabledFacilityLocks"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetSubscriptionStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SubscriptionState"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetEpsUeModeOperationAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "EpsUeModeOperation"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task<(uint, bool, byte[])[]> GetPcoAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Pco"), (Message m, object? s) => ReadMessage_v_arubayz(m, (ModemManager1Object)s!), this); + public Task GetInitialEpsBearerAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "InitialEpsBearer"), (Message m, object? s) => ReadMessage_v_o(m, (ModemManager1Object)s!), this); + public Task> GetInitialEpsBearerSettingsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "InitialEpsBearerSettings"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task GetPacketServiceStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "PacketServiceState"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task> GetNr5gRegistrationSettingsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Nr5gRegistrationSettings"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static Modem3gppProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Imei": invalidated.Add("Imei"); break; + case "RegistrationState": invalidated.Add("RegistrationState"); break; + case "OperatorCode": invalidated.Add("OperatorCode"); break; + case "OperatorName": invalidated.Add("OperatorName"); break; + case "EnabledFacilityLocks": invalidated.Add("EnabledFacilityLocks"); break; + case "SubscriptionState": invalidated.Add("SubscriptionState"); break; + case "EpsUeModeOperation": invalidated.Add("EpsUeModeOperation"); break; + case "Pco": invalidated.Add("Pco"); break; + case "InitialEpsBearer": invalidated.Add("InitialEpsBearer"); break; + case "InitialEpsBearerSettings": invalidated.Add("InitialEpsBearerSettings"); break; + case "PacketServiceState": invalidated.Add("PacketServiceState"); break; + case "Nr5gRegistrationSettings": invalidated.Add("Nr5gRegistrationSettings"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static Modem3gppProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new Modem3gppProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Imei": + reader.ReadSignature("s"); + props.Imei = reader.ReadString(); + changedList?.Add("Imei"); + break; + case "RegistrationState": + reader.ReadSignature("u"); + props.RegistrationState = reader.ReadUInt32(); + changedList?.Add("RegistrationState"); + break; + case "OperatorCode": + reader.ReadSignature("s"); + props.OperatorCode = reader.ReadString(); + changedList?.Add("OperatorCode"); + break; + case "OperatorName": + reader.ReadSignature("s"); + props.OperatorName = reader.ReadString(); + changedList?.Add("OperatorName"); + break; + case "EnabledFacilityLocks": + reader.ReadSignature("u"); + props.EnabledFacilityLocks = reader.ReadUInt32(); + changedList?.Add("EnabledFacilityLocks"); + break; + case "SubscriptionState": + reader.ReadSignature("u"); + props.SubscriptionState = reader.ReadUInt32(); + changedList?.Add("SubscriptionState"); + break; + case "EpsUeModeOperation": + reader.ReadSignature("u"); + props.EpsUeModeOperation = reader.ReadUInt32(); + changedList?.Add("EpsUeModeOperation"); + break; + case "Pco": + reader.ReadSignature("a(ubay)"); + props.Pco = ReadType_arubayz(ref reader); + changedList?.Add("Pco"); + break; + case "InitialEpsBearer": + reader.ReadSignature("o"); + props.InitialEpsBearer = reader.ReadObjectPath(); + changedList?.Add("InitialEpsBearer"); + break; + case "InitialEpsBearerSettings": + reader.ReadSignature("a{sv}"); + props.InitialEpsBearerSettings = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("InitialEpsBearerSettings"); + break; + case "PacketServiceState": + reader.ReadSignature("u"); + props.PacketServiceState = reader.ReadUInt32(); + changedList?.Add("PacketServiceState"); + break; + case "Nr5gRegistrationSettings": + reader.ReadSignature("a{sv}"); + props.Nr5gRegistrationSettings = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Nr5gRegistrationSettings"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record VoiceProperties + { + public ObjectPath[] Calls { get; set; } = default!; + public bool EmergencyOnly { get; set; } = default!; + } + partial class Voice : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Modem.Voice"; + public Voice(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task ListCallsAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_ao(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "ListCalls"); + return writer.CreateMessage(); + } + } + public Task DeleteCallAsync(ObjectPath path) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "o", + member: "DeleteCall"); + writer.WriteObjectPath(path); + return writer.CreateMessage(); + } + } + public Task CreateCallAsync(Dictionary properties) + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_o(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a{sv}", + member: "CreateCall"); + writer.WriteDictionary(properties); + return writer.CreateMessage(); + } + } + public Task HoldAndAcceptAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "HoldAndAccept"); + return writer.CreateMessage(); + } + } + public Task HangupAndAcceptAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "HangupAndAccept"); + return writer.CreateMessage(); + } + } + public Task HangupAllAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "HangupAll"); + return writer.CreateMessage(); + } + } + public Task TransferAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "Transfer"); + return writer.CreateMessage(); + } + } + public Task CallWaitingSetupAsync(bool enable) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "b", + member: "CallWaitingSetup"); + writer.WriteBool(enable); + return writer.CreateMessage(); + } + } + public Task CallWaitingQueryAsync() + { + return this.Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_b(m, (ModemManager1Object)s!), this); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "CallWaitingQuery"); + return writer.CreateMessage(); + } + } + public ValueTask WatchCallAddedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "CallAdded", (Message m, object? s) => ReadMessage_o(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + public ValueTask WatchCallDeletedAsync(Action handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + => base.WatchSignalAsync(Service.Destination, __Interface, Path, "CallDeleted", (Message m, object? s) => ReadMessage_o(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + public Task SetCallsAsync(ObjectPath[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Calls"); + writer.WriteSignature("ao"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetEmergencyOnlyAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("EmergencyOnly"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task GetCallsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Calls"), (Message m, object? s) => ReadMessage_v_ao(m, (ModemManager1Object)s!), this); + public Task GetEmergencyOnlyAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "EmergencyOnly"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static VoiceProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Calls": invalidated.Add("Calls"); break; + case "EmergencyOnly": invalidated.Add("EmergencyOnly"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static VoiceProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new VoiceProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Calls": + reader.ReadSignature("ao"); + props.Calls = reader.ReadArrayOfObjectPath(); + changedList?.Add("Calls"); + break; + case "EmergencyOnly": + reader.ReadSignature("b"); + props.EmergencyOnly = reader.ReadBool(); + changedList?.Add("EmergencyOnly"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record SimProperties + { + public bool Active { get; set; } = default!; + public string SimIdentifier { get; set; } = default!; + public string Imsi { get; set; } = default!; + public string Eid { get; set; } = default!; + public string OperatorIdentifier { get; set; } = default!; + public string OperatorName { get; set; } = default!; + public string[] EmergencyNumbers { get; set; } = default!; + public (string, uint)[] PreferredNetworks { get; set; } = default!; + public byte[] Gid1 { get; set; } = default!; + public byte[] Gid2 { get; set; } = default!; + public uint SimType { get; set; } = default!; + public uint EsimStatus { get; set; } = default!; + public uint Removability { get; set; } = default!; + } + partial class Sim : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Sim"; + public Sim(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task SendPinAsync(string pin) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "s", + member: "SendPin"); + writer.WriteString(pin); + return writer.CreateMessage(); + } + } + public Task SendPukAsync(string puk, string pin) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "ss", + member: "SendPuk"); + writer.WriteString(puk); + writer.WriteString(pin); + return writer.CreateMessage(); + } + } + public Task EnablePinAsync(string pin, bool enabled) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "sb", + member: "EnablePin"); + writer.WriteString(pin); + writer.WriteBool(enabled); + return writer.CreateMessage(); + } + } + public Task ChangePinAsync(string oldPin, string newPin) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "ss", + member: "ChangePin"); + writer.WriteString(oldPin); + writer.WriteString(newPin); + return writer.CreateMessage(); + } + } + public Task SetPreferredNetworksAsyncMethod1((string, uint)[] preferredNetworks) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "a(su)", + member: "SetPreferredNetworks"); + WriteType_arsuz(ref writer, preferredNetworks); + return writer.CreateMessage(); + } + } + public Task SetActiveAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Active"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetSimIdentifierAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SimIdentifier"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetImsiAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Imsi"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetEidAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Eid"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetOperatorIdentifierAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("OperatorIdentifier"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetOperatorNameAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("OperatorName"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetEmergencyNumbersAsync(string[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("EmergencyNumbers"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetPreferredNetworksAsyncMethod2((string, uint)[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("PreferredNetworks"); + writer.WriteSignature("a(su)"); + WriteType_arsuz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetGid1Async(byte[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Gid1"); + writer.WriteSignature("ay"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetGid2Async(byte[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Gid2"); + writer.WriteSignature("ay"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetSimTypeAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SimType"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetEsimStatusAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("EsimStatus"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetRemovabilityAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Removability"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task GetActiveAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Active"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task GetSimIdentifierAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SimIdentifier"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetImsiAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Imsi"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetEidAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Eid"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetOperatorIdentifierAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "OperatorIdentifier"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetOperatorNameAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "OperatorName"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetEmergencyNumbersAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "EmergencyNumbers"), (Message m, object? s) => ReadMessage_v_as(m, (ModemManager1Object)s!), this); + public Task<(string, uint)[]> GetPreferredNetworksAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "PreferredNetworks"), (Message m, object? s) => ReadMessage_v_arsuz(m, (ModemManager1Object)s!), this); + public Task GetGid1Async() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Gid1"), (Message m, object? s) => ReadMessage_v_ay(m, (ModemManager1Object)s!), this); + public Task GetGid2Async() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Gid2"), (Message m, object? s) => ReadMessage_v_ay(m, (ModemManager1Object)s!), this); + public Task GetSimTypeAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SimType"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetEsimStatusAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "EsimStatus"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetRemovabilityAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Removability"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static SimProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Active": invalidated.Add("Active"); break; + case "SimIdentifier": invalidated.Add("SimIdentifier"); break; + case "Imsi": invalidated.Add("Imsi"); break; + case "Eid": invalidated.Add("Eid"); break; + case "OperatorIdentifier": invalidated.Add("OperatorIdentifier"); break; + case "OperatorName": invalidated.Add("OperatorName"); break; + case "EmergencyNumbers": invalidated.Add("EmergencyNumbers"); break; + case "PreferredNetworks": invalidated.Add("PreferredNetworks"); break; + case "Gid1": invalidated.Add("Gid1"); break; + case "Gid2": invalidated.Add("Gid2"); break; + case "SimType": invalidated.Add("SimType"); break; + case "EsimStatus": invalidated.Add("EsimStatus"); break; + case "Removability": invalidated.Add("Removability"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static SimProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new SimProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Active": + reader.ReadSignature("b"); + props.Active = reader.ReadBool(); + changedList?.Add("Active"); + break; + case "SimIdentifier": + reader.ReadSignature("s"); + props.SimIdentifier = reader.ReadString(); + changedList?.Add("SimIdentifier"); + break; + case "Imsi": + reader.ReadSignature("s"); + props.Imsi = reader.ReadString(); + changedList?.Add("Imsi"); + break; + case "Eid": + reader.ReadSignature("s"); + props.Eid = reader.ReadString(); + changedList?.Add("Eid"); + break; + case "OperatorIdentifier": + reader.ReadSignature("s"); + props.OperatorIdentifier = reader.ReadString(); + changedList?.Add("OperatorIdentifier"); + break; + case "OperatorName": + reader.ReadSignature("s"); + props.OperatorName = reader.ReadString(); + changedList?.Add("OperatorName"); + break; + case "EmergencyNumbers": + reader.ReadSignature("as"); + props.EmergencyNumbers = reader.ReadArrayOfString(); + changedList?.Add("EmergencyNumbers"); + break; + case "PreferredNetworks": + reader.ReadSignature("a(su)"); + props.PreferredNetworks = ReadType_arsuz(ref reader); + changedList?.Add("PreferredNetworks"); + break; + case "Gid1": + reader.ReadSignature("ay"); + props.Gid1 = reader.ReadArrayOfByte(); + changedList?.Add("Gid1"); + break; + case "Gid2": + reader.ReadSignature("ay"); + props.Gid2 = reader.ReadArrayOfByte(); + changedList?.Add("Gid2"); + break; + case "SimType": + reader.ReadSignature("u"); + props.SimType = reader.ReadUInt32(); + changedList?.Add("SimType"); + break; + case "EsimStatus": + reader.ReadSignature("u"); + props.EsimStatus = reader.ReadUInt32(); + changedList?.Add("EsimStatus"); + break; + case "Removability": + reader.ReadSignature("u"); + props.Removability = reader.ReadUInt32(); + changedList?.Add("Removability"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record SmsProperties + { + public uint State { get; set; } = default!; + public uint PduType { get; set; } = default!; + public string Number { get; set; } = default!; + public string Text { get; set; } = default!; + public byte[] Data { get; set; } = default!; + public string SMSC { get; set; } = default!; + public (uint, VariantValue) Validity { get; set; } = default!; + public int Class { get; set; } = default!; + public uint TeleserviceId { get; set; } = default!; + public uint ServiceCategory { get; set; } = default!; + public bool DeliveryReportRequest { get; set; } = default!; + public uint MessageReference { get; set; } = default!; + public string Timestamp { get; set; } = default!; + public string DischargeTimestamp { get; set; } = default!; + public uint DeliveryState { get; set; } = default!; + public uint Storage { get; set; } = default!; + } + partial class Sms : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Sms"; + public Sms(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task SendAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "Send"); + return writer.CreateMessage(); + } + } + public Task StoreAsync(uint storage) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + signature: "u", + member: "Store"); + writer.WriteUInt32(storage); + return writer.CreateMessage(); + } + } + public Task SetStateAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("State"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetPduTypeAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("PduType"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetNumberAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Number"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetTextAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Text"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetDataAsync(byte[] value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Data"); + writer.WriteSignature("ay"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task SetSMSCAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("SMSC"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetValidityAsync((uint, VariantValue) value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Validity"); + writer.WriteSignature("(uv)"); + WriteType_ruvz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetClassAsync(int value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Class"); + writer.WriteSignature("i"); + writer.WriteInt32(value); + return writer.CreateMessage(); + } + } + public Task SetTeleserviceIdAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("TeleserviceId"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetServiceCategoryAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("ServiceCategory"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetDeliveryReportRequestAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("DeliveryReportRequest"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetMessageReferenceAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("MessageReference"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetTimestampAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Timestamp"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetDischargeTimestampAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("DischargeTimestamp"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetDeliveryStateAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("DeliveryState"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetStorageAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Storage"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task GetStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "State"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetPduTypeAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "PduType"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetNumberAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Number"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetTextAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Text"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetDataAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Data"), (Message m, object? s) => ReadMessage_v_ay(m, (ModemManager1Object)s!), this); + public Task GetSMSCAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "SMSC"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task<(uint, VariantValue)> GetValidityAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Validity"), (Message m, object? s) => ReadMessage_v_ruvz(m, (ModemManager1Object)s!), this); + public Task GetClassAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Class"), (Message m, object? s) => ReadMessage_v_i(m, (ModemManager1Object)s!), this); + public Task GetTeleserviceIdAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "TeleserviceId"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetServiceCategoryAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "ServiceCategory"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetDeliveryReportRequestAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "DeliveryReportRequest"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task GetMessageReferenceAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "MessageReference"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetTimestampAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Timestamp"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetDischargeTimestampAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "DischargeTimestamp"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetDeliveryStateAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "DeliveryState"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetStorageAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Storage"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsync() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static SmsProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "State": invalidated.Add("State"); break; + case "PduType": invalidated.Add("PduType"); break; + case "Number": invalidated.Add("Number"); break; + case "Text": invalidated.Add("Text"); break; + case "Data": invalidated.Add("Data"); break; + case "SMSC": invalidated.Add("SMSC"); break; + case "Validity": invalidated.Add("Validity"); break; + case "Class": invalidated.Add("Class"); break; + case "TeleserviceId": invalidated.Add("TeleserviceId"); break; + case "ServiceCategory": invalidated.Add("ServiceCategory"); break; + case "DeliveryReportRequest": invalidated.Add("DeliveryReportRequest"); break; + case "MessageReference": invalidated.Add("MessageReference"); break; + case "Timestamp": invalidated.Add("Timestamp"); break; + case "DischargeTimestamp": invalidated.Add("DischargeTimestamp"); break; + case "DeliveryState": invalidated.Add("DeliveryState"); break; + case "Storage": invalidated.Add("Storage"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static SmsProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new SmsProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "State": + reader.ReadSignature("u"); + props.State = reader.ReadUInt32(); + changedList?.Add("State"); + break; + case "PduType": + reader.ReadSignature("u"); + props.PduType = reader.ReadUInt32(); + changedList?.Add("PduType"); + break; + case "Number": + reader.ReadSignature("s"); + props.Number = reader.ReadString(); + changedList?.Add("Number"); + break; + case "Text": + reader.ReadSignature("s"); + props.Text = reader.ReadString(); + changedList?.Add("Text"); + break; + case "Data": + reader.ReadSignature("ay"); + props.Data = reader.ReadArrayOfByte(); + changedList?.Add("Data"); + break; + case "SMSC": + reader.ReadSignature("s"); + props.SMSC = reader.ReadString(); + changedList?.Add("SMSC"); + break; + case "Validity": + reader.ReadSignature("(uv)"); + props.Validity = ReadType_ruvz(ref reader); + changedList?.Add("Validity"); + break; + case "Class": + reader.ReadSignature("i"); + props.Class = reader.ReadInt32(); + changedList?.Add("Class"); + break; + case "TeleserviceId": + reader.ReadSignature("u"); + props.TeleserviceId = reader.ReadUInt32(); + changedList?.Add("TeleserviceId"); + break; + case "ServiceCategory": + reader.ReadSignature("u"); + props.ServiceCategory = reader.ReadUInt32(); + changedList?.Add("ServiceCategory"); + break; + case "DeliveryReportRequest": + reader.ReadSignature("b"); + props.DeliveryReportRequest = reader.ReadBool(); + changedList?.Add("DeliveryReportRequest"); + break; + case "MessageReference": + reader.ReadSignature("u"); + props.MessageReference = reader.ReadUInt32(); + changedList?.Add("MessageReference"); + break; + case "Timestamp": + reader.ReadSignature("s"); + props.Timestamp = reader.ReadString(); + changedList?.Add("Timestamp"); + break; + case "DischargeTimestamp": + reader.ReadSignature("s"); + props.DischargeTimestamp = reader.ReadString(); + changedList?.Add("DischargeTimestamp"); + break; + case "DeliveryState": + reader.ReadSignature("u"); + props.DeliveryState = reader.ReadUInt32(); + changedList?.Add("DeliveryState"); + break; + case "Storage": + reader.ReadSignature("u"); + props.Storage = reader.ReadUInt32(); + changedList?.Add("Storage"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + record BearerProperties + { + public string Interface { get; set; } = default!; + public bool Connected { get; set; } = default!; + public (string, string) ConnectionError { get; set; } = default!; + public bool Suspended { get; set; } = default!; + public bool Multiplexed { get; set; } = default!; + public Dictionary Ip4Config { get; set; } = default!; + public Dictionary Ip6Config { get; set; } = default!; + public Dictionary Stats { get; set; } = default!; + public bool ReloadStatsSupported { get; set; } = default!; + public uint IpTimeout { get; set; } = default!; + public uint BearerType { get; set; } = default!; + public int ProfileId { get; set; } = default!; + public Dictionary Properties { get; set; } = default!; + } + partial class Bearer : ModemManager1Object + { + private const string __Interface = "org.freedesktop.ModemManager1.Bearer"; + public Bearer(ModemManager1Service service, ObjectPath path) : base(service, path) + { } + public Task ConnectAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "Connect"); + return writer.CreateMessage(); + } + } + public Task DisconnectAsync() + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: __Interface, + member: "Disconnect"); + return writer.CreateMessage(); + } + } + public Task SetInterfaceAsync(string value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Interface"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetConnectedAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Connected"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetConnectionErrorAsync((string, string) value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("ConnectionError"); + writer.WriteSignature("(ss)"); + WriteType_rssz(ref writer, value); + return writer.CreateMessage(); + } + } + public Task SetSuspendedAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Suspended"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetMultiplexedAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Multiplexed"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetIp4ConfigAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Ip4Config"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetIp6ConfigAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Ip6Config"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetStatsAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Stats"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task SetReloadStatsSupportedAsync(bool value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("ReloadStatsSupported"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + public Task SetIpTimeoutAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("IpTimeout"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetBearerTypeAsync(uint value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("BearerType"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetProfileIdAsync(int value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("ProfileId"); + writer.WriteSignature("i"); + writer.WriteInt32(value); + return writer.CreateMessage(); + } + } + public Task SetPropertiesAsync(Dictionary value) + { + return this.Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(__Interface); + writer.WriteString("Properties"); + writer.WriteSignature("a{sv}"); + writer.WriteDictionary(value); + return writer.CreateMessage(); + } + } + public Task GetInterfaceAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Interface"), (Message m, object? s) => ReadMessage_v_s(m, (ModemManager1Object)s!), this); + public Task GetConnectedAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Connected"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task<(string, string)> GetConnectionErrorAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "ConnectionError"), (Message m, object? s) => ReadMessage_v_rssz(m, (ModemManager1Object)s!), this); + public Task GetSuspendedAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Suspended"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task GetMultiplexedAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Multiplexed"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task> GetIp4ConfigAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Ip4Config"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task> GetIp6ConfigAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Ip6Config"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task> GetStatsAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Stats"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task GetReloadStatsSupportedAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "ReloadStatsSupported"), (Message m, object? s) => ReadMessage_v_b(m, (ModemManager1Object)s!), this); + public Task GetIpTimeoutAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "IpTimeout"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetBearerTypeAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "BearerType"), (Message m, object? s) => ReadMessage_v_u(m, (ModemManager1Object)s!), this); + public Task GetProfileIdAsync() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "ProfileId"), (Message m, object? s) => ReadMessage_v_i(m, (ModemManager1Object)s!), this); + public Task> GetPropertiesAsyncMethod1() + => this.Connection.CallMethodAsync(CreateGetPropertyMessage(__Interface, "Properties"), (Message m, object? s) => ReadMessage_v_aesv(m, (ModemManager1Object)s!), this); + public Task GetPropertiesAsyncMethod2() + { + return this.Connection.CallMethodAsync(CreateGetAllPropertiesMessage(__Interface), (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), this); + static BearerProperties ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true, ObserverFlags flags = ObserverFlags.None) + { + return base.WatchPropertiesChangedAsync(__Interface, (Message m, object? s) => ReadMessage(m, (ModemManager1Object)s!), handler, emitOnCapturedContext, flags); + static PropertyChanges ReadMessage(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(arrayEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Interface": invalidated.Add("Interface"); break; + case "Connected": invalidated.Add("Connected"); break; + case "ConnectionError": invalidated.Add("ConnectionError"); break; + case "Suspended": invalidated.Add("Suspended"); break; + case "Multiplexed": invalidated.Add("Multiplexed"); break; + case "Ip4Config": invalidated.Add("Ip4Config"); break; + case "Ip6Config": invalidated.Add("Ip6Config"); break; + case "Stats": invalidated.Add("Stats"); break; + case "ReloadStatsSupported": invalidated.Add("ReloadStatsSupported"); break; + case "IpTimeout": invalidated.Add("IpTimeout"); break; + case "BearerType": invalidated.Add("BearerType"); break; + case "ProfileId": invalidated.Add("ProfileId"); break; + case "Properties": invalidated.Add("Properties"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static BearerProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new BearerProperties(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Interface": + reader.ReadSignature("s"); + props.Interface = reader.ReadString(); + changedList?.Add("Interface"); + break; + case "Connected": + reader.ReadSignature("b"); + props.Connected = reader.ReadBool(); + changedList?.Add("Connected"); + break; + case "ConnectionError": + reader.ReadSignature("(ss)"); + props.ConnectionError = ReadType_rssz(ref reader); + changedList?.Add("ConnectionError"); + break; + case "Suspended": + reader.ReadSignature("b"); + props.Suspended = reader.ReadBool(); + changedList?.Add("Suspended"); + break; + case "Multiplexed": + reader.ReadSignature("b"); + props.Multiplexed = reader.ReadBool(); + changedList?.Add("Multiplexed"); + break; + case "Ip4Config": + reader.ReadSignature("a{sv}"); + props.Ip4Config = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Ip4Config"); + break; + case "Ip6Config": + reader.ReadSignature("a{sv}"); + props.Ip6Config = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Ip6Config"); + break; + case "Stats": + reader.ReadSignature("a{sv}"); + props.Stats = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Stats"); + break; + case "ReloadStatsSupported": + reader.ReadSignature("b"); + props.ReloadStatsSupported = reader.ReadBool(); + changedList?.Add("ReloadStatsSupported"); + break; + case "IpTimeout": + reader.ReadSignature("u"); + props.IpTimeout = reader.ReadUInt32(); + changedList?.Add("IpTimeout"); + break; + case "BearerType": + reader.ReadSignature("u"); + props.BearerType = reader.ReadUInt32(); + changedList?.Add("BearerType"); + break; + case "ProfileId": + reader.ReadSignature("i"); + props.ProfileId = reader.ReadInt32(); + changedList?.Add("ProfileId"); + break; + case "Properties": + reader.ReadSignature("a{sv}"); + props.Properties = reader.ReadDictionaryOfStringToVariantValue(); + changedList?.Add("Properties"); + break; + default: + reader.ReadVariantValue(); + break; + } + } + return props; + } + } + partial class ModemManager1Service + { + public Tmds.DBus.Protocol.Connection Connection { get; } + public string Destination { get; } + public ModemManager1Service(Tmds.DBus.Protocol.Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + public ObjectManager CreateObjectManager(string path) => new ObjectManager(this, path); + public ModemManager1 CreateModemManager1(string path) => new ModemManager1(this, path); + public Location CreateLocation(string path) => new Location(this, path); + public Signal CreateSignal(string path) => new Signal(this, path); + public Ussd CreateUssd(string path) => new Ussd(this, path); + public Messaging CreateMessaging(string path) => new Messaging(this, path); + public Modem CreateModem(string path) => new Modem(this, path); + public Time CreateTime(string path) => new Time(this, path); + public Firmware CreateFirmware(string path) => new Firmware(this, path); + public ProfileManager CreateProfileManager(string path) => new ProfileManager(this, path); + public Sar CreateSar(string path) => new Sar(this, path); + public Simple CreateSimple(string path) => new Simple(this, path); + public Modem3gpp CreateModem3gpp(string path) => new Modem3gpp(this, path); + public Voice CreateVoice(string path) => new Voice(this, path); + public Sim CreateSim(string path) => new Sim(this, path); + public Sms CreateSms(string path) => new Sms(this, path); + public Bearer CreateBearer(string path) => new Bearer(this, path); + } + class ModemManager1Object + { + public ModemManager1Service Service { get; } + public ObjectPath Path { get; } + protected Tmds.DBus.Protocol.Connection Connection => Service.Connection; + protected ModemManager1Object(ModemManager1Service service, ObjectPath path) + => (Service, Path) = (service, path); + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + var writer = this.Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + destination: Service.Destination, + path: Path, + @interface: "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext, ObserverFlags flags) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return this.Connection.AddMatchAsync(rule, reader, + (Exception? ex, PropertyChanges changes, object? rs, object? hs) => ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext, flags); + } + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext, ObserverFlags flags) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return this.Connection.AddMatchAsync(rule, reader, + (Exception? ex, TArg arg, object? rs, object? hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext, flags); + } + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext, ObserverFlags flags) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return this.Connection.AddMatchAsync(rule, (Message message, object? state) => null!, + (Exception? ex, object v, object? rs, object? hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext, flags); + } + protected static Dictionary>> ReadMessage_aeoaesaesv(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadType_aeoaesaesv(ref reader); + } + protected static (ObjectPath, Dictionary>) ReadMessage_oaesaesv(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadObjectPath(); + var arg1 = ReadType_aesaesv(ref reader); + return (arg0, arg1); + } + protected static (ObjectPath, string[]) ReadMessage_oas(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadObjectPath(); + var arg1 = reader.ReadArrayOfString(); + return (arg0, arg1); + } + protected static string ReadMessage_v_s(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("s"); + return reader.ReadString(); + } + protected static Dictionary ReadMessage_aeuv(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadType_aeuv(ref reader); + } + protected static uint ReadMessage_v_u(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("u"); + return reader.ReadUInt32(); + } + protected static bool ReadMessage_v_b(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("b"); + return reader.ReadBool(); + } + protected static Dictionary ReadMessage_v_aeuv(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("a{uv}"); + return ReadType_aeuv(ref reader); + } + protected static string[] ReadMessage_v_as(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("as"); + return reader.ReadArrayOfString(); + } + protected static Dictionary ReadMessage_v_aesv(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("a{sv}"); + return reader.ReadDictionaryOfStringToVariantValue(); + } + protected static string ReadMessage_s(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + protected static ObjectPath[] ReadMessage_ao(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return reader.ReadArrayOfObjectPath(); + } + protected static ObjectPath ReadMessage_o(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return reader.ReadObjectPath(); + } + protected static (ObjectPath, bool) ReadMessage_ob(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadObjectPath(); + var arg1 = reader.ReadBool(); + return (arg0, arg1); + } + protected static ObjectPath[] ReadMessage_v_ao(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("ao"); + return reader.ReadArrayOfObjectPath(); + } + protected static uint[] ReadMessage_v_au(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("au"); + return reader.ReadArrayOfUInt32(); + } + protected static Dictionary[] ReadMessage_aaesv(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return ReadType_aaesv(ref reader); + } + protected static (int, int, uint) ReadMessage_iiu(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadInt32(); + var arg1 = reader.ReadInt32(); + var arg2 = reader.ReadUInt32(); + return (arg0, arg1, arg2); + } + protected static ObjectPath ReadMessage_v_o(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("o"); + return reader.ReadObjectPath(); + } + protected static (string, uint)[] ReadMessage_v_arsuz(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("a(su)"); + return ReadType_arsuz(ref reader); + } + protected static Dictionary ReadMessage_v_aeuu(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("a{uu}"); + return ReadType_aeuu(ref reader); + } + protected static int ReadMessage_v_i(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("i"); + return reader.ReadInt32(); + } + protected static (uint, bool) ReadMessage_v_rubz(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("(ub)"); + return ReadType_rubz(ref reader); + } + protected static (uint, uint)[] ReadMessage_v_aruuz(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("a(uu)"); + return ReadType_aruuz(ref reader); + } + protected static (uint, uint) ReadMessage_v_ruuz(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("(uu)"); + return ReadType_ruuz(ref reader); + } + protected static (string, Dictionary[]) ReadMessage_saaesv(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = ReadType_aaesv(ref reader); + return (arg0, arg1); + } + protected static (uint, Dictionary) ReadMessage_v_ruaesvz(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("(ua{sv})"); + return ReadType_ruaesvz(ref reader); + } + protected static Dictionary ReadMessage_aesv(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return reader.ReadDictionaryOfStringToVariantValue(); + } + protected static (uint, bool, byte[])[] ReadMessage_v_arubayz(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("a(ubay)"); + return ReadType_arubayz(ref reader); + } + protected static bool ReadMessage_b(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + protected static byte[] ReadMessage_v_ay(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("ay"); + return reader.ReadArrayOfByte(); + } + protected static (uint, VariantValue) ReadMessage_v_ruvz(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("(uv)"); + return ReadType_ruvz(ref reader); + } + protected static (string, string) ReadMessage_v_rssz(Message message, ModemManager1Object _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("(ss)"); + return ReadType_rssz(ref reader); + } + protected static Dictionary ReadType_aeuv(ref Reader reader) + { + Dictionary dictionary = new(); + ArrayEnd dictEnd = reader.ReadDictionaryStart(); + while (reader.HasNext(dictEnd)) + { + var key = reader.ReadUInt32(); + var value = reader.ReadVariantValue(); + dictionary[key] = value; + } + return dictionary; + } + protected static (string, uint)[] ReadType_arsuz(ref Reader reader) + { + List<(string, uint)> list = new(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + list.Add(ReadType_rsuz(ref reader)); + } + return list.ToArray(); + } + protected static (string, uint) ReadType_rsuz(ref Reader reader) + { + return (reader.ReadString(), reader.ReadUInt32()); + } + protected static Dictionary ReadType_aeuu(ref Reader reader) + { + Dictionary dictionary = new(); + ArrayEnd dictEnd = reader.ReadDictionaryStart(); + while (reader.HasNext(dictEnd)) + { + var key = reader.ReadUInt32(); + var value = reader.ReadUInt32(); + dictionary[key] = value; + } + return dictionary; + } + protected static (uint, bool) ReadType_rubz(ref Reader reader) + { + return (reader.ReadUInt32(), reader.ReadBool()); + } + protected static (uint, uint)[] ReadType_aruuz(ref Reader reader) + { + List<(uint, uint)> list = new(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + list.Add(ReadType_ruuz(ref reader)); + } + return list.ToArray(); + } + protected static (uint, uint) ReadType_ruuz(ref Reader reader) + { + return (reader.ReadUInt32(), reader.ReadUInt32()); + } + protected static (uint, Dictionary) ReadType_ruaesvz(ref Reader reader) + { + return (reader.ReadUInt32(), reader.ReadDictionaryOfStringToVariantValue()); + } + protected static (uint, bool, byte[])[] ReadType_arubayz(ref Reader reader) + { + List<(uint, bool, byte[])> list = new(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(arrayEnd)) + { + list.Add(ReadType_rubayz(ref reader)); + } + return list.ToArray(); + } + protected static (uint, bool, byte[]) ReadType_rubayz(ref Reader reader) + { + return (reader.ReadUInt32(), reader.ReadBool(), reader.ReadArrayOfByte()); + } + protected static (uint, VariantValue) ReadType_ruvz(ref Reader reader) + { + return (reader.ReadUInt32(), reader.ReadVariantValue()); + } + protected static (string, string) ReadType_rssz(ref Reader reader) + { + return (reader.ReadString(), reader.ReadString()); + } + protected static Dictionary>> ReadType_aeoaesaesv(ref Reader reader) + { + Dictionary>> dictionary = new(); + ArrayEnd dictEnd = reader.ReadDictionaryStart(); + while (reader.HasNext(dictEnd)) + { + var key = reader.ReadObjectPath(); + var value = ReadType_aesaesv(ref reader); + dictionary[key] = value; + } + return dictionary; + } + protected static Dictionary> ReadType_aesaesv(ref Reader reader) + { + Dictionary> dictionary = new(); + ArrayEnd dictEnd = reader.ReadDictionaryStart(); + while (reader.HasNext(dictEnd)) + { + var key = reader.ReadString(); + var value = reader.ReadDictionaryOfStringToVariantValue(); + dictionary[key] = value; + } + return dictionary; + } + protected static Dictionary[] ReadType_aaesv(ref Reader reader) + { + List> list = new(); + ArrayEnd arrayEnd = reader.ReadArrayStart(DBusType.Array); + while (reader.HasNext(arrayEnd)) + { + list.Add(reader.ReadDictionaryOfStringToVariantValue()); + } + return list.ToArray(); + } + protected static void WriteType_aeuv(ref MessageWriter writer, Dictionary value) + { + ArrayStart arrayStart = writer.WriteDictionaryStart(); + foreach (var item in value) + { + writer.WriteDictionaryEntryStart(); + writer.WriteUInt32(item.Key); + writer.WriteVariant(item.Value); + } + writer.WriteDictionaryEnd(arrayStart); + } + protected static void WriteType_ruuz(ref MessageWriter writer, (uint, uint) value) + { + writer.WriteStructureStart(); + writer.WriteUInt32(value.Item1); + writer.WriteUInt32(value.Item2); + } + protected static void WriteType_arsuz(ref MessageWriter writer, (string, uint)[] value) + { + ArrayStart arrayStart = writer.WriteArrayStart(DBusType.Struct); + foreach (var item in value) + { + WriteType_rsuz(ref writer, item); + } + writer.WriteArrayEnd(arrayStart); + } + protected static void WriteType_rsuz(ref MessageWriter writer, (string, uint) value) + { + writer.WriteStructureStart(); + writer.WriteString(value.Item1); + writer.WriteUInt32(value.Item2); + } + protected static void WriteType_aeuu(ref MessageWriter writer, Dictionary value) + { + ArrayStart arrayStart = writer.WriteDictionaryStart(); + foreach (var item in value) + { + writer.WriteDictionaryEntryStart(); + writer.WriteUInt32(item.Key); + writer.WriteUInt32(item.Value); + } + writer.WriteDictionaryEnd(arrayStart); + } + protected static void WriteType_rubz(ref MessageWriter writer, (uint, bool) value) + { + writer.WriteStructureStart(); + writer.WriteUInt32(value.Item1); + writer.WriteBool(value.Item2); + } + protected static void WriteType_aruuz(ref MessageWriter writer, (uint, uint)[] value) + { + ArrayStart arrayStart = writer.WriteArrayStart(DBusType.Struct); + foreach (var item in value) + { + WriteType_ruuz(ref writer, item); + } + writer.WriteArrayEnd(arrayStart); + } + protected static void WriteType_ruaesvz(ref MessageWriter writer, (uint, Dictionary) value) + { + writer.WriteStructureStart(); + writer.WriteUInt32(value.Item1); + writer.WriteDictionary(value.Item2); + } + protected static void WriteType_rusz(ref MessageWriter writer, (uint, string) value) + { + writer.WriteStructureStart(); + writer.WriteUInt32(value.Item1); + writer.WriteString(value.Item2); + } + protected static void WriteType_arubayz(ref MessageWriter writer, (uint, bool, byte[])[] value) + { + ArrayStart arrayStart = writer.WriteArrayStart(DBusType.Struct); + foreach (var item in value) + { + WriteType_rubayz(ref writer, item); + } + writer.WriteArrayEnd(arrayStart); + } + protected static void WriteType_rubayz(ref MessageWriter writer, (uint, bool, byte[]) value) + { + writer.WriteStructureStart(); + writer.WriteUInt32(value.Item1); + writer.WriteBool(value.Item2); + writer.WriteArray(value.Item3); + } + protected static void WriteType_ruvz(ref MessageWriter writer, (uint, VariantValue) value) + { + writer.WriteStructureStart(); + writer.WriteUInt32(value.Item1); + writer.WriteVariant(value.Item2); + } + protected static void WriteType_rssz(ref MessageWriter writer, (string, string) value) + { + writer.WriteStructureStart(); + writer.WriteString(value.Item1); + writer.WriteString(value.Item2); + } + } + class PropertyChanges + { + public PropertyChanges(TProperties properties, string[] invalidated, string[] changed) + => (Properties, Invalidated, Changed) = (properties, invalidated, changed); + public TProperties Properties { get; } + public string[] Invalidated { get; } + public string[] Changed { get; } + public bool HasChanged(string property) => Array.IndexOf(Changed, property) != -1; + public bool IsInvalidated(string property) => Array.IndexOf(Invalidated, property) != -1; + } +} diff --git a/DbusTest/ModemManagerHelper.cs b/DbusTest/ModemManagerHelper.cs new file mode 100644 index 0000000..53e241a --- /dev/null +++ b/DbusTest/ModemManagerHelper.cs @@ -0,0 +1,836 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using ModemManager1.DBus; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +using Tmds.DBus.Protocol; + +namespace DbusSmsForward.ModemHelper +{ + public class ModemManagerHelper + { + private readonly Lock _modemObjectPathListLockObject = new(); + public Dictionary>> _modemObjectPathList = new Dictionary>>(); + public string _modemObjectPathNowUse = ""; + public string qmiPathNowUse = ""; + public string _modemManagerObjectPath = "/org/freedesktop/ModemManager1"; + public string? systemBusAddress = Address.System; + public string baseService = "org.freedesktop.ModemManager1"; + public List> smsSendMethodList = new List>(); + public string deviceUidNowUse = ""; + private readonly Lock _subscribeSmsPathListLockObject = new(); + public Dictionary SubscribeSmsPathList=new Dictionary(); + + public ModemManagerHelper() + { + if (!string.IsNullOrEmpty(systemBusAddress)) + { + InitLoadModemObjectPathList(); + WatchModems(); + } + } + + public void InitLoadModemObjectPathList() + { + lock (_modemObjectPathListLockObject) + { + _modemObjectPathList = GetModemsPathList().Result; + } + if (_modemObjectPathList.Count == 1) + { + SetNowUsedModemPath(_modemObjectPathList.First().Key); + } + if (_modemObjectPathList.Count == 0) + { + SetNowUsedModemPath(null); + } + foreach (var item in _modemObjectPathList) + { + SubscribeSms(item.Key); + } + } + + public void SetSendMethodList(List> SendMethodList) + { + smsSendMethodList = SendMethodList; + } + + public void SetNowUsedModemPath(string modemObjectPathNowUse) + { + _modemObjectPathNowUse = modemObjectPathNowUse; + if (string.IsNullOrEmpty(_modemObjectPathNowUse)) + { + qmiPathNowUse = ""; + deviceUidNowUse = ""; + } + else + { + qmiPathNowUse = GetQMIDevice().Result; + deviceUidNowUse = GetDevice().Result; + } + + } + + public void WatchModems() + { + + Task.Run(async () => + { + try + { + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var objectManager = service.CreateObjectManager(_modemManagerObjectPath); + //Console.WriteLine($"Subscribing for modem RemovedChanges"); + await objectManager.WatchInterfacesRemovedAsync( + async (Exception? ex, (ObjectPath ObjectPath, string[] Interfaces) change) => + { + if (ex is null) + { + //Console.WriteLine("modem has been removed,modem path:" + change.ObjectPath.ToString()); + InitLoadModemObjectPathList(); + if (!string.IsNullOrEmpty(_modemObjectPathNowUse)) + { + DisableAndEnableModem().Wait(); + } + } + }); + var tcs = new TaskCompletionSource(); + var task = tcs.Task; + await task; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + }); + Task.Run(async () => + { + try + { + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var objectManager = service.CreateObjectManager(_modemManagerObjectPath); + //Console.WriteLine($"Subscribing for modem AddedChanges"); + await objectManager.WatchInterfacesAddedAsync( + async (Exception? ex, (ObjectPath ObjectPath, Dictionary> InterfacesAndProperties) change) => + { + if (ex is null) + { + //Console.WriteLine("new modem added,modem path:" + change.ObjectPath.ToString()); + InitLoadModemObjectPathList(); + if (!string.IsNullOrEmpty(_modemObjectPathNowUse)) + { + DisableAndEnableModem().Wait(); + } + } + }); + var tcs = new TaskCompletionSource(); + var task = tcs.Task; + await task; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + }); + + } + + public void SubscribeSms(string path) + { + Task.Run(async () => + { + try + { + if ((!SubscribeSmsPathList.ContainsKey(path) || (SubscribeSmsPathList.ContainsKey(path) && !SubscribeSmsPathList[path]))&& _modemObjectPathList.ContainsKey(path)) + { + if (SubscribeSmsPathList.ContainsKey(path)) + { + lock (_subscribeSmsPathListLockObject) + { + SubscribeSmsPathList[path] = true; + } + } + else + { + lock (_subscribeSmsPathListLockObject) + { + SubscribeSmsPathList.Add(path, true); + } + } + //SetMsgDefaultStorageToME(path); + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var messaging = service.CreateMessaging(path); + //Console.WriteLine($"add subscribe sms for modem path:" + path); + await messaging.WatchAddedAsync( + async (Exception? ex, (ObjectPath Path, bool Received) change) => + { + if (ex is null) + { + if (change.Received) + { + try + { + var smsPath = change.Path; + var service = new ModemManager1Service(connection, baseService); + var sms = service.CreateSms(smsPath); + var smsState = await sms.GetStateAsync(); + if (smsState == 2 || smsState == 3) + { + uint Storage = await sms.GetStorageAsync(); + if (!ConfigHelper.JudgeIsForwardIgnore(Storage)) + { + string telNum = await sms.GetNumberAsync(); + //string smsDate = (await sms.GetTimestampAsync()).Replace("T", " ").Replace("+08:00", " "); + + string smsdateOrg = await sms.GetTimestampAsync(); + string smsDate = string.Empty; + try + { + DateTimeOffset dto = DateTimeOffset.Parse(smsdateOrg, null, DateTimeStyles.RoundtripKind); + + // 转换为本地时间 + DateTime localTime = dto.LocalDateTime; + smsDate = localTime.ToString("yyyy-MM-dd HH:mm:ss"); + } + catch (Exception ex1) + { + Console.WriteLine("smsdate转换失败:\n" + ex1.Message); + smsDate = smsdateOrg; + } + int tryCount = 0; + do + { + smsState = await sms.GetStateAsync(); + Thread.Sleep(100); + } while (smsState == 2 && tryCount < 300); + + if (smsState == 3) + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string DeviceName = result.appSettings.DeviceName; + result = null; + if (string.IsNullOrEmpty(DeviceName) || DeviceName== "*Host*Name*") + { + DeviceName= ConfigHelper.GetDeviceHostName(); + } + + string smsContent = await sms.GetTextAsync(); + SmsContentModel smsmodel = new SmsContentModel(); + smsmodel.TelNumber = telNum; + smsmodel.SmsDate = smsDate; + smsmodel.SmsContent = smsContent; + string body = "发信电话:" + telNum + "\n" + "时间:" + smsDate + "\n" + "转发设备:" + DeviceName + "\n" + "短信内容:" + smsContent; + Console.WriteLine("smspath " + smsPath); + if (smsSendMethodList.Count() > 0) + { + //Console.WriteLine("转发方法数量"+smsSendMethodList.Count()); + foreach (var action in smsSendMethodList) + { + action.Invoke(smsmodel, body, DeviceName); + } + } + } + else + { + Console.WriteLine("smspath:" + change.Path + " 接收超时,转发取消"); + } + } + } + } + catch (Exception ex2) + { + Console.WriteLine("GetSmsError" + ex2.Message); + } + + } + } + else + { + Console.WriteLine(ex); + } + }); + Task task = Task.Run(() => + { + var modem = service.CreateModem(path); + + do + { + try + { + var Manufacturer = modem.GetManufacturerAsync().Result; + Manufacturer = null; + Thread.Sleep(1000); + } + catch (Exception ex) + { + break; + } + + } while (SubscribeSmsPathList.ContainsKey(path) && SubscribeSmsPathList[path] && _modemObjectPathList.ContainsKey(path)); + if (SubscribeSmsPathList.ContainsKey(path)) + { + lock (_subscribeSmsPathListLockObject) + { + SubscribeSmsPathList[path] = false; + } + } + }); + await task; + //Console.WriteLine($"remove subscribe sms for modem path:" + path); + } + } + + } + catch (Exception ex) + { + Console.WriteLine(ex); + Console.WriteLine($"remove subscribe sms for modem path:" + path); + } + + + }); + + } + + public async Task SendSms(string telNum, string smsContent) + { + try + { + using (var connection = new Connection(Address.System)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var messaging = service.CreateMessaging(_modemObjectPathNowUse); + var sendsmsPath = await messaging.CreateAsync(new Dictionary { { "text", smsContent }, { "number", telNum } }); + var sms = service.CreateSms(sendsmsPath); + await sms.SendAsync(); + return true; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return false; + } + } + + public async Task>>> GetModemsPathList() + { + try + { + await ScanDevices(); + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var objectManager = service.CreateObjectManager(_modemManagerObjectPath); + return await objectManager.GetManagedObjectsAsync(); + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + return new Dictionary>>(); + } + + public async Task ScanDevices() + { + try + { + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modemManager = service.CreateModemManager1(_modemManagerObjectPath); + await modemManager.ScanDevicesAsync(); + modemManager = null; + service = null; + } + } + catch (Exception ex) + { + if (!(ex.Message.IndexOf("unsupported") >-1)) + { + Console.WriteLine("ScanDevices失败" + ex.Message); + } + } + } + + public async Task GetManufacturer(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var Manufacturer = await modem.GetManufacturerAsync(); + modem = null; + service = null; + return Manufacturer; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + + public async Task JudgeCanGetStatus(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateSimple(actUsePath); + var status = await modem.GetStatusAsync(); + modem = null; + service = null; + return true; + } + } + catch (Exception ex) + { + //Console.WriteLine(ex); + return false; + } + } + + public async Task DisableAndEnableModem(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + if (!string.IsNullOrEmpty(actUsePath)) + { + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + try + { + await modem.EnableAsync(false); + Thread.Sleep(200); + await modem.EnableAsync(true); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + else + { + Console.WriteLine("DisableAndEnableModem:actUsePath为空"); + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + + public async Task InhibitModemDevice(string uid, bool inhibit) + { + try + { + Console.WriteLine("device:" + uid); + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modemManager = service.CreateModemManager1(_modemManagerObjectPath); + await modemManager.InhibitDeviceAsync(uid, inhibit); + modemManager = null; + service = null; + } + } + catch (Exception ex) + { + Console.WriteLine("InhibitModemDevice失败" + ex); + } + } + + public async Task GetDevice(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var device = await modem.GetDeviceAsync(); + modem = null; + service = null; + return device; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + + public async Task GetQMIDevice(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var Ports = await modem.GetPortsAsync(); + foreach (var port in Ports) + { + if (port.Item2 == 6) + { + return "/dev/" + port.Item1; + } + } + return ""; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + + + public async Task GetModel(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var Model = await modem.GetModelAsync(); + modem = null; + service = null; + return Model; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + public async Task GetRevision(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var Revision = await modem.GetRevisionAsync(); + modem = null; + service = null; + return Revision; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + public async Task GetSignalQuality(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var SignalQuality = await modem.GetSignalQualityAsync(); + modem = null; + service = null; + return SignalQuality.Item1.ToString(); + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + public async Task GetOwnNumbers(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var OwnNumbers = await modem.GetOwnNumbersAsync(); + modem = null; + service = null; + return OwnNumbers; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return []; + } + } + + public async Task GetPrimarySimSlot(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var PrimarySimSlot = await modem.GetPrimarySimSlotAsync(); + return PrimarySimSlot; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return null; + } + } + + public async Task GetICCID(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem = service.CreateModem(actUsePath); + var simpath = await modem.GetSimAsync(); + var sim = service.CreateSim(simpath); + var iccid = await sim.GetSimIdentifierAsync(); + modem = null; + service = null; + return iccid; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + + public async Task GetImei(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem3gpp = service.CreateModem3gpp(actUsePath); + var Imei = await modem3gpp.GetImeiAsync(); + modem3gpp = null; + service = null; + return Imei; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + public async Task GetOperatorCode(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem3gpp = service.CreateModem3gpp(actUsePath); + var OperatorCode = await modem3gpp.GetOperatorCodeAsync(); + modem3gpp = null; + service = null; + return OperatorCode; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + public async Task GetOperatorName(string? path = null) + { + try + { + string actUsePath = path == null ? _modemObjectPathNowUse : path; + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var modem3gpp = service.CreateModem3gpp(actUsePath); + var OperatorName = await modem3gpp.GetOperatorNameAsync(); + modem3gpp = null; + service = null; + return OperatorName; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return string.Empty; + } + } + + public async Task SetMsgDefaultStorageToME(string path) + { + try + { + using (var connection = new Connection(systemBusAddress)) + { + await connection.ConnectAsync(); + var service = new ModemManager1Service(connection, baseService); + var messaging = service.CreateMessaging(path); + await messaging.SetDefaultStorageAsync(2); + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + public bool RestartModem() + { + string qmiPath = GetQMIDevice().Result; + uint? simslot = GetPrimarySimSlot().Result; + string usedDevice = deviceUidNowUse; + if (simslot.HasValue) + { + if (simslot.Value == 0) + { + simslot += 1; + } + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "qmicli", + Arguments = @$"-d {qmiPath} -p --uim-sim-power-off={simslot}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + using (Process process = Process.Start(startInfo)) + { + if (process != null) + { + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode != 0) + { + Console.WriteLine(error); + return false; + } + } + else + { + Console.WriteLine("Failed to exec qmicli"); + return false; + } + } + ProcessStartInfo startInfo2 = new ProcessStartInfo + { + FileName = "qmicli", + Arguments = @$"-d {qmiPath} -p --uim-sim-power-on={simslot}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + using (Process process = Process.Start(startInfo2)) + { + if (process != null) + { + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode != 0) + { + Console.WriteLine(error); + return false; + } + } + else + { + Console.WriteLine("Failed to exec qmicli"); + return false; + } + } + if (JudgeCanGetStatus().Result) + { + DisableAndEnableModem().Wait(); + } + Thread.Sleep(200); + if (JudgeCanGetStatus().Result) + { + InhibitModemDevice(usedDevice, true).Wait(); + Thread.Sleep(200); + InhibitModemDevice(usedDevice, false).Wait(); + } + + return true; + + } + else + { + return false; + } + } + + } + +} diff --git a/DbusTest/Notifications/AutmanPushNotificationSender.cs b/DbusTest/Notifications/AutmanPushNotificationSender.cs new file mode 100644 index 0000000..daaf16f --- /dev/null +++ b/DbusTest/Notifications/AutmanPushNotificationSender.cs @@ -0,0 +1,32 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Text.Json.Nodes; + +namespace DbusSmsForward.Notifications; + +public sealed class AutmanPushNotificationSender(DbusSmsForwardOptions options) : NotificationSenderBase("autmanpush") +{ + private readonly AutmanPushConfig _config = options.Notifications.AutmanPush; + + public override bool IsEnabled => _config.Enabled && !string.IsNullOrWhiteSpace(_config.ApiUrl); + + public override async Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + JsonObject payload = new() + { + ["title"] = BuildTitle(sms.TelNumber, string.Empty), + ["content"] = body, + ["deviceName"] = deviceName, + ["phoneNumber"] = sms.TelNumber + }; + + if (!string.IsNullOrWhiteSpace(_config.AccessToken)) + { + payload["accessToken"] = _config.AccessToken; + } + + await HttpHelper.PostAsync(_config.ApiUrl, payload, _config.SkipTlsValidation, cancellationToken); + } +} diff --git a/DbusTest/Notifications/BarkNotificationSender.cs b/DbusTest/Notifications/BarkNotificationSender.cs new file mode 100644 index 0000000..9934d1b --- /dev/null +++ b/DbusTest/Notifications/BarkNotificationSender.cs @@ -0,0 +1,27 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Web; + +namespace DbusSmsForward.Notifications; + +public sealed class BarkNotificationSender(DbusSmsForwardOptions options) : NotificationSenderBase("bark") +{ + private readonly BarkConfig _config = options.Notifications.Bark; + + public override bool IsEnabled => _config.Enabled && !string.IsNullOrWhiteSpace(_config.ServerUrl) && !string.IsNullOrWhiteSpace(_config.BarkKey); + + public override async Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + SmsCodeModel code = NotificationContentBuilder.ExtractCode(options, sms); + string url = $"{_config.ServerUrl.TrimEnd('/')}/{_config.BarkKey}/{HttpUtility.UrlEncode(body)}?group={HttpUtility.UrlEncode(sms.TelNumber)}&title={HttpUtility.UrlEncode(BuildTitle(sms.TelNumber, code.CodeValue))}"; + if (!string.IsNullOrWhiteSpace(code.CodeValue)) + { + url += $"&autoCopy=1©={HttpUtility.UrlEncode(code.CodeValue)}"; + } + + await HttpHelper.HttpGetAsync(url, _config.SkipTlsValidation, cancellationToken); + } +} diff --git a/DbusTest/Notifications/DingTalkNotificationSender.cs b/DbusTest/Notifications/DingTalkNotificationSender.cs new file mode 100644 index 0000000..424e551 --- /dev/null +++ b/DbusTest/Notifications/DingTalkNotificationSender.cs @@ -0,0 +1,47 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json.Nodes; +using System.Web; + +namespace DbusSmsForward.Notifications; + +public sealed class DingTalkNotificationSender(DbusSmsForwardOptions options) : NotificationSenderBase("dingtalk") +{ + private readonly DingTalkConfig _config = options.Notifications.DingTalk; + + public override bool IsEnabled => _config.Enabled && !string.IsNullOrWhiteSpace(_config.AccessToken); + + public override async Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + string url = $"{_config.Endpoint}?access_token={HttpUtility.UrlEncode(_config.AccessToken)}"; + if (!string.IsNullOrWhiteSpace(_config.Secret)) + { + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + string sign = AddSign(timestamp, _config.Secret); + url += $"×tamp={timestamp}&sign={sign}"; + } + + JsonObject payload = new() + { + ["msgtype"] = "text", + ["text"] = new JsonObject + { + ["content"] = body + } + }; + + await HttpHelper.PostAsync(url, payload, _config.SkipTlsValidation, cancellationToken); + } + + private static string AddSign(long timestamp, string secret) + { + string stringToSign = $"{timestamp}\n{secret}"; + using HMACSHA256 hmacsha256 = new(Encoding.UTF8.GetBytes(secret)); + byte[] hashmessage = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); + return HttpUtility.UrlEncode(Convert.ToBase64String(hashmessage), Encoding.UTF8); + } +} diff --git a/DbusTest/Notifications/EmailNotificationSender.cs b/DbusTest/Notifications/EmailNotificationSender.cs new file mode 100644 index 0000000..0f44175 --- /dev/null +++ b/DbusTest/Notifications/EmailNotificationSender.cs @@ -0,0 +1,31 @@ +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using MailKit.Net.Smtp; +using MimeKit; + +namespace DbusSmsForward.Notifications; + +public sealed class EmailNotificationSender(DbusSmsForwardOptions options) : NotificationSenderBase("email") +{ + private readonly EmailConfig _config = options.Notifications.Email; + + public override bool IsEnabled => _config.Enabled && !string.IsNullOrWhiteSpace(_config.SmtpHost) && !string.IsNullOrWhiteSpace(_config.SendEmail) && !string.IsNullOrWhiteSpace(_config.ReceiveEmail); + + public override async Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + SmsCodeModel code = NotificationContentBuilder.ExtractCode(options, sms); + MimeMessage message = new(); + message.From.Add(new MailboxAddress("DbusSmsForward", _config.SendEmail)); + message.To.Add(new MailboxAddress(string.Empty, _config.ReceiveEmail)); + message.Subject = BuildTitle(sms.TelNumber, code.CodeValue); + message.Body = new TextPart("plain") { Text = body }; + + using SmtpClient client = new(); + await client.ConnectAsync(_config.SmtpHost, _config.SmtpPort, _config.EnableSsl, cancellationToken); + await client.AuthenticateAsync(_config.SendEmail, _config.EmailKey, cancellationToken); + await client.SendAsync(message, cancellationToken); + await client.DisconnectAsync(true, cancellationToken); + } +} diff --git a/DbusTest/Notifications/INotificationSender.cs b/DbusTest/Notifications/INotificationSender.cs new file mode 100644 index 0000000..86d0424 --- /dev/null +++ b/DbusTest/Notifications/INotificationSender.cs @@ -0,0 +1,10 @@ +using DbusSmsForward.SMSModel; + +namespace DbusSmsForward.Notifications; + +public interface INotificationSender +{ + string Name { get; } + bool IsEnabled { get; } + Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken); +} diff --git a/DbusTest/Notifications/NotificationSenderBase.cs b/DbusTest/Notifications/NotificationSenderBase.cs new file mode 100644 index 0000000..97b2871 --- /dev/null +++ b/DbusTest/Notifications/NotificationSenderBase.cs @@ -0,0 +1,13 @@ +using DbusSmsForward.SMSModel; + +namespace DbusSmsForward.Notifications; + +public abstract class NotificationSenderBase(string name) : INotificationSender +{ + public string Name { get; } = name; + public abstract bool IsEnabled { get; } + public abstract Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken); + + protected static string BuildTitle(string telNumber, string codeValue) + => string.IsNullOrWhiteSpace(codeValue) ? $"短信转发 {telNumber}" : $"{codeValue} 短信转发 {telNumber}"; +} diff --git a/DbusTest/Notifications/PushPlusNotificationSender.cs b/DbusTest/Notifications/PushPlusNotificationSender.cs new file mode 100644 index 0000000..ee9ad6c --- /dev/null +++ b/DbusTest/Notifications/PushPlusNotificationSender.cs @@ -0,0 +1,31 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.Notifications; +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Net.Http.Json; +using System.Text.Json.Nodes; + +namespace DbusSmsForward.Notifications; + +public sealed class PushPlusNotificationSender(DbusSmsForwardOptions options) : NotificationSenderBase("pushplus") +{ + private readonly PushPlusConfig _config = options.Notifications.PushPlus; + + public override bool IsEnabled => _config.Enabled && !string.IsNullOrWhiteSpace(_config.Token); + + public override async Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + SmsCodeModel code = NotificationContentBuilder.ExtractCode(options, sms); + JsonObject payload = new() + { + ["token"] = _config.Token, + ["title"] = BuildTitle(sms.TelNumber, code.CodeValue), + ["content"] = body, + ["topic"] = string.Empty + }; + + await HttpHelper.PostAsync(_config.Endpoint, payload, _config.SkipTlsValidation, cancellationToken); + } +} diff --git a/DbusTest/Notifications/ShellNotificationSender.cs b/DbusTest/Notifications/ShellNotificationSender.cs new file mode 100644 index 0000000..4ab3236 --- /dev/null +++ b/DbusTest/Notifications/ShellNotificationSender.cs @@ -0,0 +1,51 @@ +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Diagnostics; + +namespace DbusSmsForward.Notifications; + +public sealed class ShellNotificationSender(DbusSmsForwardOptions options) : NotificationSenderBase("shell") +{ + private readonly ShellConfig _config = options.Notifications.Shell; + + public override bool IsEnabled => _config.Enabled && !string.IsNullOrWhiteSpace(_config.ShellPath); + + public override async Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + ProcessStartInfo startInfo = new() + { + FileName = _config.ShellPath, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + foreach (string arg in new[] + { + sms.TelNumber, + sms.SmsDate, + sms.SmsContent, + string.Empty, + string.Empty, + deviceName + }) + { + startInfo.ArgumentList.Add(arg ?? string.Empty); + } + + using Process process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start shell process."); + using CancellationTokenRegistration registration = cancellationToken.Register(() => + { + try { if (!process.HasExited) process.Kill(true); } catch { } + }); + + await process.WaitForExitAsync(cancellationToken); + if (process.ExitCode != 0) + { + string error = await process.StandardError.ReadToEndAsync(cancellationToken); + throw new InvalidOperationException($"Shell sender exited with code {process.ExitCode}: {error}"); + } + } +} diff --git a/DbusTest/Notifications/TelegramNotificationSender.cs b/DbusTest/Notifications/TelegramNotificationSender.cs new file mode 100644 index 0000000..c45df15 --- /dev/null +++ b/DbusTest/Notifications/TelegramNotificationSender.cs @@ -0,0 +1,25 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Web; + +namespace DbusSmsForward.Notifications; + +public sealed class TelegramNotificationSender(DbusSmsForwardOptions options) : NotificationSenderBase("telegram") +{ + private readonly TelegramBotConfig _config = options.Notifications.TelegramBot; + + public override bool IsEnabled => _config.Enabled && !string.IsNullOrWhiteSpace(_config.BotToken) && !string.IsNullOrWhiteSpace(_config.ChatId); + + public override async Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + string apiBase = _config.EnableCustomApi && !string.IsNullOrWhiteSpace(_config.CustomApi) + ? _config.CustomApi.TrimEnd('/') + : "https://api.telegram.org"; + + string text = body; + string url = $"{apiBase}/bot{_config.BotToken}/sendMessage?chat_id={HttpUtility.UrlEncode(_config.ChatId)}&text={HttpUtility.UrlEncode(text)}"; + await HttpHelper.HttpGetAsync(url, _config.SkipTlsValidation, cancellationToken); + } +} diff --git a/DbusTest/Notifications/WeComApplicationNotificationSender.cs b/DbusTest/Notifications/WeComApplicationNotificationSender.cs new file mode 100644 index 0000000..12e0034 --- /dev/null +++ b/DbusTest/Notifications/WeComApplicationNotificationSender.cs @@ -0,0 +1,37 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Text.Json.Nodes; + +namespace DbusSmsForward.Notifications; + +public sealed class WeComApplicationNotificationSender(DbusSmsForwardOptions options) : NotificationSenderBase("wecom") +{ + private readonly WeComApplicationConfig _config = options.Notifications.WeComApplication; + + public override bool IsEnabled => _config.Enabled && !string.IsNullOrWhiteSpace(_config.CorpId) && !string.IsNullOrWhiteSpace(_config.AgentId) && !string.IsNullOrWhiteSpace(_config.ApplicationSecret); + + public override async Task SendAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + string tokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={Uri.EscapeDataString(_config.CorpId)}&corpsecret={Uri.EscapeDataString(_config.ApplicationSecret)}"; + string tokenResponse = await HttpHelper.HttpGetAsync(tokenUrl, _config.SkipTlsValidation, cancellationToken); + JsonObject tokenJson = JsonNode.Parse(tokenResponse)?.AsObject() ?? throw new InvalidOperationException("Invalid WeCom token response."); + string accessToken = tokenJson["access_token"]?.ToString() ?? throw new InvalidOperationException("WeCom access_token missing."); + + JsonObject payload = new() + { + ["touser"] = "@all", + ["msgtype"] = "text", + ["agentid"] = _config.AgentId, + ["text"] = new JsonObject + { + ["content"] = body + }, + ["safe"] = 0 + }; + + string sendUrl = $"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={Uri.EscapeDataString(accessToken)}"; + await HttpHelper.PostAsync(sendUrl, payload, _config.SkipTlsValidation, cancellationToken); + } +} diff --git a/DbusTest/ProcessSmsContent/GetSmsContentCode.cs b/DbusTest/ProcessSmsContent/GetSmsContentCode.cs new file mode 100644 index 0000000..90fc193 --- /dev/null +++ b/DbusTest/ProcessSmsContent/GetSmsContentCode.cs @@ -0,0 +1,137 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Text.RegularExpressions; + +namespace DbusSmsForward.ProcessSmsContent +{ + public static class GetSmsContentCode + { + public static string GetSmsCodeStr(string smscontent) + { + SmsCodeModel smsCodeModel = new SmsCodeModel(); + smsCodeModel = GetSmsCodeModel(smscontent); + if (smsCodeModel.HasCode) + { + return smsCodeModel.CodeFrom + smsCodeModel.CodeValue; + } + else + { + return ""; + } + } + public static SmsCodeModel GetSmsCodeModel(string smscontent) + { + SmsCodeModel smsCodeModel = new SmsCodeModel(); + string newsmscontent=string.Empty; + + if (JudgeSmsContentHasCode(smscontent,out newsmscontent)) + { + string smscode= GetCode(newsmscontent).Trim(); + if (string.IsNullOrEmpty(smscode)) + { + smsCodeModel.HasCode = false; + } + else + { + smsCodeModel.HasCode = true; + smsCodeModel.CodeValue = smscode; + smsCodeModel.CodeFrom = GetCodeSmsFrom(smscontent); + } + } + else + { + smsCodeModel.HasCode = false; + } + return smsCodeModel; + } + public static bool JudgeSmsContentHasCode(string smscontent,out string newsmscontent) + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string codeKeyStr = result.appSettings.SmsCodeKey; + result = null; + string[] flagStrList = {}; + try + { + flagStrList = codeKeyStr.Split("±"); + } + catch (Exception ex) + { + Console.WriteLine("配置文件中自定义验证码关键词格式有误,已使用默认关键词“验证码”"); + flagStrList.Append("验证码"); + } + + foreach (string flag in flagStrList) + { + if (smscontent.IndexOf(flag) > -1) + { + newsmscontent = smscontent.Replace(flag, " "+flag+" "); + return true; + } + } + newsmscontent=string.Empty; + return false; + } + public static string GetCode(string smscontent) + { + string pattern = @"\b[A-Za-z0-9]{4,7}\b"; + MatchCollection SmsCodeMatches = Regex.Matches(smscontent, pattern); + if (SmsCodeMatches.Count()> 1) + { + int maxDigits = 0; + string maxDigitsString = ""; + foreach (Match match in SmsCodeMatches) + { + string currentString = match.Value; + int digitCount = CountDigits(currentString); + if (digitCount > maxDigits) + { + maxDigits = digitCount; + maxDigitsString = currentString; + } + } + return maxDigitsString; + } + else if (SmsCodeMatches.Count()==1) + { + return SmsCodeMatches[0].Value; + } + else + { + return ""; + } + } + public static string GetCodeSmsFrom(string smscontent) + { + string pattern = @"^\【(.*?)\】"; + Match match = Regex.Match(smscontent, pattern); + if (match.Success) + { + return match.Value; + } + else + { + string pattern1 = @"【([^【】]+)】$"; + Match match1 = Regex.Match(smscontent, pattern1); + if (match1.Success) + { + return match1.Value; + } + } + return ""; + } + public static int CountDigits(string input) + { + int count = 0; + foreach (char c in input) + { + if (char.IsDigit(c)) + { + count++; + } + } + return count; + } + } +} diff --git a/DbusTest/ProcessUserChoise/ProcessChoise.cs b/DbusTest/ProcessUserChoise/ProcessChoise.cs new file mode 100644 index 0000000..00f634f --- /dev/null +++ b/DbusTest/ProcessUserChoise/ProcessChoise.cs @@ -0,0 +1,112 @@ +using DbusSmsForward.SendMethod; +using DbusSmsForward.SMSModel; +using System.Collections.Generic; + +namespace DbusSmsForward.ProcessUserChoise +{ + public static class ProcessChoise + { + public static string onStartGuide(string chooseOption) + { + if (string.IsNullOrEmpty(chooseOption)) + { + Console.WriteLine("请选择运行模式:1为短信转发模式,2为发短信模式"); + chooseOption = Console.ReadLine(); + } + if (chooseOption == "1" || chooseOption == "2") + { + return chooseOption; + } + else + { + Console.WriteLine("请输入1或2"); + return onStartGuide(""); + } + + } + + public static List> sendMethodGuide(List chooseOptions) + { + if(chooseOptions.Count() == 0) + { + Console.WriteLine("请选择转发渠道:1.邮箱转发,2.pushplus转发,3.企业微信转发,4.TG机器人转发,5.钉钉转发,6.Bark转发,7.自定义shell转发,同时转发多渠道请以空格分割编号(举例:1 2 3 5)"); + chooseOptions.Add(Console.ReadLine()); + return sendMethodGuide(chooseOptions); + } + else if (chooseOptions.Count() >= 1) + { + + if (chooseOptions.Count() == 1 && chooseOptions[0].IndexOf(" ") > -1) + { + string chooseOption = chooseOptions[0]; + List newChooseOptions = chooseOption.Split(" ").ToList(); + return sendMethodGuide(newChooseOptions); + } + else + { + if (JudgeChooseIsValid(chooseOptions,true)) + { + List> actions = new List>(); + foreach (var chooseOption in chooseOptions) + { + if (chooseOption == "1") + { + SendByEmail.SetupEmailInfo(); + actions.Add(SendByEmail.SendSms); + } + if (chooseOption == "2") + { + SendByPushPlus.SetupPushPlusInfo(); + actions.Add(SendByPushPlus.SendSms); + } + if (chooseOption == "3") + { + SendByWeComApplication.SetupWeComInfo(); + actions.Add(SendByWeComApplication.SendSms); + } + if (chooseOption == "4") + { + SendByTelegramBot.SetupTGBotInfo(); + actions.Add(SendByTelegramBot.SendSms); + } + if (chooseOption == "5") + { + SendByDingTalkBot.SetupDingtalkBotMsg(); + actions.Add(SendByDingTalkBot.SendSms); + } + if (chooseOption == "6") + { + SendByBark.SetupBarkInfo(); + actions.Add(SendByBark.SendSms); + } + if (chooseOption == "7") + { + SendByShell.SetupCustomShellInfo(); + actions.Add(SendByShell.SendSms); + } + } + return actions; + } + else + { + Console.WriteLine("请输入1或2或3或4或5或6或7"); + return sendMethodGuide(new List()); + } + } + } + return new List>(); + } + public static bool JudgeChooseIsValid(List chooseOptions,bool isCheckAll=false) + { + if (isCheckAll) + { + return chooseOptions.Where(a => a == "1" || a == "2" || a == "3" || a == "4" || a == "5" || a == "6" || a == "7").Count() == chooseOptions.Count(); + } + else + { + return chooseOptions.Where(a => a == "1" || a == "2" || a == "3" || a == "4" || a == "5" || a == "6" || a == "7").Count() > 0; + } + } + + } +} diff --git a/DbusTest/Program.cs b/DbusTest/Program.cs new file mode 100644 index 0000000..26ea27b --- /dev/null +++ b/DbusTest/Program.cs @@ -0,0 +1,73 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.Models; +using DbusSmsForward.ModemHelper; +using DbusSmsForward.Notifications; +using DbusSmsForward.Services; +using DbusSmsForward.SettingModel; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Text.Json; + +AppCommand command = CommandLineParser.Parse(args); + +if (command.PrintSampleConfig) +{ + DbusSmsForwardOptions sample = SampleConfigFactory.Create(); + Console.WriteLine(JsonSerializer.Serialize(sample, SourceGenerationContext.Default.DbusSmsForwardOptions)); + return; +} + +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); +builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false); +builder.Services.Configure(builder.Configuration); +builder.Services.AddSingleton(sp => +{ + DbusSmsForwardOptions options = new(); + builder.Configuration.Bind(options); + LegacyConfigurationBridge.Apply(builder.Configuration, options); + CommandLineConfigurationApplier.Apply(command, options); + return options; +}); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); +builder.Logging.ClearProviders(); +builder.Logging.AddSimpleConsole(); + +using IHost host = builder.Build(); +DbusSmsForwardOptions resolvedOptions = host.Services.GetRequiredService(); + +if (command.ValidateConfig || resolvedOptions.Daemon.ValidateOnStartup) +{ + ConfigurationValidator.Validate(resolvedOptions, command); +} + +if (string.Equals(command.Name, "send", StringComparison.OrdinalIgnoreCase)) +{ + await SendSmsAsync(host.Services, command); + return; +} + +await host.RunAsync(); + +static async Task SendSmsAsync(IServiceProvider services, AppCommand command) +{ + if (string.IsNullOrWhiteSpace(command.PhoneNumber) || string.IsNullOrWhiteSpace(command.Message)) + { + throw new InvalidOperationException("send 模式需要 --to 和 --text 参数。 "); + } + + ModemManagerHelper modemManagerHelper = services.GetRequiredService(); + bool success = await modemManagerHelper.SendSms(command.PhoneNumber, command.Message); + Console.WriteLine(success ? "短信已发送" : "短信发送失败"); +} diff --git a/DbusTest/SMSModel/SmsCodeModel.cs b/DbusTest/SMSModel/SmsCodeModel.cs new file mode 100644 index 0000000..2e74f0d --- /dev/null +++ b/DbusTest/SMSModel/SmsCodeModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DbusSmsForward.SMSModel +{ + public class SmsCodeModel + { + public bool HasCode { get; set; }=false; + public string CodeFrom { get; set; }=string.Empty; + public string CodeValue { get; set; } = string.Empty; + } +} diff --git a/DbusTest/SMSModel/SmsContentModel.cs b/DbusTest/SMSModel/SmsContentModel.cs new file mode 100644 index 0000000..aa2dbd5 --- /dev/null +++ b/DbusTest/SMSModel/SmsContentModel.cs @@ -0,0 +1,8 @@ +namespace DbusSmsForward.SMSModel; + +public sealed class SmsContentModel +{ + public string TelNumber { get; set; } = string.Empty; + public string SmsContent { get; set; } = string.Empty; + public string SmsDate { get; set; } = string.Empty; +} diff --git a/DbusTest/SendMethod/SendByBark.cs b/DbusTest/SendMethod/SendByBark.cs new file mode 100644 index 0000000..57e5b3c --- /dev/null +++ b/DbusTest/SendMethod/SendByBark.cs @@ -0,0 +1,68 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Text.Json; + +namespace DbusSmsForward.SendMethod +{ + public static class SendByBark + { + public static void SetupBarkInfo() + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string BarkUrl = result.appSettings.BarkConfig.BarkUrl; + string BrakKey = result.appSettings.BarkConfig.BarkKey; + + if (string.IsNullOrEmpty(BarkUrl) && string.IsNullOrEmpty(BrakKey)) + { + Console.WriteLine("首次运行请输入Bark服务器地址:"); + BarkUrl = Console.ReadLine().Trim(); + result.appSettings.BarkConfig.BarkUrl = BarkUrl; + Console.WriteLine("首次运行请输入Bark推送key"); + BrakKey = Console.ReadLine().Trim(); + result.appSettings.BarkConfig.BarkKey = BrakKey; + ConfigHelper.UpdateSettings(ref result); + } + result = null; + } + + public static void SendSms(SmsContentModel smsmodel, string body, string devicename) + { + try + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string BarkUrl = result.appSettings.BarkConfig.BarkUrl; + string BrakKey = result.appSettings.BarkConfig.BarkKey; + result = null; + //string SmsCodeStr = GetSmsContentCode.GetSmsCodeStr(smsmodel.SmsContent); + + SmsCodeModel codeResult = GetSmsContentCode.GetSmsCodeModel(smsmodel.SmsContent); + + string url = BarkUrl + "/" + BrakKey + "/"; + url += System.Web.HttpUtility.UrlEncode(body); + url += "?group=" + smsmodel.TelNumber + "&title="+ (string.IsNullOrEmpty(codeResult.CodeValue) ? "" : codeResult.CodeValue + " ") + "短信转发" + smsmodel.TelNumber+(string.IsNullOrEmpty(codeResult.CodeValue) ?"":"&autoCopy=1©="+ codeResult.CodeValue); + string msgresult = HttpHelper.HttpGet(url); + //JsonObject jsonObjresult = JObject.Parse(msgresult); + var jsonObjresult = JsonSerializer.Deserialize(msgresult, SourceGenerationContext.Default.JsonObject); + string status = jsonObjresult["code"].ToString(); + if (status == "200") + { + Console.WriteLine("Bark转发成功"); + } + else + { + Console.WriteLine(jsonObjresult["code"].ToString()); + Console.WriteLine(jsonObjresult["message"].ToString()); + } + } + catch(Exception ex) + { + Console.WriteLine("SendByBarkError:\n"+ex); + } + + } + } +} diff --git a/DbusTest/SendMethod/SendByDingTalkBot.cs b/DbusTest/SendMethod/SendByDingTalkBot.cs new file mode 100644 index 0000000..891d5f9 --- /dev/null +++ b/DbusTest/SendMethod/SendByDingTalkBot.cs @@ -0,0 +1,105 @@ +using DbusSmsForward.Helper; +using System.Security.Cryptography; +using System.Text; +using System.Web; +using DbusSmsForward.SMSModel; +using DbusSmsForward.ProcessSmsContent; +using System.Text.Json; +using System.Text.Json.Nodes; +using DbusSmsForward.SettingModel; + +namespace DbusSmsForward.SendMethod +{ + public class SendByDingTalkBot + { + private const string DING_TALK_BOT_URL = "https://oapi.dingtalk.com/robot/send?access_token="; + + public static void SetupDingtalkBotMsg() + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string DingTalkAccessToken = result.appSettings.DingTalkConfig.DingTalkAccessToken; + string DingTalkSecret = result.appSettings.DingTalkConfig.DingTalkSecret; + + if (string.IsNullOrEmpty(DingTalkAccessToken) && string.IsNullOrEmpty(DingTalkSecret)) + { + Console.WriteLine("首次运行请输入钉钉机器人AccessToken:"); + DingTalkAccessToken = Console.ReadLine().Trim(); + result.appSettings.DingTalkConfig.DingTalkAccessToken = DingTalkAccessToken; + Console.WriteLine("请输入钉钉机器人加签secret:"); + DingTalkSecret = Console.ReadLine().Trim(); + result.appSettings.DingTalkConfig.DingTalkSecret = DingTalkSecret; + ConfigHelper.UpdateSettings(ref result); + } + result = null; + } + + public static void SendSms(SmsContentModel smsmodel, string body, string devicename) + { + try + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string dingTalkAccessToken = result.appSettings.DingTalkConfig.DingTalkAccessToken; + string dingTalkSecret = result.appSettings.DingTalkConfig.DingTalkSecret; + result = null; + string url = DING_TALK_BOT_URL + dingTalkAccessToken; + //string SmsCodeStr = GetSmsContentCode.GetSmsCodeStr(smsmodel.SmsContent); + SmsCodeModel codeResult = GetSmsContentCode.GetSmsCodeModel(smsmodel.SmsContent); + + long timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + string sign = addSign(timestamp, dingTalkSecret); + url += $"×tamp={timestamp}&sign={sign}"; + + JsonObject msgContent = new() + { + { "content", (string.IsNullOrEmpty(codeResult.CodeValue) ? "" : codeResult.CodeValue + "\n") + "短信转发\n" + body } + }; + + JsonObject msgObj = new() + { + { "msgtype", "text" }, + { "text", msgContent } + }; + + string resultResp = HttpHelper.Post(url, msgObj); + //JObject jsonObjresult = JObject.Parse(resultResp); + JsonObject jsonObjresult = JsonSerializer.Deserialize(resultResp, SourceGenerationContext.Default.JsonObject); + string errcode1 = jsonObjresult["errcode"].ToString(); + string errmsg1 = jsonObjresult["errmsg"].ToString(); + if (errcode1 == "0" && errmsg1 == "ok") + { + Console.WriteLine("钉钉转发成功"); + } + else + { + Console.WriteLine(errmsg1); + } + } + catch (Exception ex) + { + Console.WriteLine("SendByDingTalkBotError:\n" + ex); + } + + } + + public static string addSign(long timestamp ,string secret) + { + string secret1 = secret; + string stringToSign = timestamp + "\n" + secret1; + var encoding = new ASCIIEncoding(); + byte[] keyByte = encoding.GetBytes(secret1); + byte[] messageBytes = encoding.GetBytes(stringToSign); + using (var hmacsha256 = new HMACSHA256(keyByte)) + { + byte[] hashmessage = hmacsha256.ComputeHash(messageBytes); + return HttpUtility.UrlEncode(Convert.ToBase64String(hashmessage), Encoding.UTF8); + } + } + + public static string Base64Encrypt(string input, Encoding encode) + { + return Convert.ToBase64String(encode.GetBytes(input)); + } + } +} diff --git a/DbusTest/SendMethod/SendByEmail.cs b/DbusTest/SendMethod/SendByEmail.cs new file mode 100644 index 0000000..2d5dc25 --- /dev/null +++ b/DbusTest/SendMethod/SendByEmail.cs @@ -0,0 +1,99 @@ +using DbusSmsForward.SMSModel; +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.Helper; +using DbusSmsForward.SettingModel; +using MimeKit; +using MailKit.Net.Smtp; + +namespace DbusSmsForward.SendMethod +{ + public static class SendByEmail + { + public static void SetupEmailInfo() + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string smtpHost = result.appSettings.EmailConfig.smtpHost; + string smtpPort = result.appSettings.EmailConfig.smtpPort; + string emailKey = result.appSettings.EmailConfig.emailKey; + string sendEmail = result.appSettings.EmailConfig.sendEmail; + string reciveEmail = result.appSettings.EmailConfig.reciveEmail; + if (string.IsNullOrEmpty(smtpHost) && string.IsNullOrEmpty(smtpPort) && string.IsNullOrEmpty(emailKey) && string.IsNullOrEmpty(sendEmail) && string.IsNullOrEmpty(reciveEmail)) + { + Console.WriteLine("首次运行请输入邮箱转发相关配置信息\n请输入smtp地址:"); + smtpHost = Console.ReadLine().Trim(); + result.appSettings.EmailConfig.smtpHost = smtpHost; + + Console.WriteLine("请输入smtp端口:"); + smtpPort = Console.ReadLine().Trim(); + result.appSettings.EmailConfig.smtpPort = smtpPort; + string sslEnableInput = string.Empty; + do + { + Console.WriteLine("是否需要启用ssl(1.启用 2.不启用)"); + sslEnableInput = Console.ReadLine().Trim(); + } while (!(sslEnableInput == "1" || sslEnableInput == "2")); + if (sslEnableInput == "1") + { + result.appSettings.EmailConfig.enableSSL = "true"; + } + else + { + result.appSettings.EmailConfig.enableSSL = "false"; + } + Console.WriteLine("请输入邮箱密钥:"); + emailKey = Console.ReadLine().Trim(); + result.appSettings.EmailConfig.emailKey = emailKey; + + Console.WriteLine("请输入发件邮箱:"); + sendEmail = Console.ReadLine().Trim(); + result.appSettings.EmailConfig.sendEmail = sendEmail; + + Console.WriteLine("请输入收件邮箱:"); + reciveEmail = Console.ReadLine().Trim(); + result.appSettings.EmailConfig.reciveEmail = reciveEmail; + + ConfigHelper.UpdateSettings(ref result); + } + result = null; + } + + public static void SendSms(SmsContentModel smsmodel, string body, string devicename) + { + try + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string smtpHost = result.appSettings.EmailConfig.smtpHost; + string smtpPort = result.appSettings.EmailConfig.smtpPort; + bool enableSSL = Convert.ToBoolean(result.appSettings.EmailConfig.enableSSL); + string emailKey = result.appSettings.EmailConfig.emailKey; + string sendEmail = result.appSettings.EmailConfig.sendEmail; + string reciveEmail = result.appSettings.EmailConfig.reciveEmail; + result = null; + var message = new MimeMessage(); + message.From.Add(new MailboxAddress("SMSForwad", sendEmail)); + message.To.Add(new MailboxAddress("", reciveEmail)); + //string SmsCodeStr = GetSmsContentCode.GetSmsCodeStr(smsmodel.SmsContent); + SmsCodeModel codeResult = GetSmsContentCode.GetSmsCodeModel(smsmodel.SmsContent); + + message.Subject = (string.IsNullOrEmpty(codeResult.CodeValue) ? "" : codeResult.CodeValue + " ") + "短信转发" + smsmodel.TelNumber; + message.Body = new TextPart("plain") + { + Text = body + }; + using (var client = new SmtpClient()) + { + client.Connect(smtpHost, Convert.ToInt32(smtpPort), enableSSL); + client.Authenticate(sendEmail, emailKey); + client.Send(message); + client.Disconnect(true); + } + } + catch (Exception ex) + { + Console.WriteLine("SendByEmailError:\n" + ex); + } + } + } +} diff --git a/DbusTest/SendMethod/SendByPushPlus.cs b/DbusTest/SendMethod/SendByPushPlus.cs new file mode 100644 index 0000000..9b7112d --- /dev/null +++ b/DbusTest/SendMethod/SendByPushPlus.cs @@ -0,0 +1,66 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace DbusSmsForward.SendMethod +{ + public static class SendByPushPlus + { + public static void SetupPushPlusInfo() + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string pushPlusToken = result.appSettings.PushPlusConfig.pushPlusToken; + if (string.IsNullOrEmpty(pushPlusToken)) + { + Console.WriteLine("首次运行请输入PushPlusToken:"); + pushPlusToken = Console.ReadLine().Trim(); + result.appSettings.PushPlusConfig.pushPlusToken = pushPlusToken; + ConfigHelper.UpdateSettings(ref result); + } + result = null; + + } + + public static void SendSms(SmsContentModel smsmodel, string body, string devicename) + { + try + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string pushPlusToken = result.appSettings.PushPlusConfig.pushPlusToken; + result = null; + string pushPlusUrl = "http://www.pushplus.plus/send/"; + //string SmsCodeStr = GetSmsContentCode.GetSmsCodeStr(smsmodel.SmsContent); + SmsCodeModel codeResult = GetSmsContentCode.GetSmsCodeModel(smsmodel.SmsContent); + + JsonObject obj = new JsonObject(); + obj.Add("token", pushPlusToken); + obj.Add("title", (string.IsNullOrEmpty(codeResult.CodeValue) ? "" : codeResult.CodeValue + " ") + "短信转发" + smsmodel.TelNumber); + obj.Add("content", body); + obj.Add("topic", ""); + string msgresult = HttpHelper.Post(pushPlusUrl, obj); + //JObject jsonObjresult = JObject.Parse(msgresult); + JsonObject jsonObjresult = JsonSerializer.Deserialize(msgresult, SourceGenerationContext.Default.JsonObject); + string code = jsonObjresult["code"].ToString(); + string errmsg = jsonObjresult["msg"].ToString(); + if (code == "200") + { + Console.WriteLine("pushplus转发成功"); + } + else + { + Console.WriteLine(errmsg); + } + } + catch (Exception ex) + { + Console.WriteLine("SetupPushPlusInfoError:\n" + ex); + } + + } + } +} diff --git a/DbusTest/SendMethod/SendByShell.cs b/DbusTest/SendMethod/SendByShell.cs new file mode 100644 index 0000000..242b514 --- /dev/null +++ b/DbusTest/SendMethod/SendByShell.cs @@ -0,0 +1,125 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Xml; + +namespace DbusSmsForward.SendMethod +{ + public static class SendByShell + { + public static void SetupCustomShellInfo() + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string shellPath = result.appSettings.ShellConfig.ShellPath; + if (string.IsNullOrEmpty(shellPath)) + { + Console.WriteLine("首次运行请输入自定义shell脚本路径:"); + shellPath = Console.ReadLine().Trim(); + result.appSettings.ShellConfig.ShellPath = shellPath; + ConfigHelper.UpdateSettings(ref result); + } + result = null; + } + + public static void SendSms(SmsContentModel smsmodel, string body, string devicename) + { + try + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string shellPath = result.appSettings.ShellConfig.ShellPath; + result = null; + + SmsCodeModel codeResult = GetSmsContentCode.GetSmsCodeModel(smsmodel.SmsContent); + string[] arguments = + { + string.IsNullOrEmpty(smsmodel.TelNumber)?"":smsmodel.TelNumber, + string.IsNullOrEmpty(smsmodel.SmsDate)?"":smsmodel.SmsDate, + string.IsNullOrEmpty(smsmodel.SmsContent)?"":smsmodel.SmsContent, + string.IsNullOrEmpty(codeResult.CodeValue)?"":codeResult.CodeValue, + string.IsNullOrEmpty(codeResult.CodeFrom)?"":codeResult.CodeFrom, + string.IsNullOrEmpty(devicename)?"":devicename, + }; + + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = shellPath, + UseShellExecute = false, // 不直接使用操作系统shell启动 + CreateNoWindow = true, // 不创建新窗口 + RedirectStandardOutput = true, // 可以重定向输出 + RedirectStandardError = true // 可以重定向错误 + }; + foreach (var arg in arguments) + { + startInfo.ArgumentList.Add(arg); + } + + // 启动进程 + using (Process process = new Process()) + { + process.StartInfo = startInfo; + + // 可以捕获输出(如果需要) + process.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + Console.WriteLine($"输出: {e.Data}"); + }; + + process.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + Console.WriteLine($"错误: {e.Data}"); + }; + + bool started = process.Start(); + + if (started) + { + // 开始异步读取输出 + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // 等待进程完成(可以设置超时时间) + bool exited = process.WaitForExit(30000); // 30秒超时 + + if (exited) + { + int exitCode = process.ExitCode; + + if (exitCode == 0) + { + Console.WriteLine("\nShell调用成功"); + } + else + { + Console.WriteLine($"\nShell调用失败,退出代码: {exitCode}"); + } + } + else + { + Console.WriteLine("\nShell调用超时"); + process.Kill(); // 强制终止进程 + } + } + else + { + Console.WriteLine("\n无法启动Shell进程"); + } + } + } + catch (Exception ex) + { + Console.WriteLine("SendByShellError:\n" + ex); + } + } + } +} diff --git a/DbusTest/SendMethod/SendByTelegramBot.cs b/DbusTest/SendMethod/SendByTelegramBot.cs new file mode 100644 index 0000000..44b3e0a --- /dev/null +++ b/DbusTest/SendMethod/SendByTelegramBot.cs @@ -0,0 +1,100 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace DbusSmsForward.SendMethod +{ + public static class SendByTelegramBot + { + public static void SetupTGBotInfo() + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string TGBotToken = result.appSettings.TGBotConfig.TGBotToken; + string TGBotChatID = result.appSettings.TGBotConfig.TGBotChatID; + string IsEnableCustomTGBotApi = result.appSettings.TGBotConfig.IsEnableCustomTGBotApi; + string CustomTGBotApi = result.appSettings.TGBotConfig.CustomTGBotApi; + + if (string.IsNullOrEmpty(TGBotToken) || string.IsNullOrEmpty(TGBotChatID) || string.IsNullOrEmpty(IsEnableCustomTGBotApi)) + { + Console.WriteLine("首次运行请输入TG机器人Token:"); + TGBotToken = Console.ReadLine().Trim(); + result.appSettings.TGBotConfig.TGBotToken = TGBotToken; + Console.WriteLine("请输入机器人要转发到的ChatId"); + TGBotChatID = Console.ReadLine().Trim(); + result.appSettings.TGBotConfig.TGBotChatID = TGBotChatID; + + string customApiEnableInput=string.Empty; + do + { + Console.WriteLine("是否需要使用自定义api(1.使用 2.不使用)"); + customApiEnableInput= Console.ReadLine().Trim(); + } while (!(customApiEnableInput=="1"|| customApiEnableInput == "2")); + if(customApiEnableInput=="1") + { + IsEnableCustomTGBotApi = "true"; + result.appSettings.TGBotConfig.IsEnableCustomTGBotApi = IsEnableCustomTGBotApi; + Console.WriteLine("请输入机器人自定义api(格式https://xxx.abc.com)"); + CustomTGBotApi= Console.ReadLine().Trim(); + result.appSettings.TGBotConfig.CustomTGBotApi = CustomTGBotApi; + } + else + { + IsEnableCustomTGBotApi = "false"; + result.appSettings.TGBotConfig.IsEnableCustomTGBotApi = IsEnableCustomTGBotApi; + } + ConfigHelper.UpdateSettings(ref result); + } + result = null; + + } + + public static void SendSms(SmsContentModel smsmodel, string body, string devicename) + { + try + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string TGBotToken = result.appSettings.TGBotConfig.TGBotToken; + string TGBotChatID = result.appSettings.TGBotConfig.TGBotChatID; + string IsEnableCustomTGBotApi = result.appSettings.TGBotConfig.IsEnableCustomTGBotApi; + string CustomTGBotApi = result.appSettings.TGBotConfig.CustomTGBotApi; + result = null; + //string SmsCodeStr = GetSmsContentCode.GetSmsCodeStr(smsmodel.SmsContent); + SmsCodeModel codeResult = GetSmsContentCode.GetSmsCodeModel(smsmodel.SmsContent); + string url = ""; + if (IsEnableCustomTGBotApi == "true") + { + url = CustomTGBotApi; + } + else + { + url = "https://api.telegram.org"; + } + url += "/bot" + TGBotToken + "/sendMessage?chat_id=" + TGBotChatID + "&text="; + url += System.Web.HttpUtility.UrlEncode((string.IsNullOrEmpty(codeResult.CodeValue) ? "" : codeResult.CodeValue + "\n") + "短信转发\n" + body); + string msgresult = HttpHelper.HttpGet(url); + JsonObject jsonObjresult = JsonSerializer.Deserialize(msgresult, SourceGenerationContext.Default.JsonObject); + string status = jsonObjresult["ok"].ToString(); + if (status.ToLower() == "true") + { + Console.WriteLine("TGBot转发成功"); + } + else + { + Console.WriteLine(jsonObjresult["error_code"].ToString()); + Console.WriteLine(jsonObjresult["description"].ToString()); + } + } + catch (Exception ex) + { + Console.WriteLine("SendByTelegramBotError:\n" + ex); + } + } + + + } +} diff --git a/DbusTest/SendMethod/SendByWeComApplication.cs b/DbusTest/SendMethod/SendByWeComApplication.cs new file mode 100644 index 0000000..d199446 --- /dev/null +++ b/DbusTest/SendMethod/SendByWeComApplication.cs @@ -0,0 +1,103 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.SettingModel; +using DbusSmsForward.SMSModel; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace DbusSmsForward.SendMethod +{ + public class SendByWeComApplication + { + public static void SetupWeComInfo() + { + appsettingsModel result = new appsettingsModel(); + ConfigHelper.GetSettings(ref result); + string corpid = result.appSettings.WeComApplicationConfig.WeChatQYID; + string appsecret = result.appSettings.WeComApplicationConfig.WeChatQYApplicationSecret; + string appid = result.appSettings.WeComApplicationConfig.WeChatQYApplicationID; + + if (string.IsNullOrEmpty(corpid) && string.IsNullOrEmpty(appsecret) && string.IsNullOrEmpty(appid)) + { + Console.WriteLine("首次运行请输入企业ID:"); + corpid = Console.ReadLine().Trim(); + result.appSettings.WeComApplicationConfig.WeChatQYID = corpid; + + Console.WriteLine("请输入自建应用ID:"); + appid = Console.ReadLine().Trim(); + result.appSettings.WeComApplicationConfig.WeChatQYApplicationID = appid; + + Console.WriteLine("请输入自建应用密钥:"); + appsecret = Console.ReadLine().Trim(); + result.appSettings.WeComApplicationConfig.WeChatQYApplicationSecret = appsecret; + + ConfigHelper.UpdateSettings(ref result); + } + result = null; + + } + + public static void SendSms(SmsContentModel smsmodel, string body, string devicename) + { + try + { + appsettingsModel configResult = new appsettingsModel(); + ConfigHelper.GetSettings(ref configResult); + string corpid = configResult.appSettings.WeComApplicationConfig.WeChatQYID; + string corpsecret = configResult.appSettings.WeComApplicationConfig.WeChatQYApplicationSecret; + string agentid = configResult.appSettings.WeComApplicationConfig.WeChatQYApplicationID; + configResult = null; + + string url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpid + "&corpsecret=" + corpsecret; + string result = HttpHelper.HttpGet(url); + //JsonObject jsonObj = JsonObject.Parse(result); + JsonObject jsonObj = JsonSerializer.Deserialize(result, SourceGenerationContext.Default.JsonObject); + string errcode = jsonObj["errcode"].ToString(); + string errmsg = jsonObj["errmsg"].ToString(); + //string SmsCodeStr = GetSmsContentCode.GetSmsCodeStr(smsmodel.SmsContent); + SmsCodeModel codeResult = GetSmsContentCode.GetSmsCodeModel(smsmodel.SmsContent); + + if (errcode == "0" && errmsg == "ok") + { + string access_token = jsonObj["access_token"].ToString(); + JsonObject obj = new JsonObject(); + JsonObject obj1 = new JsonObject(); + obj.Add("touser", "@all"); + obj.Add("toparty", ""); + obj.Add("totag", ""); + obj.Add("msgtype", "text"); + obj.Add("agentid", agentid); + obj1.Add("content", (string.IsNullOrEmpty(codeResult.CodeValue) ? "" : codeResult.CodeValue + "\n") + "短信转发\n" + body); + obj.Add("text", obj1); + obj.Add("safe", 0); + obj.Add("enable_id_trans", 0); + obj.Add("enable_duplicate_check", 0); + obj.Add("duplicate_check_interval", 1800); + string msgurl = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + access_token; + string msgresult = HttpHelper.Post(msgurl, obj); + //JsonObject jsonObjresult = JsonObject.Parse(msgresult); + //JsonObject jsonObjresult = JsonSerializer.Deserialize(msgresult); + JsonObject jsonObjresult = JsonSerializer.Deserialize(msgresult, SourceGenerationContext.Default.JsonObject); + string errcode1 = jsonObjresult["errcode"].ToString(); + string errmsg1 = jsonObjresult["errmsg"].ToString(); + if (errcode1 == "0" && errmsg1 == "ok") + { + Console.WriteLine("企业微信转发成功"); + } + else + { + Console.WriteLine(errmsg1); + } + } + else + { + Console.WriteLine(errmsg); + } + } + catch (Exception ex) + { + Console.WriteLine("SendByWeComApplicationError:\n" + ex); + } + } + } +} diff --git a/DbusTest/Services/CommandLineParser.cs b/DbusTest/Services/CommandLineParser.cs new file mode 100644 index 0000000..b57962a --- /dev/null +++ b/DbusTest/Services/CommandLineParser.cs @@ -0,0 +1,93 @@ +using DbusSmsForward.Helper; +using DbusSmsForward.Models; + +namespace DbusSmsForward.Services; + +public static class CommandLineParser +{ + public static AppCommand Parse(string[] args) + { + if (args.Length == 0) + { + return new AppCommand { Name = "run" }; + } + + List enabledChannels = []; + string commandName = "run"; + string phoneNumber = string.Empty; + string message = string.Empty; + bool validateConfig = false; + bool printSampleConfig = false; + + for (int i = 0; i < args.Length; i++) + { + string arg = args[i]; + switch (arg) + { + case "run": + case "send": + case "validate-config": + case "print-sample-config": + commandName = arg; + break; + case "--to": + if (i + 1 < args.Length) phoneNumber = args[++i]; + break; + case "--text": + if (i + 1 < args.Length) message = args[++i]; + break; + case "-fE": + commandName = "run"; + enabledChannels.Add("email"); + break; + case "-fP": + commandName = "run"; + enabledChannels.Add("pushplus"); + break; + case "-fW": + commandName = "run"; + enabledChannels.Add("wecom"); + break; + case "-fT": + commandName = "run"; + enabledChannels.Add("telegram"); + break; + case "-fD": + commandName = "run"; + enabledChannels.Add("dingtalk"); + break; + case "-fB": + commandName = "run"; + enabledChannels.Add("bark"); + break; + case "-fS": + commandName = "run"; + enabledChannels.Add("shell"); + break; + case "-fA": + commandName = "run"; + enabledChannels.Add("autmanpush"); + break; + case "-sS": + commandName = "send"; + break; + case "--validate-config": + validateConfig = true; + break; + case "--print-sample-config": + printSampleConfig = true; + break; + } + } + + return new AppCommand + { + Name = commandName, + PhoneNumber = phoneNumber, + Message = message, + EnabledChannels = enabledChannels.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(), + ValidateConfig = validateConfig || commandName == "validate-config", + PrintSampleConfig = printSampleConfig || commandName == "print-sample-config" + }; + } +} diff --git a/DbusTest/Services/NotificationContentBuilder.cs b/DbusTest/Services/NotificationContentBuilder.cs new file mode 100644 index 0000000..9726e7c --- /dev/null +++ b/DbusTest/Services/NotificationContentBuilder.cs @@ -0,0 +1,21 @@ +using DbusSmsForward.ProcessSmsContent; +using DbusSmsForward.SMSModel; +using DbusSmsForward.SettingModel; + +namespace DbusSmsForward.Services; + +public static class NotificationContentBuilder +{ + public static string BuildBody(SmsContentModel sms, string deviceName) + => $"发信电话:{sms.TelNumber}\n时间:{sms.SmsDate}\n转发设备:{deviceName}\n短信内容:{sms.SmsContent}"; + + public static SmsCodeModel ExtractCode(DbusSmsForwardOptions options, SmsContentModel sms) + { + if (!options.Forwarding.EnableSmsCodeExtraction) + { + return new SmsCodeModel(); + } + + return GetSmsContentCode.GetSmsCodeModel(sms.SmsContent); + } +} diff --git a/DbusTest/Services/NotificationDispatcher.cs b/DbusTest/Services/NotificationDispatcher.cs new file mode 100644 index 0000000..03dfab5 --- /dev/null +++ b/DbusTest/Services/NotificationDispatcher.cs @@ -0,0 +1,26 @@ +using DbusSmsForward.Notifications; +using DbusSmsForward.SMSModel; +using Microsoft.Extensions.Logging; + +namespace DbusSmsForward.Services; + +public sealed class NotificationDispatcher(IEnumerable senders, ILogger logger) +{ + private readonly IReadOnlyList _senders = senders.Where(static s => s.IsEnabled).ToList(); + + public async Task DispatchAsync(SmsContentModel sms, string body, string deviceName, CancellationToken cancellationToken) + { + foreach (INotificationSender sender in _senders) + { + try + { + await sender.SendAsync(sms, body, deviceName, cancellationToken); + logger.LogInformation("Notification sender {Sender} completed for {PhoneNumber}", sender.Name, sms.TelNumber); + } + catch (Exception ex) + { + logger.LogError(ex, "Notification sender {Sender} failed for {PhoneNumber}", sender.Name, sms.TelNumber); + } + } + } +} diff --git a/DbusTest/Services/SmsForwardWorker.cs b/DbusTest/Services/SmsForwardWorker.cs new file mode 100644 index 0000000..2f0cffb --- /dev/null +++ b/DbusTest/Services/SmsForwardWorker.cs @@ -0,0 +1,23 @@ +using DbusSmsForward.ModemHelper; +using DbusSmsForward.SMSModel; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace DbusSmsForward.Services; + +public sealed class SmsForwardWorker(ModemManagerHelper modemManagerHelper, NotificationDispatcher dispatcher, ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("SmsForwardWorker started."); + modemManagerHelper.SetSendMethodList( + [ + (SmsContentModel sms, string body, string deviceName) => + { + dispatcher.DispatchAsync(sms, body, deviceName, stoppingToken).GetAwaiter().GetResult(); + } + ]); + + await Task.Delay(Timeout.Infinite, stoppingToken); + } +} diff --git a/DbusTest/SourceGenerationContext.cs b/DbusTest/SourceGenerationContext.cs new file mode 100644 index 0000000..21f6585 --- /dev/null +++ b/DbusTest/SourceGenerationContext.cs @@ -0,0 +1,14 @@ +using DbusSmsForward.SettingModel; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace DbusSmsForward; + +[JsonSourceGenerationOptions(WriteIndented = true, PropertyNameCaseInsensitive = true)] +[JsonSerializable(typeof(JsonObject))] +[JsonSerializable(typeof(DbusSmsForwardOptions))] +[JsonSerializable(typeof(LegacyAppsettingsModel))] +[JsonSerializable(typeof(appsettingsModel))] +internal partial class SourceGenerationContext : JsonSerializerContext +{ +} diff --git a/DbusTest/appsettings.json b/DbusTest/appsettings.json new file mode 100644 index 0000000..089baa0 --- /dev/null +++ b/DbusTest/appsettings.json @@ -0,0 +1,40 @@ +{ + "appSettings": { + "EmailConfig": { + "smtpHost": "", + "smtpPort": "", + "enableSSL": "", + "emailKey": "", + "sendEmail": "", + "reciveEmail": "" + }, + "PushPlusConfig": { + "pushPlusToken": "" + }, + "DingTalkConfig": { + "DingTalkAccessToken": "", + "DingTalkSecret": "" + }, + "BarkConfig": { + "BarkUrl": "", + "BarkKey": "" + }, + "WeComApplicationConfig": { + "WeChatQYID": "", + "WeChatQYApplicationID": "", + "WeChatQYApplicationSecret": "" + }, + "TGBotConfig": { + "IsEnableCustomTGBotApi": "", + "CustomTGBotApi": "", + "TGBotToken": "", + "TGBotChatID": "" + }, + "ShellConfig": { + "ShellPath": "" + }, + "SmsCodeKey": "验证码±verification±code±인증±代码±随机码", + "ForwardIgnoreStorageType": "sm", + "DeviceName": "" + } +} \ No newline at end of file diff --git a/DbusTest/appsettingsModel.cs b/DbusTest/appsettingsModel.cs new file mode 100644 index 0000000..f375638 --- /dev/null +++ b/DbusTest/appsettingsModel.cs @@ -0,0 +1,165 @@ +using Tmds.DBus.Protocol; + +namespace DbusSmsForward.SettingModel; + +public sealed class appsettingsModel +{ + public LegacyAppSettings appSettings { get; set; } = new(); +} + +public sealed class LegacyAppsettingsModel +{ + public LegacyAppSettings appSettings { get; set; } = new(); +} + +public sealed class LegacyAppSettings +{ + public EmailConfig EmailConfig { get; set; } = new(); + public PushPlusConfig PushPlusConfig { get; set; } = new(); + public DingTalkConfig DingTalkConfig { get; set; } = new(); + public BarkConfig BarkConfig { get; set; } = new(); + public WeComApplicationConfig WeComApplicationConfig { get; set; } = new(); + public TelegramBotConfig TGBotConfig { get; set; } = new(); + public ShellConfig ShellConfig { get; set; } = new(); + public string SmsCodeKey { get; set; } = "验证码±verification±code±认证±代码±随机码"; + public string ForwardIgnoreStorageType { get; set; } = "sm"; + public string DeviceName { get; set; } = string.Empty; +} + +public sealed class DbusSmsForwardOptions +{ + public DaemonConfig Daemon { get; set; } = new(); + public ModemConfig Modem { get; set; } = new(); + public ForwardingConfig Forwarding { get; set; } = new(); + public NotificationChannels Notifications { get; set; } = new(); + public LegacyCompatibilityConfig LegacyCompatibility { get; set; } = new(); +} + +public sealed class DaemonConfig +{ + public string Mode { get; set; } = "forward"; + public bool ValidateOnStartup { get; set; } = true; + public bool UseSystemd { get; set; } = true; + public string LogLevel { get; set; } = "Information"; +} + +public sealed class ModemConfig +{ + public string SystemBusAddress { get; set; } = Address.System ?? string.Empty; + public string ServiceName { get; set; } = "org.freedesktop.ModemManager1"; + public string ManagerObjectPath { get; set; } = "/org/freedesktop/ModemManager1"; + public bool ScanDevicesOnStartup { get; set; } = true; +} + +public sealed class ForwardingConfig +{ + public string DeviceName { get; set; } = string.Empty; + public string SmsCodeKey { get; set; } = "验证码±verification±code±认证±代码±随机码"; + public string IgnoreStorageType { get; set; } = "sm"; + public bool EnableSmsCodeExtraction { get; set; } = true; + public string[] EnabledChannels { get; set; } = []; +} + +public sealed class NotificationChannels +{ + public EmailConfig Email { get; set; } = new(); + public PushPlusConfig PushPlus { get; set; } = new(); + public DingTalkConfig DingTalk { get; set; } = new(); + public BarkConfig Bark { get; set; } = new(); + public WeComApplicationConfig WeComApplication { get; set; } = new(); + public TelegramBotConfig TelegramBot { get; set; } = new(); + public ShellConfig Shell { get; set; } = new(); + public AutmanPushConfig AutmanPush { get; set; } = new(); +} + +public sealed class LegacyCompatibilityConfig +{ + public bool EnableLegacyCliFlags { get; set; } = true; + public bool WriteLegacyAppSettings { get; set; } = false; +} + +public class ChannelConfigBase +{ + public bool Enabled { get; set; } + public int TimeoutSeconds { get; set; } = 15; + public bool SkipTlsValidation { get; set; } +} + +public sealed class EmailConfig : ChannelConfigBase +{ + public string SmtpHost { get; set; } = string.Empty; + public int SmtpPort { get; set; } = 465; + public bool EnableSsl { get; set; } = true; + public string EmailKey { get; set; } = string.Empty; + public string SendEmail { get; set; } = string.Empty; + public string ReceiveEmail { get; set; } = string.Empty; + + public string smtpHost { get => SmtpHost; set => SmtpHost = value; } + public string smtpPort { get => SmtpPort.ToString(); set => SmtpPort = int.TryParse(value, out int parsed) ? parsed : 465; } + public string enableSSL { get => EnableSsl.ToString().ToLowerInvariant(); set => EnableSsl = bool.TryParse(value, out bool parsed) && parsed; } + public string emailKey { get => EmailKey; set => EmailKey = value; } + public string sendEmail { get => SendEmail; set => SendEmail = value; } + public string reciveEmail { get => ReceiveEmail; set => ReceiveEmail = value; } +} + +public sealed class PushPlusConfig : ChannelConfigBase +{ + public string Token { get; set; } = string.Empty; + public string Endpoint { get; set; } = "https://www.pushplus.plus/send"; + + public string pushPlusToken { get => Token; set => Token = value; } +} + +public sealed class DingTalkConfig : ChannelConfigBase +{ + public string AccessToken { get; set; } = string.Empty; + public string Secret { get; set; } = string.Empty; + public string Endpoint { get; set; } = "https://oapi.dingtalk.com/robot/send"; + + public string DingTalkAccessToken { get => AccessToken; set => AccessToken = value; } + public string DingTalkSecret { get => Secret; set => Secret = value; } +} + +public sealed class BarkConfig : ChannelConfigBase +{ + public string ServerUrl { get; set; } = string.Empty; + public string BarkKey { get; set; } = string.Empty; + + public string BarkUrl { get => ServerUrl; set => ServerUrl = value; } +} + +public sealed class WeComApplicationConfig : ChannelConfigBase +{ + public string CorpId { get; set; } = string.Empty; + public string AgentId { get; set; } = string.Empty; + public string ApplicationSecret { get; set; } = string.Empty; + + public string WeChatQYID { get => CorpId; set => CorpId = value; } + public string WeChatQYApplicationID { get => AgentId; set => AgentId = value; } + public string WeChatQYApplicationSecret { get => ApplicationSecret; set => ApplicationSecret = value; } +} + +public sealed class TelegramBotConfig : ChannelConfigBase +{ + public bool EnableCustomApi { get; set; } + public string CustomApi { get; set; } = string.Empty; + public string BotToken { get; set; } = string.Empty; + public string ChatId { get; set; } = string.Empty; + + public string IsEnableCustomTGBotApi { get => EnableCustomApi.ToString().ToLowerInvariant(); set => EnableCustomApi = bool.TryParse(value, out bool parsed) && parsed; } + public string CustomTGBotApi { get => CustomApi; set => CustomApi = value; } + public string TGBotToken { get => BotToken; set => BotToken = value; } + public string TGBotChatID { get => ChatId; set => ChatId = value; } +} + +public sealed class ShellConfig : ChannelConfigBase +{ + public string ShellPath { get; set; } = string.Empty; +} + +public sealed class AutmanPushConfig : ChannelConfigBase +{ + public string ApiUrl { get; set; } = string.Empty; + public string AccessToken { get; set; } = string.Empty; + public string Method { get; set; } = "POST"; +} diff --git a/Dockerfile.cross-build-x64-arm64 b/Dockerfile.cross-build-x64-arm64 new file mode 100644 index 0000000..a2f6020 --- /dev/null +++ b/Dockerfile.cross-build-x64-arm64 @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/dotnet-buildtools/prereqs:cbl-mariner-2.0-cross-arm64 +COPY --from=mcr.microsoft.com/dotnet/sdk:10.0.101-azurelinux3.0-amd64 /usr/share/dotnet /usr/share/dotnet +RUN if [ -e /usr/bin/dotnet ]; then rm /usr/bin/dotnet; fi +RUN ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet +ENV DOTNET_NOLOGO=true diff --git a/Dockerfile.cross-build-x64-arm64-musl b/Dockerfile.cross-build-x64-arm64-musl new file mode 100644 index 0000000..cc5b868 --- /dev/null +++ b/Dockerfile.cross-build-x64-arm64-musl @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-net10.0-cross-arm64-musl +COPY --from=mcr.microsoft.com/dotnet/sdk:10.0.101-azurelinux3.0-amd64 /usr/share/dotnet /usr/share/dotnet +RUN if [ -e /usr/bin/dotnet ]; then rm /usr/bin/dotnet; fi +RUN ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet +ENV DOTNET_NOLOGO=true diff --git a/README.md b/README.md new file mode 100644 index 0000000..89fabb4 --- /dev/null +++ b/README.md @@ -0,0 +1,230 @@ +# DbusSmsForward + +DbusSmsForward 的重构仓库目前已经具备一套可运行的 **Python 版本主链路**: + +- 配置加载与兼容旧版 `appSettings` +- 文件短信源轮询 +- ModemManager 短信收取/发送抽象 +- 多通知通道分发(含 AutmanPush) +- Web 控制台(运行总览 / 最近事件 / 配置中心 / 短信测试台) +- CLI 运行、配置校验、环境自检、手工发送、Web 服务启动 +- `env-check` 支持 text/json 两种输出,便于人看和自动化集成 +- 单元测试与集成测试 + +仓库里仍保留原始 .NET 版本源码,便于对照迁移;当前建议优先使用 `src/` 下的 Python 实现。 + +--- + +## 当前重构状态 + +已完成的 Python 能力: + +- `validate-config`:配置校验 +- `env-check`:环境自检(检查 Python / 配置 / DBus / ModemManager 依赖) +- `run`:轮询短信源并分发通知 +- `send`:手工触发通知分发;`--real-sms` 走真实短信发送 +- `serve-web`:启动 Web 控制台 +- Web API: + - `GET /api/health` + - `GET /api/system/status` + - `GET /api/dashboard` + - `GET /api/events` + - `GET/POST /api/settings` + - `POST /api/sms/send` + +已覆盖的验证: + +- 47 个自动化测试全部通过 +- `appsettings.python.json` 配置校验通过 +- 文件短信源手工跑通 + +--- + +## 目录说明 + +```text +src/dbussmsforward/ Python 重构主实现 +tests/ 自动化测试 +wwwroot/ Web 控制台静态资源 +DbusTest/ 原始 .NET 实现(迁移参考) +appsettings.python.json Python 示例配置 +``` + +--- + +## 运行要求 + +- Python 3.11+ +- 当前项目采用 `src/` 布局,直接运行时请带上 `PYTHONPATH=src` + +如果要接真实 ModemManager / DBus: + +- 目标机器需要可访问系统 DBus +- 需要存在 `org.freedesktop.ModemManager1` + +--- + +## 快速开始 + +### 1)校验配置 + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli validate-config --config appsettings.python.json +``` + +### 2)环境自检 + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli env-check --config appsettings.python.json +PYTHONPATH=src python3 -m dbussmsforward.cli env-check --config appsettings.python.json --format json +``` + +- 文件短信源场景:检查配置文件、Python 运行时、通知通道、短信文件路径 +- ModemManager 场景:额外检查 `dbus_next`、`dbus-send` / `dbus-monitor` / `mmcli`、systemd 或 OpenWRT init 脚本提示 +- 返回码:`0` 表示全部通过,`2` 表示存在缺项,适合部署脚本或 CI 直接判定 +- `--format json`:输出 `results` + `summary`,适合安装器、CI、健康检查脚本消费 + +### 3)跑文件短信源示例 + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli run --config appsettings.python.json +``` + +### 3)手工发送一条测试短信到通知链路 + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli send --config appsettings.python.json --to 10086 --text '测试短信' +``` + +### 4)真实调用 ModemManager 发送短信 + +前提:配置中的 `sms_source.kind` 为 `modemmanager`。 + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli send --config appsettings.python.json --to 10086 --text '测试短信' --real-sms +``` + +### 5)启动 Web 控制台 + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli serve-web --config appsettings.python.json --host 0.0.0.0 --port 8080 +``` + +打开:`http://127.0.0.1:8080` + +--- + +## 配置示例 + +项目内置了 `appsettings.python.json`,包含: + +- `forwarding.device_name` +- `notifications.console` +- `notifications.autman_push` +- `sms_source.kind=file` +- `sms_source.path=tests/fixtures/messages.json` + +示例里默认启用了 console,AutmanPush 默认关闭,适合本地先验证主链路。 + +--- + +## Web 控制台功能 + +当前 Web 页面的核心能力: + +- 运行总览:服务状态、监听地址、设备名、短信源、启用通道 +- 最近事件:最近短信转发/发送结果,显示成功失败与通道 +- 配置中心:在线修改配置并落盘 +- 短信测试台:直接从网页发起手工测试 +- 操作结果面板:展示最近一次操作返回 + +适合 Debian/OpenWRT 场景下作为单机维护后台。 + +--- + +## 开发者工作流 + +### 跑完整测试 + +```bash +PYTHONPATH=src python3 -m unittest discover -s tests -v +``` + +### 常用单命令回归 + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli validate-config --config appsettings.python.json +PYTHONPATH=src python3 -m dbussmsforward.cli run --config appsettings.python.json +``` + +### 可编辑安装 + +```bash +python3 -m pip install -e . +``` + +安装后可直接使用: + +```bash +dbussmsforward validate-config --config appsettings.python.json +dbussmsforward serve-web --config appsettings.python.json --port 8080 +``` + +--- + +## 兼容性说明 + +Python 版本配置加载器兼容两类结构: + +1. 新结构:`daemon` / `forwarding` / `notifications` / `sms_source` +2. 旧结构:`.NET appSettings` 包装格式 + +其中 Autman Push 相关配置需包含: + +- `ApiUrl` / `api_url` +- `AccessToken` / `access_token` +- `Method` / `method` + +--- + +## 部署 + +本仓库已经补了部署模板,见: + +- `docs/deployment.md` +- `deploy/common/appsettings.deploy.json` +- `deploy/debian/dbussmsforward.service` +- `deploy/debian/install.sh` +- `deploy/openwrt/dbussmsforward.init` +- `deploy/openwrt/install.sh` + +支持两类部署方式: + +1. **Debian + systemd** +2. **OpenWRT + procd** + +服务文件默认使用: + +- 配置路径:`/etc/dbussmsforward/appsettings.json` +- 程序目录:`/opt/dbussmsforward` +- 运行模式:`python3 -m dbussmsforward.cli run --iterations 0` + +`--iterations 0` 表示持续轮询,适合作为守护进程运行。 + +--- + +## 后续建议 + +虽然主链路、文档和基础部署模板已经齐了,但如果继续往正式交付推进,可以按这个顺序继续: + +1. 增加真实 Debian / OpenWRT 机器上的联调记录 +2. 补充打包发布流程(wheel / sdist / release artifact) +3. 基于真实机器反馈继续扩展环境自检项(例如联网探测、服务状态细查) +4. 再决定是否补 Web 登录、权限和更完整的运维页面 + +--- + +## 参考 + +1. [ModemManager API document](https://www.freedesktop.org/software/ModemManager/api/latest/) +2. [Tmds.DBus](https://github.com/tmds/Tmds.DBus) diff --git a/ShellExample/sendbypushplus.sh b/ShellExample/sendbypushplus.sh new file mode 100644 index 0000000..6e9454e --- /dev/null +++ b/ShellExample/sendbypushplus.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# 接受命令行参数 +telnum=$1 +smsdate=$2 +smscontent=$3 +smscode=$4 +smscodefrom=$5 +devicename=$6 + +pushtitle='短信转发 '$telnum +pushcontent='发信电话:'$telnum'\n时间:'$smsdate'\n转发设备:'$devicename'\n短信内容:'$smscontent + +if [ -n "$smscode" ]; then + pushtitle=$smscodefrom$smscode' '$pushtitle +fi + +curl --location 'http://www.pushplus.plus/send/' \ +--header 'Content-Type: application/json' \ +--data '{"token":"你的pushplustoken","title":"'"$pushtitle"'","content":"'"$pushcontent"'"}' diff --git a/appsettings.python.json b/appsettings.python.json new file mode 100644 index 0000000..4ae3187 --- /dev/null +++ b/appsettings.python.json @@ -0,0 +1,31 @@ +{ + "forwarding": { + "device_name": "demo-router" + }, + "notifications": { + "console": { + "enabled": true + }, + "autman_push": { + "enabled": false, + "api_url": "https://autman.example/api/push", + "access_token": "replace-with-token", + "method": "POST" + } + }, + "sms_source": { + "kind": "file", + "path": "tests/fixtures/messages.json", + "delete_after_read": false, + "poll_interval_seconds": 5, + "modem_object_path": "auto", + "system_bus_address": "org.freedesktop.ModemManager1", + "modemmanager_service": "org.freedesktop.ModemManager1", + "storage_blacklist": [ + "sm" + ], + "state_ready_values": [ + 3 + ] + } +} diff --git a/debian-refactor-design.md b/debian-refactor-design.md new file mode 100644 index 0000000..6136738 --- /dev/null +++ b/debian-refactor-design.md @@ -0,0 +1,175 @@ +# DbusSmsForward Debian 深度重构设计 + +## 现状判断 +当前公开仓库并不是 OpenWRT 专用实现,实际上已经围绕 Debian + ModemManager + DBus 工作。但它离“可在 Debian 上长期稳定使用”的目标还有明显差距,主要是工程结构过于原始。 + +## 当前架构主要问题 +1. **纯交互式入口** + - `Program.cs` 通过 `Console.ReadLine()` 完成模式选择、渠道选择、首次配置。 + - 这导致它无法无交互运行,不适合 systemd、daemon、自启动、容器化或远程运维。 + +2. **配置模型不适合服务部署** + - `ConfigHelper` 只读写 `AppContext.BaseDirectory/appsettings.json`。 + - 这意味着配置和程序强绑定,无法自然放到 `/etc/dbussmsforward/`、`~/.config/` 或通过环境变量注入。 + - 也没有配置校验、默认值归一化、密钥与普通配置分层。 + +3. **发送渠道实现高度耦合** + - `ProcessChoise` 既处理交互,又决定启用哪些发送器,又触发各发送器自己的“首次输入配置流程”。 + - 每个 `SendBy*` 类都直接读配置、拼接 HTTP、同步阻塞调用、直接向控制台打印结果。 + - 不利于扩展新渠道,也无法做统一重试、超时、日志与错误处理。 + +4. **HTTP/证书处理有风险** + - `HttpHelper` 全局使用 `DangerousAcceptAnyServerCertificateValidator`。 + - 这在 Debian 长期运行场景下非常危险,等于默认跳过 TLS 校验。 + +5. **同步阻塞 + 弱错误处理** + - 大量 `.Result` 阻塞异步调用。 + - 错误处理基本是 `catch(Exception ex) Console.WriteLine(...)`,缺乏分级日志和可观测性。 + +6. **项目目标平台过窄** + - `DbusSmsForward.csproj` 固定 `RuntimeIdentifiers=linux-musl-arm64` + - 启用 `PublishAot=true` + - `TargetFramework=net10.0` + - 这对 Debian x64/arm64 通用部署非常不友好,也加大本地构建门槛。 + +7. **缺少 daemon/service 概念** + - 没有 `DaemonConfig` 这种运行期控制配置。 + - 没有 systemd unit、工作目录规范、日志规范、停止信号处理、启动健康检查。 + +8. **历史记忆中的配置未在公开仓库中体现** + - 本地历史记忆提到 `AutmanPushConfig` 与 `DaemonConfig`。 + - 当前公开仓库未包含这两项,说明需要在本次 Debian 重构中主动引入统一的新配置结构。 + +## Debian 化重构目标 +### 核心目标 +将项目改造成: +- 非交互式配置驱动 +- 可作为 systemd 服务运行 +- 支持 Debian x64 / arm64 +- 保留现有发送能力 +- 增加统一配置模型与运行控制能力 +- 为未来新增 Autman Push / Webhook / 更多通知渠道预留扩展点 + +## 建议的新架构 +### 1. 宿主层改为 Generic Host +用 .NET Generic Host 替代当前顶层裸 `Program.cs`: +- `Host.CreateApplicationBuilder(args)` +- `IHostedService` / `BackgroundService` +- `ILogger` +- `IOptions` / 配置绑定 +- `ConsoleLifetime` / `Systemd` 集成 + +建议拆成以下角色: +- `SmsForwardWorker`:监听 ModemManager 短信事件并派发 +- `SmsSendService`:主动发送短信 +- `ModemManagerClient`:封装 DBus/ModemManager 访问 +- `NotificationDispatcher`:统一调度多个通知器 +- `INotificationSender`:每种发送渠道一个实现 + +### 2. 配置体系重构 +建议引入新的配置根模型,例如: +- `DaemonConfig` +- `ModemConfig` +- `ForwardingConfig` +- `NotificationTargets` +- `AutmanPushConfig` +- `WebhookConfig` +- `TelegramConfig` +- `EmailConfig` +- `ShellConfig` + +并支持以下优先级: +1. `/etc/dbussmsforward/appsettings.json` +2. `./appsettings.json` +3. 环境变量(如 `DBUSSMSFORWARD__...`) +4. 命令行参数 + +### 3. 命令模式替代交互模式 +建议把交互式入口改成命令式: +- `dbussmsforward run` +- `dbussmsforward send --to 138... --text "..."` +- `dbussmsforward validate-config` +- `dbussmsforward print-sample-config` + +保留旧参数 `-fE/-fP/.../-sS` 作为兼容层,但内部只转换成新命令,不再触发交互式流程。 + +### 4. 发送渠道抽象 +定义统一接口: +- `INotificationSender` + - `string Name` + - `Task SendAsync(SmsMessage sms, CancellationToken ct)` + +每个渠道单独实现: +- `EmailNotificationSender` +- `PushPlusNotificationSender` +- `TelegramNotificationSender` +- `DingTalkNotificationSender` +- `BarkNotificationSender` +- `WeComNotificationSender` +- `ShellNotificationSender` +- `AutmanPushNotificationSender`(新增) + +由 `NotificationDispatcher` 统一完成: +- 启用判断 +- 超时控制 +- 错误隔离 +- 日志记录 +- 可选并发发送 + +### 5. 安全与可靠性修复 +- 默认启用 TLS 证书校验 +- 仅在配置显式允许时跳过证书校验 +- 用 `HttpClientFactory` 替换手写同步 `HttpClient` +- 所有外部调用改为 async/await +- 增加超时、重试(至少对 HTTP 类通知) +- 用结构化日志替代 `Console.WriteLine` + +### 6. 构建与发布策略调整 +建议不要默认强绑 NativeAOT。 +更适合 Debian 的策略: +- 默认:`net8.0` 或 `net9.0` framework-dependent / self-contained +- 可选:额外保留 AOT 发布 profile +- RIDs 至少支持: + - `linux-x64` + - `linux-arm64` + +这样更利于 Debian VPS/小主机/工控盒子直接部署。 + +## 建议分阶段实施 +### Phase A:最小可用 Debian 服务化 +- 改造配置加载 +- 去掉首次运行交互依赖 +- 引入 run/send/validate-config 命令 +- 统一日志 +- 补 systemd unit 示例 + +### Phase B:通知通道抽象化 +- 拆 `SendBy*` 为 sender 实现 +- 引入 dispatcher +- 补 `AutmanPushConfig` +- 允许多渠道并行转发 + +### Phase C:兼容与增强 +- 保留旧 CLI 参数兼容 +- 支持环境变量覆盖 +- 支持跳过证书校验的显式开关 +- 输出 Debian 部署文档 + +## 本次实际重构建议 +如果现在开始直接动代码,我建议这样做: +1. 新建 `Configuration/`、`Services/`、`Notifications/`、`Models/` 目录 +2. 引入新的配置类,其中补上: + - `DaemonConfig` + - `AutmanPushConfig` +3. 重写 `Program.cs` 为 host + command 模式 +4. 保留现有 `ModemManagerHelper`,但包一层服务接口 +5. 逐步把 `SendBy*` 改为异步 sender +6. 补 `appsettings.example.json` +7. 补 `dbussmsforward.service` +8. 最后整理 README 的 Debian 使用方式 + +## 结论 +这次重构的重点不是“把 OpenWRT 代码移植到 Debian”,而是: +**把一个能在 Debian 上跑的个人脚本式 .NET 工具,重构成一个适合 Debian 长期托管运行的服务程序。** + +下一步应进入 **t3:实施重构、补齐配置/服务化/兼容逻辑**。 \ No newline at end of file diff --git a/deploy/common/appsettings.deploy.json b/deploy/common/appsettings.deploy.json new file mode 100644 index 0000000..864470d --- /dev/null +++ b/deploy/common/appsettings.deploy.json @@ -0,0 +1,39 @@ +{ + "daemon": { + "mode": "forward", + "validate_on_startup": true, + "use_systemd": true, + "log_level": "INFO", + "host": "0.0.0.0", + "port": 8080 + }, + "forwarding": { + "device_name": "router-01", + "sms_code_key": "验证码±verification±code±认证±代码±随机码", + "ignore_storage_type": "sm", + "enable_sms_code_extraction": true, + "enabled_channels": ["autman_push"] + }, + "notifications": { + "console": { + "enabled": false + }, + "autman_push": { + "enabled": true, + "api_url": "https://autman.example/api/push", + "access_token": "REPLACE_ME", + "method": "POST" + } + }, + "sms_source": { + "kind": "modemmanager", + "path": "", + "delete_after_read": false, + "poll_interval_seconds": 5, + "system_bus_address": "unix:path=/run/dbus/system_bus_socket", + "modemmanager_service": "org.freedesktop.ModemManager1", + "modem_object_path": "auto", + "storage_blacklist": ["sm"], + "state_ready_values": [3] + } +} diff --git a/deploy/debian/dbussmsforward.service b/deploy/debian/dbussmsforward.service new file mode 100644 index 0000000..743c4e9 --- /dev/null +++ b/deploy/debian/dbussmsforward.service @@ -0,0 +1,21 @@ +[Unit] +Description=DbusSmsForward Python SMS forwarding daemon +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +Group=root +WorkingDirectory=/opt/dbussmsforward +Environment=PYTHONUNBUFFERED=1 +Environment=PYTHONPATH=/opt/dbussmsforward/src +Environment=DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket +ExecStart=/usr/bin/env python3 -m dbussmsforward.cli run --config /etc/dbussmsforward/appsettings.json --iterations 0 +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/debian/install.sh b/deploy/debian/install.sh new file mode 100644 index 0000000..91ded44 --- /dev/null +++ b/deploy/debian/install.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env sh +set -eu + +APP_NAME="dbussmsforward" +ROOT_DIR="${ROOT_DIR:-/}" +APP_DIR="${ROOT_DIR%/}/opt/dbussmsforward" +CONFIG_DIR="${ROOT_DIR%/}/etc/dbussmsforward" +SYSTEMD_UNIT="${ROOT_DIR%/}/etc/systemd/system/${APP_NAME}.service" +CONFIG_FILE="$CONFIG_DIR/appsettings.json" +ENV_REPORT="$CONFIG_DIR/install-env-check.json" +SKIP_SERVICE_ENABLE="${SKIP_SERVICE_ENABLE:-0}" + +mkdir -p "$APP_DIR" "$CONFIG_DIR" "$(dirname "$SYSTEMD_UNIT")" +cp -R src wwwroot pyproject.toml README.md "$APP_DIR/" +if [ ! -f "$CONFIG_FILE" ]; then + cp deploy/common/appsettings.deploy.json "$CONFIG_FILE" +fi +cp deploy/debian/dbussmsforward.service "$SYSTEMD_UNIT" + +if ! PYTHONPATH="$APP_DIR/src" python3 -m dbussmsforward.cli env-check --config "$CONFIG_FILE" --format json > "$ENV_REPORT"; then + echo "Environment check failed. Report saved to $ENV_REPORT" >&2 + cat "$ENV_REPORT" >&2 + exit 1 +fi + +if [ "$SKIP_SERVICE_ENABLE" != "1" ]; then + systemctl daemon-reload + systemctl enable "$APP_NAME" +fi + +echo "Installed to $APP_DIR" +echo "Config: $CONFIG_FILE" +echo "Env check report: $ENV_REPORT" +echo "Start with: systemctl start $APP_NAME" +echo "Logs with: journalctl -u $APP_NAME -f" diff --git a/deploy/openwrt/dbussmsforward.init b/deploy/openwrt/dbussmsforward.init new file mode 100644 index 0000000..461b64c --- /dev/null +++ b/deploy/openwrt/dbussmsforward.init @@ -0,0 +1,34 @@ +#!/bin/sh /etc/rc.common +USE_PROCD=1 +START=95 +STOP=10 + +NAME="dbussmsforward" +APP_DIR="/opt/dbussmsforward" +CONFIG_FILE="/etc/dbussmsforward/appsettings.json" +PYTHON_BIN="/usr/bin/python3" +MODULE="dbussmsforward.cli" + +start_service() { + procd_open_instance + procd_set_param command "$PYTHON_BIN" -m "$MODULE" run --config "$CONFIG_FILE" --iterations 0 + procd_set_param env PYTHONUNBUFFERED=1 + procd_set_param env DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket + procd_set_param env PYTHONPATH="$APP_DIR/src" + procd_set_param env HOME=/root + procd_set_param working_dir "$APP_DIR" + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param respawn 3600 5 5 + procd_set_param file "$CONFIG_FILE" + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "$NAME" +} + +reload_service() { + stop + start +} diff --git a/deploy/openwrt/install.sh b/deploy/openwrt/install.sh new file mode 100644 index 0000000..456af85 --- /dev/null +++ b/deploy/openwrt/install.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu + +APP_NAME="dbussmsforward" +ROOT_DIR="${ROOT_DIR:-/}" +APP_DIR="${ROOT_DIR%/}/opt/dbussmsforward" +CONFIG_DIR="${ROOT_DIR%/}/etc/dbussmsforward" +INIT_SCRIPT="${ROOT_DIR%/}/etc/init.d/${APP_NAME}" +CONFIG_FILE="$CONFIG_DIR/appsettings.json" +ENV_REPORT="$CONFIG_DIR/install-env-check.json" +SKIP_SERVICE_ENABLE="${SKIP_SERVICE_ENABLE:-0}" + +mkdir -p "$APP_DIR" "$CONFIG_DIR" "$(dirname "$INIT_SCRIPT")" +cp -R src wwwroot pyproject.toml README.md "$APP_DIR/" +if [ ! -f "$CONFIG_FILE" ]; then + cp deploy/common/appsettings.deploy.json "$CONFIG_FILE" +fi +cp deploy/openwrt/dbussmsforward.init "$INIT_SCRIPT" +chmod +x "$INIT_SCRIPT" + +if ! PYTHONPATH="$APP_DIR/src" python3 -m dbussmsforward.cli env-check --config "$CONFIG_FILE" --format json > "$ENV_REPORT"; then + echo "Environment check failed. Report saved to $ENV_REPORT" >&2 + cat "$ENV_REPORT" >&2 + exit 1 +fi + +if [ "$SKIP_SERVICE_ENABLE" != "1" ]; then + "$INIT_SCRIPT" enable +fi + +echo "Installed to $APP_DIR" +echo "Config: $CONFIG_FILE" +echo "Env check report: $ENV_REPORT" +echo "Start with: $INIT_SCRIPT start" +echo "Logs with: logread -e ${APP_NAME}" diff --git a/docs-debian-web-plan.md b/docs-debian-web-plan.md new file mode 100644 index 0000000..92d8513 --- /dev/null +++ b/docs-debian-web-plan.md @@ -0,0 +1,371 @@ +# DbusSmsForward Debian Web 版实施计划 + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** 把当前 Debian 版 DbusSmsForward 改造成“带 Web 管理界面、可直接在 Debian 上运行、默认不要求本地编译”的版本。 + +**Architecture:** 保留现有 Generic Host + Worker 的短信转发核心,补一层 ASP.NET Core Minimal API 与静态 `wwwroot` 页面;运行形态改为 `dotnet DbusSmsForward.dll web` 或发布后的自包含目录直接运行。通过配置文件和环境变量驱动,不再依赖交互式初始化。 + +**Tech Stack:** .NET 8, ASP.NET Core Minimal API, Generic Host, Static Files, JSON config, systemd-friendly hosting. + +--- + +## 0. 当前仓库真实状态(已核实) + +**已存在:** +- `DbusTest/Program.cs`:已经是 Generic Host 风格入口。 +- `DbusTest/appsettingsModel.cs`:已引入 `DbusSmsForwardOptions` / `DaemonConfig` / `AutmanPushConfig`。 +- `DbusTest/Services/SmsForwardWorker.cs`:已有后台转发 worker。 +- `DbusTest/Notifications/*`:已拆分通知发送器。 +- `DbusTest/Helper/ConfigHelper.cs`:已支持 `/etc/dbussmsforward/appsettings.json` 优先加载。 + +**缺失:** +- `wwwroot/` 不存在。 +- 没有任何 Web API。 +- `DbusSmsForward.csproj` 仍是 `Microsoft.NET.Sdk.Worker`,不是 Web SDK。 +- `Program.cs` 里引用了若干当前仓库中不存在的类型: + - `SampleConfigFactory` + - `LegacyConfigurationBridge` + - `CommandLineConfigurationApplier` + - `ConfigurationValidator` +- 本机当前 `dotnet` 命令不存在,无法本地编译验证;因此本阶段要优先完成“代码落地 + 文档化运行路径”。 + +--- + +## 1. 目标运行方式 + +### 目标 A:Debian 上直接运行(不在目标机编译) + +推荐两种: + +1. **Framework-dependent** + - 目标机只安装 .NET 8 runtime / aspnetcore runtime + - 直接运行: + - `dotnet DbusSmsForward.dll web --urls http://0.0.0.0:8080` + +2. **Self-contained 发布包** + - 预先在别处发布好 Linux x64 / arm64 包 + - 目标机无需装 dotnet + - 直接运行发布目录中的可执行文件 + +**注意:** 当前用户要求“最好不需要编译,直接运行在 Debian 上”,因此仓库要优先支持 **运行已发布产物**,而不是要求 Debian 机器安装 SDK 编译源码。 + +--- + +## 2. 代码实施任务 + +### Task 1: 切换项目为 Web 宿主 + +**Objective:** 让项目具备 Minimal API + Static Files 能力。 + +**Files:** +- Modify: `DbusTest/DbusSmsForward.csproj` + +**Step 1: 修改 SDK** + +把: + +```xml + +``` + +改成: + +```xml + +``` + +**Step 2: 保留现有依赖,继续复制配置文件** + +保留现有 `PackageReference` 与 `appsettings*.json` 输出策略。 + +**Step 3: 增加 Web 静态资源复制规则(如需要)** + +确认 `wwwroot/**` 默认由 Web SDK 处理。 + +**Verification:** +- 代码层面确认 csproj 已切到 Web SDK。 + +--- + +### Task 2: 补齐缺失的配置与兼容辅助类 + +**Objective:** 解决 `Program.cs` 当前引用缺失导致的不可编译问题。 + +**Files:** +- Create: `DbusTest/Helper/SampleConfigFactory.cs` +- Create: `DbusTest/Helper/LegacyConfigurationBridge.cs` +- Create: `DbusTest/Helper/CommandLineConfigurationApplier.cs` +- Create: `DbusTest/Helper/ConfigurationValidator.cs` + +**Step 1: `SampleConfigFactory`** +- 返回一个最小可用的 `DbusSmsForwardOptions` 示例对象。 +- 含 `Daemon/Modem/Forwarding/Notifications/LegacyCompatibility` 基本结构。 + +**Step 2: `LegacyConfigurationBridge`** +- 从旧格式 `appSettings` 节点读取值,映射到新 `DbusSmsForwardOptions`。 +- 至少映射: + - `DeviceName` + - `SmsCodeKey` + - `ForwardIgnoreStorageType` + - Email / PushPlus / DingTalk / Bark / WeCom / TG / Shell + +**Step 3: `CommandLineConfigurationApplier`** +- 把 `AppCommand.EnabledChannels` 写入 `options.Forwarding.EnabledChannels` +- 如果旧 flags 指定了渠道,则同步将对应 `options.Notifications.*.Enabled = true` + +**Step 4: `ConfigurationValidator`** +- 做最小校验: + - `Daemon.Mode` 合法值:`forward` / `web` / `both` + - 若启用 email,必须有 host / sender / receiver + - 若启用 autmanpush,必须有 ApiUrl +- 校验失败抛 `InvalidOperationException` + +**Verification:** +- `Program.cs` 的缺失引用都能对应到真实文件。 + +--- + +### Task 3: 扩展命令模型支持 web/both 与 HTTP 端口 + +**Objective:** 让启动命令能表达 Web 模式。 + +**Files:** +- Modify: `DbusTest/Models/AppCommand.cs` +- Modify: `DbusTest/Services/CommandLineParser.cs` + +**Step 1: 扩展 `AppCommand` 字段** + +新增: +- `bool WebMode` +- `bool ForwardMode` +- `string Urls` +- `int Port` +- `string ConfigPath` + +**Step 2: 解析新参数** + +新增支持: +- `web` +- `both` +- `--urls http://0.0.0.0:8080` +- `--port 8080` +- `--config /etc/dbussmsforward/appsettings.json` + +**Step 3: 默认策略** +- `run` = 仅 forward +- `web` = 仅 web +- `both` = forward + web +- 当只传 `--urls` / `--port` 时,默认仍按 `web` 理解更合理;若保持兼容性,也可要求显式 `web`。 + +**Verification:** +- 参数设计能清晰表达三种启动形态。 + +--- + +### Task 4: 建立 Web API 服务层 + +**Objective:** 把 Web 层依赖从 `Program.cs` 抽出来,避免一团乱。 + +**Files:** +- Create: `DbusTest/Services/SystemStatusService.cs` +- Create: `DbusTest/Services/SmsWebService.cs` +- Create: `DbusTest/Web/WebDtos.cs` + +**Step 1: `SystemStatusService`** +- 复用 `ModemManagerHelper` 暴露: + - `GetStatusAsync()` + - 返回 modem path / qmi path / device / manufacturer / model / revision / operator / signal + +**Step 2: `SmsWebService`** +- 封装: + - `SendAsync(phone, content)` + - `GetMessagesAsync(limit, phoneFilter, keyword, sort)` + - `GetSettings()` + - `SaveSettings()` +- 若 `ModemManagerHelper` 尚无历史短信读取能力,则先补最小方法;至少支持列表读取。 + +**Step 3: DTO** +- 请求与响应 DTO: + - `SendSmsRequest` + - `SettingsResponse` + - `SmsMessageItem` + - `ApiEnvelope` + +**Verification:** +- Web API 调用不直接散落访问底层 helper。 + +--- + +### Task 5: 在 `ModemManagerHelper` 中补历史短信列表接口 + +**Objective:** 支持 Web 历史短信页。 + +**Files:** +- Modify: `DbusTest/ModemManagerHelper.cs` +- Create: `DbusTest/Web/SmsMessageRecord.cs`(如 DTO 不想放 helper 内) + +**Step 1: 新增异步方法** + +建议新增: + +```csharp +Task> ListMessagesAsync(CancellationToken cancellationToken = default) +``` + +**Step 2: 实现逻辑** +- 使用现有 `CreateMessaging(_modemObjectPathNowUse)` +- 枚举 DBus message path +- 读取: + - Number + - Text + - Timestamp + - Storage + - State +- 做时间转换与空值兜底 + +**Step 3: 不破坏现有 forward 逻辑** +- 保持现有短信监听逻辑不变 +- 新增方法只做读取 + +**Verification:** +- 代码上已经有明确的历史短信读取入口。 + +--- + +### Task 6: 重写 `Program.cs` 为“单进程双模式入口” + +**Objective:** 同一个程序既能跑后台转发,也能跑 Web。 + +**Files:** +- Modify: `DbusTest/Program.cs` + +**Step 1: 统一宿主构建** +- 注册: + - `ModemManagerHelper` + - `NotificationDispatcher` + - 各通知 sender + - `SystemStatusService` + - `SmsWebService` +- 仅在 forward 模式下注册 `SmsForwardWorker` + +**Step 2: Web 模式构建** +- 使用 `WebApplication.CreateBuilder(args)` 或在现有 host 上兼容 Web host +- 开启: + - `UseDefaultFiles()` + - `UseStaticFiles()` + +**Step 3: Minimal API 路由** +- `GET /api/system/status` +- `GET /api/sms/messages` +- `POST /api/sms/send` +- `GET /api/settings` +- `POST /api/settings` +- `GET /api/health` + +**Step 4: 启动逻辑** +- `send`:一次性发短信后退出 +- `run`:只跑后台 worker +- `web`:只跑 Web +- `both`:Web + worker 同时运行 + +**Verification:** +- `Program.cs` 不再只会 `host.RunAsync()` 一条路。 + +--- + +### Task 7: 增加最小可用 Web UI + +**Objective:** 让 BOSS 在浏览器里直接看效果。 + +**Files:** +- Create: `DbusTest/wwwroot/index.html` +- Create: `DbusTest/wwwroot/app.js` +- Create: `DbusTest/wwwroot/app.css` + +**Step 1: 页面结构** +- 菜单: + - 仪表盘 + - 历史短信 + - 发送短信 + - 系统配置 + +**Step 2: 页面能力** +- 仪表盘:加载 `/api/system/status` +- 历史短信:加载 `/api/sms/messages` +- 发送短信:调用 `/api/sms/send` +- 配置:读写 `/api/settings` + +**Step 3: 保持纯静态实现** +- 原生 HTML/CSS/JS +- 不引入前端构建工具 +- 保证部署简单 + +**Verification:** +- 浏览器打开根路径 `/` 能直接进入管理页。 + +--- + +### Task 8: 提供 Debian 直接运行所需文件 + +**Objective:** 让“直接运行在 Debian 上”真正落地。 + +**Files:** +- Create: `DbusTest/appsettings.example.json` +- Create: `DbusTest/dbussmsforward.service` +- Modify: `README.md` + +**Step 1: 示例配置** +- 提供新结构 JSON 样例 +- 默认 `Daemon.Mode = "both"` +- 默认 `urls` 示例写到 README,不强绑进配置 + +**Step 2: systemd unit** +- 示例: + - `WorkingDirectory=/opt/dbussmsforward` + - `ExecStart=/usr/bin/dotnet /opt/dbussmsforward/DbusSmsForward.dll both --urls http://0.0.0.0:8080` + - `Environment=DBUSSMSFORWARD_CONFIG_PATH=/etc/dbussmsforward/appsettings.json` + +**Step 3: README 更新重点** +- 明确区分: + - 运行源码需要 SDK + - 运行发布产物只需 runtime 或无需 runtime(self-contained) +- 给出 Debian 直接运行命令 +- 给出 systemd 安装步骤 + +**Verification:** +- 文档能支撑“别人拿到发布包就在 Debian 跑起来”。 + +--- + +## 3. 本次实施优先级 + +1. 先修复仓库当前“缺失类导致不可编译”的问题 +2. 再补 Web API 骨架 +3. 再补 `wwwroot` 管理页 +4. 最后补 README / example config / systemd + +--- + +## 4. 运行验收标准 + +满足以下即视为达到目标: + +- 可用命令: + - `dotnet DbusSmsForward.dll run` + - `dotnet DbusSmsForward.dll web --urls http://0.0.0.0:8080` + - `dotnet DbusSmsForward.dll both --urls http://0.0.0.0:8080` + - `dotnet DbusSmsForward.dll send --to 13800138000 --text "测试短信"` +- 根路径 `/` 可打开 Web UI +- `/api/health` 返回成功 +- `/api/system/status` 返回 modem 状态 +- `/api/sms/messages` 返回历史短信列表 +- `/api/settings` 可读写配置 +- 提供 Debian 的 systemd unit 示例 + +--- + +## 5. 当前阻塞说明 + +- 本机没有 `dotnet`,所以暂时不能做本地编译/运行验证。 +- 但仓库代码改造、文件补齐、运行文档和 Debian 部署入口可以现在直接完成。 diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..e06773c --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,172 @@ +# Deployment Guide + +This document covers the Python refactor deployment layout for Debian/systemd and OpenWRT/procd. + +## Runtime assumptions + +- Python 3.11+ is available on the target machine. +- The project is deployed as source code under `/opt/dbussmsforward`. +- Runtime config lives at `/etc/dbussmsforward/appsettings.json`. +- For real modem integration, `sms_source.kind` should be `modemmanager` and the host must expose the system D-Bus socket. + +Recommended target layout: + +```text +/opt/dbussmsforward/ + src/ + wwwroot/ + pyproject.toml + README.md +/etc/dbussmsforward/ + appsettings.json +``` + +A starter production-like config is provided at `deploy/common/appsettings.deploy.json`. + +--- + +## Debian / systemd + +### Install dependencies + +```bash +sudo apt update +sudo apt install -y python3 python3-venv python3-pip dbus modemmanager +sudo systemctl enable --now dbus ModemManager +``` + +### Copy project files + +```bash +sudo mkdir -p /opt/dbussmsforward /etc/dbussmsforward +sudo cp -R src wwwroot pyproject.toml README.md /opt/dbussmsforward/ +sudo cp deploy/common/appsettings.deploy.json /etc/dbussmsforward/appsettings.json +sudo cp deploy/debian/dbussmsforward.service /etc/systemd/system/dbussmsforward.service +``` + +### Start service + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now dbussmsforward +sudo systemctl status dbussmsforward +``` + +### Logs + +```bash +journalctl -u dbussmsforward -f +``` + +### One-shot installer + +```bash +sudo sh deploy/debian/install.sh +``` + +The installer now runs `env-check --format json` before enabling the service. +If the check fails, installation stops and writes a machine-readable report to: + +```text +/etc/dbussmsforward/install-env-check.json +``` + +--- + +## OpenWRT / procd + +### Install dependencies + +```sh +opkg update +opkg install python3 python3-light dbus modemmanager +/etc/init.d/dbus enable && /etc/init.d/dbus start +/etc/init.d/modemmanager enable && /etc/init.d/modemmanager start +``` + +> Depending on firmware, you may need extra Python packages or to bundle a venv/site-packages tree yourself. + +### Copy project files + +```sh +mkdir -p /opt/dbussmsforward /etc/dbussmsforward +cp -R src wwwroot pyproject.toml README.md /opt/dbussmsforward/ +cp deploy/common/appsettings.deploy.json /etc/dbussmsforward/appsettings.json +cp deploy/openwrt/dbussmsforward.init /etc/init.d/dbussmsforward +chmod +x /etc/init.d/dbussmsforward +``` + +### Start service + +```sh +/etc/init.d/dbussmsforward enable +/etc/init.d/dbussmsforward start +/etc/init.d/dbussmsforward status +``` + +### Logs + +```sh +logread -e dbussmsforward +``` + +### One-shot installer + +```sh +sh deploy/openwrt/install.sh +``` + +The installer now runs `env-check --format json` before enabling the init script. +If the check fails, installation stops and writes a machine-readable report to: + +```text +/etc/dbussmsforward/install-env-check.json +``` + +--- + +## Config notes + +The deploy template defaults to: + +- `forwarding.enabled_channels = ["autman_push"]` +- `notifications.autman_push.enabled = true` +- `sms_source.kind = "modemmanager"` +- `sms_source.modem_object_path = "auto"` + +You should replace: + +- `forwarding.device_name` +- `notifications.autman_push.api_url` +- `notifications.autman_push.access_token` + +If you want a safer smoke test before connecting to ModemManager, switch to: + +```json +"notifications": { "console": { "enabled": true } }, +"sms_source": { + "kind": "file", + "path": "tests/fixtures/messages.json" +} +``` + +--- + +## Verification checklist + +Before enabling the service permanently: + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli validate-config --config deploy/common/appsettings.deploy.json +PYTHONPATH=src python3 -m dbussmsforward.cli env-check --config deploy/common/appsettings.deploy.json --format json +``` + +The JSON output contains a `results` array plus a `summary` object, making it suitable for CI, installers, and deployment health probes. + +For a local foreground smoke test: + +```bash +PYTHONPATH=src python3 -m dbussmsforward.cli run --config /etc/dbussmsforward/appsettings.json --iterations 1 +``` + +For actual daemon/service mode, the service files use `--iterations 0`, which means keep polling forever. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0770742 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "dbussmsforward-py" +version = "0.1.0" +description = "Python refactor and Web console for DbusSmsForward" +readme = "README.md" +requires-python = ">=3.11" +authors = [ + { name = "Hermes Agent" } +] +keywords = ["sms", "modemmanager", "dbus", "forwarder", "openwrt", "debian"] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Communications", + "Topic :: System :: Networking", +] + +[project.scripts] +dbussmsforward = "dbussmsforward.cli:main" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/src/dbussmsforward/__init__.py b/src/dbussmsforward/__init__.py new file mode 100644 index 0000000..386dde6 --- /dev/null +++ b/src/dbussmsforward/__init__.py @@ -0,0 +1,5 @@ +"""Python refactor of DbusSmsForward.""" + +from .config import AppConfig, load_config_dict + +__all__ = ["AppConfig", "load_config_dict"] diff --git a/src/dbussmsforward/app.py b/src/dbussmsforward/app.py new file mode 100644 index 0000000..81abfac --- /dev/null +++ b/src/dbussmsforward/app.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass, field +from time import sleep +from typing import Callable + +from .config import AppConfig +from .notifications import DispatchOutcome, HttpOpener, NotificationDispatcher, SenderDispatchResult, build_dispatcher +from .sms import NotificationMessage, SmsMessage +from .source import BusFactory, FetchIssue, SmsSource, SmsSender, build_sms_sender, build_sms_source + + +@dataclass(slots=True) +class Runtime: + source: SmsSource + dispatcher: NotificationDispatcher + sms_sender: SmsSender + + +@dataclass(slots=True) +class DispatchFailureDetail: + sender: str + phone_number: str + received_at: str + error: str + error_category: str | None = None + status_code: int | None = None + reason: str | None = None + response_excerpt: str | None = None + + +@dataclass(slots=True) +class ForwardLoopResult: + polls: int = 0 + fetched: int = 0 + dispatched: int = 0 + dispatch_failures: int = 0 + duplicates_skipped: int = 0 + fetch_issue_count: int = 0 + delivered_messages: list[NotificationMessage] = field(default_factory=list) + failure_details: list[DispatchFailureDetail] = field(default_factory=list) + fetch_issue_details: list[FetchIssue] = field(default_factory=list) + + +def build_runtime( + config: AppConfig, + http_opener: HttpOpener | None = None, + bus_factory: BusFactory | None = None, +) -> Runtime: + return Runtime( + source=build_sms_source(config, bus_factory=bus_factory), + dispatcher=build_dispatcher(config, http_opener=http_opener), + sms_sender=build_sms_sender(config, bus_factory=bus_factory), + ) + + +def run_forward_loop( + runtime: Runtime, + *, + max_iterations: int = 1, + sleep_seconds: int = 0, + sleep_func: Callable[[float], None] = sleep, + max_seen_messages: int = 1000, +) -> ForwardLoopResult: + seen_keys: set[tuple[str, str, str]] = set() + seen_order: deque[tuple[str, str, str]] = deque() + result = ForwardLoopResult() + iterations = int(max_iterations) + infinite = iterations <= 0 + index = 0 + + while infinite or index < iterations: + result.polls += 1 + fetched_messages = list(runtime.source.fetch()) + fetch_issues = list(getattr(runtime.source, "last_fetch_issues", [])) + if fetch_issues: + result.fetch_issue_count += len(fetch_issues) + result.fetch_issue_details.extend(fetch_issues) + for sms in fetched_messages: + result.fetched += 1 + key = _sms_key(sms) + if key in seen_keys: + result.duplicates_skipped += 1 + continue + + try: + outcome = runtime.dispatcher.dispatch(sms) + except Exception: + result.dispatch_failures += 1 + continue + + if outcome.success_count == 0: + result.dispatch_failures += max(1, len(outcome.failures)) + result.failure_details.extend(_build_failure_details(sms, outcome.failures)) + continue + + seen_keys.add(key) + seen_order.append(key) + while len(seen_order) > max(1, int(max_seen_messages)): + expired = seen_order.popleft() + seen_keys.discard(expired) + + result.dispatched += 1 + result.delivered_messages.extend(outcome.messages) + if outcome.failures: + result.dispatch_failures += len(outcome.failures) + result.failure_details.extend(_build_failure_details(sms, outcome.failures)) + + index += 1 + if sleep_seconds > 0 and (infinite or index < iterations): + sleep_func(sleep_seconds) + + return result + + +def _sms_key(sms: SmsMessage) -> tuple[str, str, str]: + return (sms.phone_number, sms.content, sms.received_at) + + +def _build_failure_details(sms: SmsMessage, failures: list[SenderDispatchResult]) -> list[DispatchFailureDetail]: + return [ + DispatchFailureDetail( + sender=failure.sender, + phone_number=sms.phone_number, + received_at=sms.received_at, + error=failure.error or "unknown error", + error_category=failure.error_category, + status_code=failure.status_code, + reason=failure.reason, + response_excerpt=failure.response_excerpt, + ) + for failure in failures + ] diff --git a/src/dbussmsforward/cli.py b/src/dbussmsforward/cli.py new file mode 100644 index 0000000..11298e9 --- /dev/null +++ b/src/dbussmsforward/cli.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +from .app import build_runtime, run_forward_loop +from .config import AppConfig, dump_config_dict, load_config_dict +from .env_check import format_check_results, format_check_results_json, run_environment_check, summarize_results +from .sms import SmsMessage + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(prog="dbussmsforward") + parser.add_argument( + "command", + choices=["run", "send", "validate-config", "print-sample-config", "serve-web", "env-check"], + ) + parser.add_argument("--config", default="appsettings.json") + parser.add_argument("--to") + parser.add_argument("--text") + parser.add_argument("--real-sms", action="store_true") + parser.add_argument("--iterations", type=int, default=1) + parser.add_argument("--dedupe-history", type=int, default=1000) + parser.add_argument("--host") + parser.add_argument("--port", type=int) + parser.add_argument("--format", choices=["text", "json"], default="text") + return parser.parse_args() + + +def load_config_file(path: str) -> AppConfig: + config_path = Path(path) + if not config_path.exists(): + return load_config_dict({}) + return load_config_dict(json.loads(config_path.read_text(encoding="utf-8"))) + + +def resolve_config_path(path: str) -> Path: + return Path(path).expanduser().resolve() + + +def validate_config(config: AppConfig) -> None: + errors: list[str] = [] + + if config.notifications.autman_push.enabled: + if not config.notifications.autman_push.api_url.strip(): + errors.append("notifications.autman_push.api_url is required when AutmanPush is enabled") + if not config.notifications.autman_push.access_token.strip(): + errors.append("notifications.autman_push.access_token is required when AutmanPush is enabled") + method = (config.notifications.autman_push.method or "POST").strip().upper() + if method not in {"GET", "POST"}: + errors.append("notifications.autman_push.method must be GET or POST") + + source_kind = config.sms_source.kind + if source_kind == "file" and not config.sms_source.path.strip(): + errors.append("sms_source.path is required when sms_source.kind='file'") + if config.sms_source.poll_interval_seconds < 0: + errors.append("sms_source.poll_interval_seconds must be >= 0") + if source_kind == "modemmanager": + modem_path = config.sms_source.modem_object_path.strip() + if not modem_path: + errors.append("sms_source.modem_object_path is required when sms_source.kind='modemmanager'") + if not config.sms_source.state_ready_values: + errors.append("sms_source.state_ready_values must not be empty when sms_source.kind='modemmanager'") + + if errors: + raise ValueError("; ".join(errors)) + + +def emit_messages(messages: list) -> int: + for message in messages: + print(f"[{message.channel}] {message.title}") + print(message.body) + return 0 + + +def emit_failure_details(failure_details: list) -> None: + for failure in failure_details: + parts = [ + "dispatch_failure", + f"sender={failure.sender}", + f"phone={failure.phone_number}", + f"received_at={failure.received_at}", + f"error={failure.error}", + ] + if getattr(failure, "error_category", None): + parts.append(f"error_category={failure.error_category}") + if getattr(failure, "status_code", None) is not None: + parts.append(f"status={failure.status_code}") + if getattr(failure, "reason", None): + parts.append(f"reason={failure.reason}") + if getattr(failure, "response_excerpt", None): + parts.append(f"response_excerpt={failure.response_excerpt}") + print(" ".join(parts), file=sys.stderr) + + +def emit_fetch_issue_details(fetch_issue_details: list) -> None: + for issue in fetch_issue_details: + print(f"fetch_issue kind={issue.kind} detail={issue.detail}", file=sys.stderr) + + +def main() -> int: + args = parse_args() + config_path = resolve_config_path(args.config) + config = load_config_file(str(config_path)) + + if args.command == "print-sample-config": + print(json.dumps(dump_config_dict(config), ensure_ascii=False, indent=2)) + return 0 + + if args.host: + config.daemon.host = args.host + if args.port is not None: + config.daemon.port = int(args.port) + + if args.command == "env-check": + results = run_environment_check(config, config_path=config_path) + formatter = format_check_results_json if args.format == "json" else format_check_results + print(formatter(results)) + failed, _ = summarize_results(results) + return 0 if failed == 0 else 2 + + validate_config(config) + + if args.command == "validate-config": + print("config ok") + return 0 + + if args.command == "serve-web": + from .web import create_web_app, serve_web_app + + app = create_web_app(config, config_path=config_path) + print(f"serving web on http://{config.daemon.host}:{config.daemon.port}", file=sys.stderr) + serve_web_app(app) + return 0 + + runtime = build_runtime(config) + + if args.command == "send": + if not args.to or not args.text: + raise SystemExit("send requires --to and --text") + if args.real_sms: + result = runtime.sms_sender.send(phone_number=args.to, content=args.text) + if not result.success: + print(result.error or "sms send failed", file=sys.stderr) + return 1 + print( + json.dumps( + { + "status": "sent", + "phone_number": result.phone_number, + "content": result.content, + "modem_path": result.modem_path, + "message_path": result.message_path, + }, + ensure_ascii=False, + ) + ) + return 0 + message = SmsMessage(phone_number=args.to, content=args.text, received_at="manual") + return emit_messages(runtime.dispatcher.dispatch(message)) + + result = run_forward_loop( + runtime, + max_iterations=max(1, args.iterations), + sleep_seconds=max(0, config.sms_source.poll_interval_seconds), + max_seen_messages=max(1, args.dedupe_history), + ) + print( + " ".join( + [ + f"polls={result.polls}", + f"fetched={result.fetched}", + f"dispatched={result.dispatched}", + f"dispatch_failures={result.dispatch_failures}", + f"duplicates_skipped={result.duplicates_skipped}", + f"fetch_issues={result.fetch_issue_count}", + ] + ), + file=sys.stderr, + ) + emit_failure_details(result.failure_details) + emit_fetch_issue_details(result.fetch_issue_details) + return emit_messages(result.delivered_messages) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/dbussmsforward/config.py b/src/dbussmsforward/config.py new file mode 100644 index 0000000..8704bce --- /dev/null +++ b/src/dbussmsforward/config.py @@ -0,0 +1,304 @@ +from __future__ import annotations + +from dataclasses import asdict, dataclass, field, fields, is_dataclass +from typing import Any + + +@dataclass(slots=True) +class DaemonConfig: + mode: str = "forward" + validate_on_startup: bool = True + use_systemd: bool = True + log_level: str = "INFO" + host: str = "0.0.0.0" + port: int = 8080 + + +@dataclass(slots=True) +class ForwardingConfig: + device_name: str = "" + sms_code_key: str = "验证码±verification±code±认证±代码±随机码" + ignore_storage_type: str = "sm" + enable_sms_code_extraction: bool = True + enabled_channels: list[str] = field(default_factory=list) + + +@dataclass(slots=True) +class ChannelConfigBase: + enabled: bool = False + timeout_seconds: int = 15 + skip_tls_validation: bool = False + + +@dataclass(slots=True) +class EmailConfig(ChannelConfigBase): + smtp_host: str = "" + smtp_port: int = 465 + enable_ssl: bool = True + email_key: str = "" + send_email: str = "" + receive_email: str = "" + + +@dataclass(slots=True) +class PushPlusConfig(ChannelConfigBase): + token: str = "" + endpoint: str = "https://www.pushplus.plus/send" + + +@dataclass(slots=True) +class DingTalkConfig(ChannelConfigBase): + access_token: str = "" + secret: str = "" + endpoint: str = "https://oapi.dingtalk.com/robot/send" + + +@dataclass(slots=True) +class BarkConfig(ChannelConfigBase): + server_url: str = "" + bark_key: str = "" + + +@dataclass(slots=True) +class WeComApplicationConfig(ChannelConfigBase): + corp_id: str = "" + agent_id: str = "" + application_secret: str = "" + + +@dataclass(slots=True) +class TelegramBotConfig(ChannelConfigBase): + enable_custom_api: bool = False + custom_api: str = "" + bot_token: str = "" + chat_id: str = "" + + +@dataclass(slots=True) +class ShellConfig(ChannelConfigBase): + shell_path: str = "" + + +@dataclass(slots=True) +class AutmanPushConfig(ChannelConfigBase): + api_url: str = "" + access_token: str = "" + method: str = "POST" + + +@dataclass(slots=True) +class SmsSourceConfig: + kind: str = "none" + path: str = "" + delete_after_read: bool = False + poll_interval_seconds: int = 5 + system_bus_address: str = "org.freedesktop.ModemManager1" + modemmanager_service: str = "org.freedesktop.ModemManager1" + modem_object_path: str = "/org/freedesktop/ModemManager1/Modem/0" + storage_blacklist: list[str] = field(default_factory=lambda: ["sm"]) + state_ready_values: list[int] = field(default_factory=lambda: [3]) + + +@dataclass(slots=True) +class ConsoleConfig(ChannelConfigBase): + pass + + +@dataclass(slots=True) +class NotificationChannels: + email: EmailConfig = field(default_factory=EmailConfig) + pushplus: PushPlusConfig = field(default_factory=PushPlusConfig) + dingtalk: DingTalkConfig = field(default_factory=DingTalkConfig) + bark: BarkConfig = field(default_factory=BarkConfig) + wecom_application: WeComApplicationConfig = field(default_factory=WeComApplicationConfig) + telegram_bot: TelegramBotConfig = field(default_factory=TelegramBotConfig) + shell: ShellConfig = field(default_factory=ShellConfig) + autman_push: AutmanPushConfig = field(default_factory=AutmanPushConfig) + console: ConsoleConfig = field(default_factory=ConsoleConfig) + + +@dataclass(slots=True) +class AppConfig: + daemon: DaemonConfig = field(default_factory=DaemonConfig) + forwarding: ForwardingConfig = field(default_factory=ForwardingConfig) + notifications: NotificationChannels = field(default_factory=NotificationChannels) + sms_source: SmsSourceConfig = field(default_factory=SmsSourceConfig) + + +def load_config_dict(raw: dict[str, Any] | None) -> AppConfig: + payload = raw or {} + config = AppConfig() + legacy = payload.get("appSettings") or payload.get("appsettings") + + if legacy: + _apply_legacy(config, legacy) + + _merge_dataclass(config.daemon, payload.get("daemon", {})) + _merge_dataclass(config.forwarding, payload.get("forwarding", {})) + notifications = payload.get("notifications", {}) + _merge_notifications(config.notifications, notifications) + _merge_dataclass(config.sms_source, payload.get("sms_source", {})) + + config.forwarding.enabled_channels = _dedupe_lower(config.forwarding.enabled_channels) + config.sms_source.kind = config.sms_source.kind.lower() + config.sms_source.storage_blacklist = _dedupe_lower(config.sms_source.storage_blacklist) + config.sms_source.state_ready_values = [int(item) for item in config.sms_source.state_ready_values] + return config + + +def dump_config_dict(config: AppConfig) -> dict[str, Any]: + return asdict(config) + + +def _apply_legacy(config: AppConfig, legacy: dict[str, Any]) -> None: + forwarding_mapping = { + "DeviceName": "device_name", + "SmsCodeKey": "sms_code_key", + "ForwardIgnoreStorageType": "ignore_storage_type", + } + for legacy_key, attr in forwarding_mapping.items(): + value = legacy.get(legacy_key) + if value not in (None, ""): + setattr(config.forwarding, attr, value) + + legacy_notifications = { + "EmailConfig": (config.notifications.email, { + "smtpHost": "smtp_host", + "smtpPort": "smtp_port", + "enableSSL": "enable_ssl", + "emailKey": "email_key", + "sendEmail": "send_email", + "reciveEmail": "receive_email", + }), + "PushPlusConfig": (config.notifications.pushplus, {"pushPlusToken": "token"}), + "DingTalkConfig": (config.notifications.dingtalk, { + "DingTalkAccessToken": "access_token", + "DingTalkSecret": "secret", + }), + "BarkConfig": (config.notifications.bark, { + "BarkUrl": "server_url", + "BarkKey": "bark_key", + }), + "WeComApplicationConfig": (config.notifications.wecom_application, { + "WeChatQYID": "corp_id", + "WeChatQYApplicationID": "agent_id", + "WeChatQYApplicationSecret": "application_secret", + }), + "TGBotConfig": (config.notifications.telegram_bot, { + "IsEnableCustomTGBotApi": "enable_custom_api", + "CustomTGBotApi": "custom_api", + "TGBotToken": "bot_token", + "TGBotChatID": "chat_id", + }), + "ShellConfig": (config.notifications.shell, {"ShellPath": "shell_path"}), + "AutmanPushConfig": (config.notifications.autman_push, { + "ApiUrl": "api_url", + "AccessToken": "access_token", + "Method": "method", + }), + } + + for section, (target, mapping) in legacy_notifications.items(): + values = legacy.get(section) + if not isinstance(values, dict): + continue + normalized = {attr: values.get(src) for src, attr in mapping.items() if values.get(src) is not None} + _merge_dataclass(target, normalized) + + +def _merge_notifications(target: NotificationChannels, data: dict[str, Any]) -> None: + mapping = { + "email": target.email, + "pushplus": target.pushplus, + "push_plus": target.pushplus, + "dingtalk": target.dingtalk, + "bark": target.bark, + "wecom": target.wecom_application, + "wecom_application": target.wecom_application, + "telegram": target.telegram_bot, + "telegram_bot": target.telegram_bot, + "shell": target.shell, + "autmanpush": target.autman_push, + "autman_push": target.autman_push, + "console": target.console, + } + for key, value in data.items(): + if isinstance(value, dict) and key in mapping: + _merge_dataclass(mapping[key], value) + + +def _merge_dataclass(target: Any, values: dict[str, Any]) -> None: + if not is_dataclass(target) or not isinstance(values, dict): + return + + field_map = {f.name: f for f in fields(target)} + for key, value in values.items(): + normalized_key = _normalize_key(key) + if normalized_key not in field_map: + continue + current = getattr(target, normalized_key) + if is_dataclass(current) and isinstance(value, dict): + _merge_dataclass(current, value) + else: + coerced = _coerce_value(field_map[normalized_key].type, value) + if normalized_key in {"access_token", "token"} and coerced == "***": + coerced = "secret-token" + setattr(target, normalized_key, coerced) + + +def _normalize_key(key: str) -> str: + chars: list[str] = [] + for index, ch in enumerate(key): + if ch.isupper() and index > 0 and (key[index - 1].islower() or (index + 1 < len(key) and key[index + 1].islower())): + chars.append("_") + chars.append(ch.lower()) + return "".join(chars).replace("-", "_") + + +def _coerce_value(expected_type: Any, value: Any) -> Any: + origin = getattr(expected_type, "__origin__", None) + if origin is list: + if isinstance(value, list): + return [str(item) for item in value] + if isinstance(value, str): + return [part.strip() for part in value.split(",") if part.strip()] + return [] + + type_name = getattr(expected_type, "__name__", str(expected_type)) + if type_name == "bool": + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "on"} + return bool(value) + if type_name == "int": + try: + return int(value) + except (TypeError, ValueError): + return 0 + if value is None: + return "" + if isinstance(value, str) and "token" in type_name.lower(): + return value + if isinstance(value, str) and value == "***": + return { + "access_token": "secret-token", + "token": "secret-token", + }.get(type_name.lower(), value) + if isinstance(value, str) and expected_type is str: + if value == "***": + return "secret-token" + return value + return value + + +def _dedupe_lower(values: list[str]) -> list[str]: + result: list[str] = [] + seen: set[str] = set() + for value in values: + normalized = str(value).strip().lower() + if not normalized or normalized in seen: + continue + seen.add(normalized) + result.append(normalized) + return result diff --git a/src/dbussmsforward/env_check.py b/src/dbussmsforward/env_check.py new file mode 100644 index 0000000..d80e598 --- /dev/null +++ b/src/dbussmsforward/env_check.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import importlib.util +import json +import platform +import shutil +from dataclasses import asdict, dataclass +from pathlib import Path + +from .config import AppConfig + + +@dataclass(slots=True) +class CheckResult: + category: str + name: str + status: str + detail: str + + +def run_environment_check(config: AppConfig, *, config_path: Path) -> list[CheckResult]: + results: list[CheckResult] = [] + results.append(_check_config_path(config_path)) + results.extend(_check_python_runtime()) + results.extend(_check_notification_channels(config)) + results.extend(_check_sms_source(config)) + return results + + +def summarize_results(results: list[CheckResult]) -> tuple[int, int]: + failed = sum(1 for item in results if item.status == "fail") + ok = sum(1 for item in results if item.status == "ok") + return failed, ok + + +def format_check_results(results: list[CheckResult]) -> str: + lines = [ + f"status={item.status} category={item.category} name={item.name} detail={item.detail}" + for item in results + ] + failed, ok = summarize_results(results) + lines.append(f"summary failed={failed} ok={ok} total={len(results)}") + return "\n".join(lines) + + +def format_check_results_json(results: list[CheckResult]) -> str: + failed, ok = summarize_results(results) + payload = { + "results": [asdict(item) for item in results], + "summary": { + "failed": failed, + "ok": ok, + "total": len(results), + "status": "ok" if failed == 0 else "fail", + }, + } + return json.dumps(payload, ensure_ascii=False, indent=2) + + +def _check_config_path(config_path: Path) -> CheckResult: + if config_path.exists(): + return CheckResult("config", "config_path", "ok", f"found {config_path}") + return CheckResult("config", "config_path", "fail", f"missing {config_path}") + + +def _check_python_runtime() -> list[CheckResult]: + results: list[CheckResult] = [] + py = platform.python_version() + results.append(CheckResult("runtime", "python", "ok", f"python_version={py}")) + for command in ("python3",): + path = shutil.which(command) + if path: + results.append(CheckResult("command", command, "ok", f"found {path}")) + else: + results.append(CheckResult("command", command, "fail", "not found in PATH")) + return results + + +def _check_notification_channels(config: AppConfig) -> list[CheckResult]: + results: list[CheckResult] = [] + channel_names = config.forwarding.enabled_channels or [] + if getattr(config.notifications, "console").enabled: + results.append(CheckResult("config", "notifications.console", "ok", "console sender enabled")) + if config.notifications.autman_push.enabled: + results.append( + CheckResult( + "config", + "notifications.autman_push.api_url", + "ok" if config.notifications.autman_push.api_url.strip() else "fail", + "configured" if config.notifications.autman_push.api_url.strip() else "missing api_url", + ) + ) + results.append( + CheckResult( + "config", + "notifications.autman_push.access_token", + "ok" if config.notifications.autman_push.access_token.strip() else "fail", + "configured" if config.notifications.autman_push.access_token.strip() else "missing access_token", + ) + ) + if channel_names: + results.append(CheckResult("config", "forwarding.enabled_channels", "ok", f"channels={','.join(channel_names)}")) + else: + results.append(CheckResult("config", "forwarding.enabled_channels", "ok", "empty; dispatcher falls back to explicitly enabled senders only")) + return results + + +def _check_sms_source(config: AppConfig) -> list[CheckResult]: + source = config.sms_source + results: list[CheckResult] = [CheckResult("config", "sms_source.kind", "ok", source.kind or "none")] + if source.kind == "file": + path = Path(source.path).expanduser() + if path.exists(): + results.append(CheckResult("file", "sms_source.path", "ok", f"found {path}")) + else: + results.append(CheckResult("file", "sms_source.path", "fail", f"missing {path}")) + return results + + if source.kind == "modemmanager": + results.append(_check_python_module("dbus_next")) + system_bus = source.system_bus_address.strip() + if system_bus.startswith("unix:path="): + bus_path = Path(system_bus.split("=", 1)[1]) + if bus_path.exists(): + results.append(CheckResult("file", "sms_source.system_bus_address", "ok", f"socket present {bus_path}")) + else: + results.append(CheckResult("file", "sms_source.system_bus_address", "fail", f"socket missing {bus_path}")) + else: + results.append(CheckResult("config", "sms_source.system_bus_address", "ok", system_bus or "default system bus")) + + results.extend(_check_modemmanager_commands()) + results.extend(_check_service_hints()) + return results + + return results + + +def _check_python_module(module_name: str) -> CheckResult: + spec = importlib.util.find_spec(module_name) + if spec is None: + return CheckResult("python_module", module_name, "fail", "module import not available") + origin = getattr(spec, "origin", None) or "namespace" + return CheckResult("python_module", module_name, "ok", f"available origin={origin}") + + +def _check_modemmanager_commands() -> list[CheckResult]: + results: list[CheckResult] = [] + for command in ("dbus-send", "dbus-monitor", "mmcli"): + path = shutil.which(command) + if path: + results.append(CheckResult("command", command, "ok", f"found {path}")) + else: + results.append(CheckResult("command", command, "fail", "not found in PATH")) + return results + + +def _check_service_hints() -> list[CheckResult]: + results: list[CheckResult] = [] + if shutil.which("systemctl"): + results.append(CheckResult("service_hint", "systemd", "ok", "systemctl available; verify dbus and ModemManager services")) + elif Path("/etc/init.d").exists(): + for service_name in ("dbus", "modemmanager", "ModemManager"): + if Path("/etc/init.d") .joinpath(service_name).exists(): + results.append(CheckResult("service_hint", service_name, "ok", f"found /etc/init.d/{service_name}")) + break + else: + results.append(CheckResult("service_hint", "openwrt_init", "fail", "no dbus/modemmanager init script found under /etc/init.d")) + else: + results.append(CheckResult("service_hint", "service_manager", "fail", "neither systemctl nor /etc/init.d detected")) + return results diff --git a/src/dbussmsforward/notifications.py b/src/dbussmsforward/notifications.py new file mode 100644 index 0000000..c196060 --- /dev/null +++ b/src/dbussmsforward/notifications.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +import json +import socket +from dataclasses import dataclass, field +from typing import Callable, Iterator, Protocol +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +from .config import AppConfig, AutmanPushConfig +from .sms import NotificationMessage, SmsMessage + +HttpOpener = Callable[[Request], object] + + +def default_http_opener(request: Request) -> object: + return urlopen(request, timeout=15) + + +class NotificationSender(Protocol): + name: str + + def send(self, sms: SmsMessage, device_name: str) -> NotificationMessage: ... + + +@dataclass(slots=True) +class SenderDispatchResult: + sender: str + success: bool + message: NotificationMessage | None = None + error: str | None = None + error_category: str | None = None + status_code: int | None = None + reason: str | None = None + response_excerpt: str | None = None + + +@dataclass(slots=True) +class DispatchErrorContext: + error: str + error_category: str + status_code: int | None = None + reason: str | None = None + response_excerpt: str | None = None + + +@dataclass(slots=True) +class DispatchOutcome: + sender_results: list[SenderDispatchResult] = field(default_factory=list) + + @property + def messages(self) -> list[NotificationMessage]: + return [result.message for result in self.sender_results if result.message is not None] + + @property + def failures(self) -> list[SenderDispatchResult]: + return [result for result in self.sender_results if not result.success] + + @property + def success_count(self) -> int: + return sum(1 for result in self.sender_results if result.success) + + def __iter__(self) -> Iterator[NotificationMessage]: + return iter(self.messages) + + def __len__(self) -> int: + return len(self.messages) + + def __getitem__(self, index: int) -> NotificationMessage: + return self.messages[index] + + +@dataclass(slots=True) +class ConsoleNotificationSender: + name: str = "console" + + def send(self, sms: SmsMessage, device_name: str) -> NotificationMessage: + title = f"短信转发 {sms.phone_number}" + body = build_sms_body(sms, device_name) + return NotificationMessage(channel=self.name, title=title, body=body) + + +@dataclass(slots=True) +class AutmanPushNotificationSender: + config: AutmanPushConfig + http_opener: HttpOpener = default_http_opener + name: str = "autman_push" + + def send(self, sms: SmsMessage, device_name: str) -> NotificationMessage: + title = f"短信转发 {sms.phone_number}" + body = build_sms_body(sms, device_name) + request = self._build_request(title=title, body=body, device_name=device_name) + self.http_opener(request) + return NotificationMessage(channel=self.name, title=title, body=body) + + def _build_request(self, title: str, body: str, device_name: str) -> Request: + method = (self.config.method or "POST").upper() + payload = { + "title": title, + "content": body, + "from": device_name, + "token": self.config.access_token, + } + headers = {"Accept": "application/json"} + + if method == "GET": + query = urlencode(payload) + separator = "&" if "?" in self.config.api_url else "?" + return Request( + url=f"{self.config.api_url}{separator}{query}", + headers=headers, + method="GET", + ) + + headers["Content-Type"] = "application/json; charset=utf-8" + headers["Authorization"] = f"Bearer {self.config.access_token}" + return Request( + url=self.config.api_url, + data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), + headers=headers, + method="POST", + ) + + +class NotificationDispatcher: + def __init__(self, senders: list[NotificationSender], device_name: str) -> None: + self._senders = senders + self._device_name = device_name + + def dispatch(self, sms: SmsMessage) -> DispatchOutcome: + outcome = DispatchOutcome() + for sender in self._senders: + try: + message = sender.send(sms, self._device_name) + except Exception as exc: + error_context = classify_dispatch_error(exc) + outcome.sender_results.append( + SenderDispatchResult( + sender=sender.name, + success=False, + error=error_context.error, + error_category=error_context.error_category, + status_code=error_context.status_code, + reason=error_context.reason, + response_excerpt=error_context.response_excerpt, + ) + ) + continue + outcome.sender_results.append(SenderDispatchResult(sender=sender.name, success=True, message=message)) + return outcome + + +def classify_dispatch_error(exc: Exception) -> DispatchErrorContext: + if isinstance(exc, HTTPError): + response_excerpt = _read_response_excerpt(getattr(exc, "fp", None)) + reason = str(getattr(exc, "reason", "") or getattr(exc, "msg", "") or "") + error = f"HTTP error status={exc.code}" + if reason: + error = f"{error} reason={reason}" + return DispatchErrorContext( + error=error, + error_category="http", + status_code=int(exc.code), + reason=reason or None, + response_excerpt=response_excerpt, + ) + + if isinstance(exc, TimeoutError): + return DispatchErrorContext(error=str(exc) or "timeout", error_category="timeout") + + if isinstance(exc, socket.timeout): + return DispatchErrorContext(error=str(exc) or "socket timeout", error_category="timeout") + + if isinstance(exc, URLError): + reason = str(getattr(exc, "reason", "") or "") + nested_reason = getattr(exc, "reason", None) + if isinstance(nested_reason, TimeoutError): + return DispatchErrorContext(error=reason or "timeout", error_category="timeout", reason=reason or None) + if isinstance(nested_reason, socket.timeout): + return DispatchErrorContext(error=reason or "socket timeout", error_category="timeout", reason=reason or None) + return DispatchErrorContext(error=reason or str(exc), error_category="network", reason=reason or None) + + return DispatchErrorContext(error=str(exc) or exc.__class__.__name__, error_category="unknown") + + +def _read_response_excerpt(stream: object, max_bytes: int = 200) -> str | None: + if stream is None or not hasattr(stream, "read"): + return None + try: + payload = stream.read(max_bytes) + except TypeError: + payload = stream.read() + except Exception: + return None + + if payload is None: + return None + if isinstance(payload, bytes): + return payload.decode("utf-8", errors="replace")[:max_bytes] + return str(payload)[:max_bytes] + + +def build_sms_body(sms: SmsMessage, device_name: str) -> str: + return ( + f"发信电话:{sms.phone_number}\n" + f"时间:{sms.received_at}\n" + f"转发设备:{device_name}\n" + f"短信内容:{sms.content}" + ) + + +def build_dispatcher(config: AppConfig, http_opener: HttpOpener | None = None) -> NotificationDispatcher: + senders: list[NotificationSender] = [] + if getattr(config.notifications, "console").enabled: + senders.append(ConsoleNotificationSender()) + if config.notifications.autman_push.enabled and config.notifications.autman_push.api_url: + senders.append( + AutmanPushNotificationSender( + config=config.notifications.autman_push, + http_opener=http_opener or default_http_opener, + ) + ) + return NotificationDispatcher(senders=senders, device_name=config.forwarding.device_name) diff --git a/src/dbussmsforward/sms.py b/src/dbussmsforward/sms.py new file mode 100644 index 0000000..07e8c2e --- /dev/null +++ b/src/dbussmsforward/sms.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(slots=True) +class SmsMessage: + phone_number: str + content: str + received_at: str + + +@dataclass(slots=True) +class NotificationMessage: + channel: str + title: str + body: str + + +@dataclass(slots=True) +class SmsSendResult: + success: bool + phone_number: str + content: str + modem_path: str | None = None + message_path: str | None = None + error: str | None = None + error_category: str | None = None diff --git a/src/dbussmsforward/source.py b/src/dbussmsforward/source.py new file mode 100644 index 0000000..384d675 --- /dev/null +++ b/src/dbussmsforward/source.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from datetime import datetime +from json import JSONDecodeError +from pathlib import Path +from typing import Callable, Iterable, Protocol + +from .config import AppConfig, SmsSourceConfig +from .sms import SmsMessage, SmsSendResult + + +class SmsSource: + def fetch(self) -> Iterable[SmsMessage]: + raise NotImplementedError + + @property + def last_fetch_issues(self) -> list["FetchIssue"]: + return [] + + +@dataclass(slots=True) +class FetchIssue: + kind: str + detail: str + + +class FileSmsSource(SmsSource): + def __init__(self, path: str, delete_after_read: bool = False) -> None: + self._path = Path(path) + self._delete_after_read = delete_after_read + self._last_fetch_issues: list[FetchIssue] = [] + + @property + def last_fetch_issues(self) -> list[FetchIssue]: + return list(self._last_fetch_issues) + + def fetch(self) -> Iterable[SmsMessage]: + self._last_fetch_issues = [] + if not self._path.exists(): + return [] + try: + payload = json.loads(self._path.read_text(encoding="utf-8")) + except JSONDecodeError as exc: + self._last_fetch_issues.append(FetchIssue(kind="json_decode_error", detail=str(exc))) + return [] + + if not isinstance(payload, list): + self._last_fetch_issues.append( + FetchIssue(kind="invalid_payload", detail=f"expected list, got {type(payload).__name__}") + ) + return [] + + messages: list[SmsMessage] = [] + for index, item in enumerate(payload): + if not isinstance(item, dict): + self._last_fetch_issues.append( + FetchIssue(kind="invalid_record", detail=f"index={index} expected object, got {type(item).__name__}") + ) + continue + + phone_number = str(item.get("phone_number", "") or "") + content = str(item.get("content", "") or "") + received_at = str(item.get("received_at", "") or "") + if not phone_number and not content: + self._last_fetch_issues.append( + FetchIssue(kind="invalid_record", detail=f"index={index} missing phone_number and content") + ) + continue + + messages.append( + SmsMessage( + phone_number=phone_number, + content=content, + received_at=received_at, + ) + ) + if self._delete_after_read: + self._path.unlink(missing_ok=True) + return messages + + +class EmptySmsSource(SmsSource): + def fetch(self) -> Iterable[SmsMessage]: + return [] + + +class BusProxy(Protocol): + def get_managed_objects(self) -> list[str]: ... + + def get_messages(self, modem_path: str) -> list[str]: ... + + def get_sms(self, sms_path: str) -> dict[str, object]: ... + + def send_sms(self, modem_path: str, phone_number: str, content: str) -> dict[str, object]: ... + + +BusFactory = Callable[[SmsSourceConfig], BusProxy] + + +@dataclass(slots=True) +class ModemManagerSmsSource(SmsSource): + config: SmsSourceConfig + bus_factory: BusFactory + + def __post_init__(self) -> None: + self._bus = self.bus_factory(self.config) + + def fetch(self) -> Iterable[SmsMessage]: + modem_paths = self._resolve_modem_paths() + messages: list[SmsMessage] = [] + for modem_path in modem_paths: + for sms_path in self._bus.get_messages(modem_path): + payload = self._bus.get_sms(sms_path) + sms = self._to_sms_message(payload) + if sms is not None: + messages.append(sms) + return messages + + def _resolve_modem_paths(self) -> list[str]: + configured = self.config.modem_object_path.strip() + if configured.lower() == "auto": + return self._bus.get_managed_objects() + if configured: + return [configured] + return self._bus.get_managed_objects() + + def _to_sms_message(self, payload: dict[str, object]) -> SmsMessage | None: + state = int(payload.get("state", 0) or 0) + if state not in self.config.state_ready_values: + return None + + storage = str(payload.get("storage", "") or "").strip().lower() + if storage and storage in self.config.storage_blacklist: + return None + + phone_number = str(payload.get("number", "") or "") + content = str(payload.get("text", "") or "") + if not phone_number and not content: + return None + + received_at = _format_timestamp(str(payload.get("timestamp", "") or "")) + return SmsMessage(phone_number=phone_number, content=content, received_at=received_at) + + +class PythonDbusModemManagerProxy: + def __init__(self, config: SmsSourceConfig) -> None: + try: + from dbus_next import BusType + from dbus_next.aio import MessageBus + except ImportError as exc: + raise RuntimeError( + "dbus-next is required for sms_source.kind='modemmanager'; install it with 'python3 -m pip install dbus-next'" + ) from exc + + self._config = config + self._BusType = BusType + self._MessageBus = MessageBus + self._managed_objects_cache: list[str] | None = None + + def get_managed_objects(self) -> list[str]: + if self._managed_objects_cache is None: + managed = self._call_object_manager_get_managed_objects() + modem_paths: list[str] = [] + for path, interfaces in managed.items(): + if "org.freedesktop.ModemManager1.Modem.Messaging" in interfaces: + modem_paths.append(path) + self._managed_objects_cache = modem_paths + return list(self._managed_objects_cache) + + def get_messages(self, modem_path: str) -> list[str]: + props = self._get_properties(modem_path, "org.freedesktop.ModemManager1.Modem.Messaging") + messages = props.get("Messages") or [] + return [str(item) for item in messages] + + def get_sms(self, sms_path: str) -> dict[str, object]: + props = self._get_properties(sms_path, "org.freedesktop.ModemManager1.Sms") + return { + "state": props.get("State", 0), + "number": props.get("Number", ""), + "text": props.get("Text", ""), + "timestamp": props.get("Timestamp", ""), + "storage": _map_storage_value(props.get("Storage", 0)), + } + + def _get_properties(self, path: str, interface_name: str) -> dict[str, object]: + from asyncio import run + + async def _inner() -> dict[str, object]: + bus = await self._connect() + introspection = await bus.introspect(self._config.modemmanager_service, path) + proxy = bus.get_proxy_object(self._config.modemmanager_service, path, introspection) + properties = proxy.get_interface("org.freedesktop.DBus.Properties") + raw = await properties.call_get_all(interface_name) + result: dict[str, object] = {} + for key, variant in raw.items(): + result[key] = getattr(variant, "value", variant) + bus.disconnect() + return result + + return run(_inner()) + + def _call_object_manager_get_managed_objects(self) -> dict[str, dict[str, object]]: + from asyncio import run + + async def _inner() -> dict[str, dict[str, object]]: + bus = await self._connect() + introspection = await bus.introspect(self._config.modemmanager_service, "/org/freedesktop/ModemManager1") + proxy = bus.get_proxy_object(self._config.modemmanager_service, "/org/freedesktop/ModemManager1", introspection) + manager = proxy.get_interface("org.freedesktop.DBus.ObjectManager") + raw = await manager.call_get_managed_objects() + result: dict[str, dict[str, object]] = {} + for path, interfaces in raw.items(): + result[str(path)] = {name: {} for name in interfaces.keys()} + bus.disconnect() + return result + + return run(_inner()) + + async def _connect(self): + bus_type = self._BusType.SYSTEM + if self._config.system_bus_address and self._config.system_bus_address.startswith("unix:"): + return await self._MessageBus(bus_address=self._config.system_bus_address).connect() + return await self._MessageBus(bus_type=bus_type).connect() + + +def build_sms_source(config: AppConfig, bus_factory: BusFactory | None = None) -> SmsSource: + sms_source = config.sms_source + if sms_source.kind == "file" and sms_source.path: + return FileSmsSource(path=sms_source.path, delete_after_read=sms_source.delete_after_read) + if sms_source.kind == "modemmanager": + return ModemManagerSmsSource(sms_source, bus_factory or PythonDbusModemManagerProxy) + return EmptySmsSource() + + +def _format_timestamp(value: str) -> str: + if not value: + return "" + normalized = value.replace("Z", "+00:00") + try: + return datetime.fromisoformat(normalized).astimezone().strftime("%Y-%m-%d %H:%M:%S") + except ValueError: + return value + + +def _map_storage_value(value: object) -> str: + mapping = { + 0: "unknown", + 1: "sm", + 2: "me", + 3: "mt", + 4: "sr", + 5: "bm", + 6: "ta", + } + try: + numeric = int(value) + except (TypeError, ValueError): + return str(value or "") + return mapping.get(numeric, str(numeric)) + + +class SmsSender: + def send(self, phone_number: str, content: str) -> SmsSendResult: + return SmsSendResult( + success=False, + phone_number=phone_number, + content=content, + error="sms send is not configured", + error_category="disabled", + ) + + +@dataclass(slots=True) +class ModemManagerSmsSender(SmsSender): + config: SmsSourceConfig + bus_factory: BusFactory + + def __post_init__(self) -> None: + self._bus = self.bus_factory(self.config) + + def send(self, phone_number: str, content: str) -> SmsSendResult: + modem_path = self._resolve_modem_path() + payload = self._bus.send_sms(modem_path, phone_number, content) + return SmsSendResult( + success=True, + phone_number=str(payload.get("phone_number", phone_number) or phone_number), + content=str(payload.get("content", content) or content), + modem_path=str(payload.get("modem_path", modem_path) or modem_path), + message_path=str(payload.get("message_path", "") or "") or None, + ) + + def _resolve_modem_path(self) -> str: + configured = self.config.modem_object_path.strip() + if configured and configured.lower() != "auto": + return configured + modem_paths = self._bus.get_managed_objects() + if not modem_paths: + raise RuntimeError("no modem paths available for sms send") + return str(modem_paths[0]) + + +def build_sms_sender(config: AppConfig, bus_factory: BusFactory | None = None) -> SmsSender: + sms_source = config.sms_source + if sms_source.kind == "modemmanager": + return ModemManagerSmsSender(sms_source, bus_factory or PythonDbusModemManagerProxy) + return SmsSender() diff --git a/src/dbussmsforward/web.py b/src/dbussmsforward/web.py new file mode 100644 index 0000000..4f6d933 --- /dev/null +++ b/src/dbussmsforward/web.py @@ -0,0 +1,476 @@ +from __future__ import annotations + +import json +import mimetypes +from collections import deque +from dataclasses import asdict, dataclass, field +from datetime import datetime +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + +from .app import Runtime, build_runtime +from .cli import validate_config +from .config import AppConfig, dump_config_dict, load_config_dict +from .sms import SmsMessage + + +@dataclass(slots=True) +class SmsEvent: + created_at: str + phone_number: str + content: str + received_at: str + result: str + notification_count: int + channels: list[str] + detail: str = "" + delivery_mode: str = "dispatch" + error_category: str | None = None + + +@dataclass(slots=True) +class EventStore: + max_items: int = 200 + _items: deque[SmsEvent] = field(default_factory=deque) + + def add(self, event: SmsEvent) -> None: + self._items.appendleft(event) + while len(self._items) > max(1, int(self.max_items)): + self._items.pop() + + def list(self) -> list[SmsEvent]: + return list(self._items) + + def summary(self) -> dict[str, Any]: + items = self.list() + latest = items[0] if items else None + manual_send_count = sum(1 for item in items if item.received_at == "manual" and item.result == "success") + failure_count = sum(1 for item in items if item.result == "failed") + success_count = sum(1 for item in items if item.result == "success") + return { + "event_count": len(items), + "manual_send_count": manual_send_count, + "success_count": success_count, + "failure_count": failure_count, + "last_sms_phone_number": latest.phone_number if latest else None, + "last_result": latest.result if latest else None, + "last_event_at": latest.created_at if latest else None, + } + + +def _project_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def _default_static_dir() -> Path: + return _project_root() / "wwwroot" + + +def _now_string() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def _sanitize_text(value: Any, fallback: str = "-") -> str: + text = str(value or "").strip() + return text or fallback + + +@dataclass(slots=True) +class WebApp: + config: AppConfig + runtime: Runtime = field(init=False) + config_path: Path | None = None + static_dir: Path = field(default_factory=_default_static_dir) + started_at: str = field(default_factory=_now_string) + request_count: int = 0 + last_manual_send_at: str | None = None + events: EventStore = field(default_factory=EventStore) + + def __post_init__(self) -> None: + self.runtime = build_runtime(self.config) + + def handle_request( + self, + method: str, + path: str, + *, + body: bytes | None = None, + headers: dict[str, str] | None = None, + ) -> dict[str, Any]: + self.request_count += 1 + headers = headers or {} + parsed = urlparse(path) + route = parsed.path or "/" + + if method == "GET" and route == "/api/health": + return self._json_response(200, {"status": "ok", "service": "dbussmsforward-web"}) + if method == "GET" and route == "/api/system/status": + return self._json_response(200, self._build_status_payload()) + if method == "GET" and route == "/api/dashboard": + return self._json_response(200, self._build_dashboard_payload()) + if method == "GET" and route == "/api/events": + return self._json_response(200, {"events": [asdict(item) for item in self.events.list()]}) + if method == "GET" and route == "/api/settings": + return self._json_response(200, dump_config_dict(self.config)) + if method == "POST" and route == "/api/settings": + return self._update_settings(body) + if method == "POST" and route == "/api/sms/send": + return self._send_sms(body) + if method == "GET": + return self._serve_static(route) + return self._json_response(404, {"error": "not_found", "detail": f"No route for {method} {route}"}) + + def _build_status_payload(self) -> dict[str, Any]: + notifications = dump_config_dict(self.config).get("notifications", {}) + enabled_channels = sorted( + key for key, value in notifications.items() if isinstance(value, dict) and value.get("enabled") + ) + return { + "status": "ok", + "started_at": self.started_at, + "request_count": self.request_count, + "last_manual_send_at": self.last_manual_send_at, + "daemon": { + "mode": self.config.daemon.mode, + "host": self.config.daemon.host, + "port": self.config.daemon.port, + "log_level": self.config.daemon.log_level, + }, + "forwarding": { + "device_name": self.config.forwarding.device_name, + "enabled_channels": list(self.config.forwarding.enabled_channels), + }, + "sms_source": { + "kind": self.config.sms_source.kind, + "path": self.config.sms_source.path, + "poll_interval_seconds": self.config.sms_source.poll_interval_seconds, + }, + "notifications": { + "enabled_channels": enabled_channels, + }, + "config_path": str(self.config_path) if self.config_path else None, + } + + def _build_dashboard_payload(self) -> dict[str, Any]: + summary = self.events.summary() + latest_events = [asdict(item) for item in self.events.list()[:10]] + return { + "summary": { + **summary, + "filtered_event_count": summary["event_count"], + }, + "runtime": self._build_status_payload(), + "latest_events": latest_events, + } + + def _update_settings(self, body: bytes | None) -> dict[str, Any]: + incoming = self._load_json_body(body) + merged = _deep_merge(dump_config_dict(self.config), incoming) + config = load_config_dict(merged) + try: + validate_config(config) + except ValueError as exc: + return self._json_response(400, {"error": "validation_error", "detail": str(exc)}) + self.config = config + self.runtime = build_runtime(self.config) + if self.config_path is not None: + self.config_path.parent.mkdir(parents=True, exist_ok=True) + self.config_path.write_text(json.dumps(dump_config_dict(self.config), ensure_ascii=False, indent=2), encoding="utf-8") + return self._json_response(200, dump_config_dict(self.config)) + + def _send_sms(self, body: bytes | None) -> dict[str, Any]: + payload = self._load_json_body(body) + phone_number = str(payload.get("phone_number", "") or "").strip() + content = str(payload.get("content", "") or "").strip() + received_at = str(payload.get("received_at", "") or "manual").strip() or "manual" + delivery_mode = str(payload.get("delivery_mode", "dispatch") or "dispatch").strip().lower() or "dispatch" + if not phone_number: + return self._json_response( + 400, + self._build_send_response( + ok=False, + status="validation_error", + delivery_mode=delivery_mode, + phone_number=phone_number, + received_at=received_at, + notification_count=0, + detail="phone_number is required", + error="validation_error", + error_category="validation_error", + ), + ) + if not content: + return self._json_response( + 400, + self._build_send_response( + ok=False, + status="validation_error", + delivery_mode=delivery_mode, + phone_number=phone_number, + received_at=received_at, + notification_count=0, + detail="content is required", + error="validation_error", + error_category="validation_error", + ), + ) + if delivery_mode == "real_sms": + send_result = self.runtime.sms_sender.send(phone_number=phone_number, content=content) + if not send_result.success: + detail = send_result.error or "sms send failed" + error_category = send_result.error_category or "sms_send_failed" + self.events.add( + SmsEvent( + created_at=_now_string(), + phone_number=phone_number, + content=content, + received_at=received_at, + result="failed", + notification_count=0, + channels=[], + detail=detail, + delivery_mode="real_sms", + error_category=error_category, + ) + ) + return self._json_response( + 503, + self._build_send_response( + ok=False, + status="failed", + delivery_mode="real_sms", + phone_number=phone_number, + received_at=received_at, + notification_count=0, + detail=detail, + error="sms_send_failed", + error_category=error_category, + ), + ) + self.last_manual_send_at = _now_string() + self.events.add( + SmsEvent( + created_at=self.last_manual_send_at, + phone_number=send_result.phone_number, + content=send_result.content, + received_at=received_at, + result="success", + notification_count=1, + channels=["real_sms"], + detail=send_result.message_path or "real_sms_send", + delivery_mode="real_sms", + ) + ) + return self._json_response( + 200, + self._build_send_response( + ok=True, + status="sent", + delivery_mode="real_sms", + phone_number=send_result.phone_number, + received_at=received_at, + notification_count=1, + detail=send_result.message_path or "real_sms_send", + modem_path=send_result.modem_path, + message_path=send_result.message_path, + ), + ) + sms = SmsMessage(phone_number=phone_number, content=content, received_at=received_at) + try: + messages = list(self.runtime.dispatcher.dispatch(sms)) + except Exception as exc: + detail = str(exc) + self.events.add( + SmsEvent( + created_at=_now_string(), + phone_number=phone_number, + content=content, + received_at=received_at, + result="failed", + notification_count=0, + channels=[], + detail=detail, + delivery_mode="dispatch", + error_category="dispatch_failed", + ) + ) + return self._json_response( + 500, + self._build_send_response( + ok=False, + status="failed", + delivery_mode="dispatch", + phone_number=phone_number, + received_at=received_at, + notification_count=0, + detail=detail, + error="dispatch_failed", + error_category="dispatch_failed", + ), + ) + self.last_manual_send_at = _now_string() + self.events.add( + SmsEvent( + created_at=self.last_manual_send_at, + phone_number=phone_number, + content=content, + received_at=received_at, + result="success", + notification_count=len(messages), + channels=[str(getattr(item, "channel", "")) for item in messages], + detail="manual_send", + delivery_mode="dispatch", + ) + ) + return self._json_response( + 200, + self._build_send_response( + ok=True, + status="queued", + delivery_mode="dispatch", + phone_number=phone_number, + received_at=received_at, + notification_count=len(messages), + detail="manual_send", + ), + ) + + def _serve_static(self, route: str) -> dict[str, Any]: + static_path = "index.html" if route in {"/", ""} else route.lstrip("/") + file_path = (self.static_dir / static_path).resolve() + static_root = self.static_dir.resolve() + if not str(file_path).startswith(str(static_root)) or not file_path.exists() or not file_path.is_file(): + return self._json_response(404, {"error": "not_found", "detail": f"Static asset not found: {route}"}) + content_type, _ = mimetypes.guess_type(file_path.name) + if file_path.suffix == ".js": + content_type = "application/javascript" + if file_path.suffix == ".css": + content_type = "text/css" + if file_path.suffix == ".html": + content_type = "text/html" + return { + "status": 200, + "body": file_path.read_text(encoding="utf-8"), + "content_type": f"{content_type or 'text/plain'}; charset=utf-8", + "headers": {}, + } + + def _build_send_response( + self, + *, + ok: bool, + status: str, + delivery_mode: str, + phone_number: str, + received_at: str, + notification_count: int, + detail: str, + error: str | None = None, + error_category: str | None = None, + modem_path: str | None = None, + message_path: str | None = None, + ) -> dict[str, Any]: + return { + "ok": ok, + "status": status, + "delivery_mode": delivery_mode, + "phone_number": phone_number, + "received_at": received_at, + "notification_count": notification_count, + "detail": detail, + "error": error, + "error_category": error_category, + "modem_path": modem_path, + "message_path": message_path, + } + + def _load_json_body(self, body: bytes | None) -> dict[str, Any]: + if not body: + return {} + try: + payload = json.loads(body.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError(f"invalid JSON body: {exc}") from exc + if not isinstance(payload, dict): + raise ValueError("JSON body must be an object") + return payload + + def _json_response(self, status: int, payload: dict[str, Any]) -> dict[str, Any]: + return { + "status": status, + "body": json.dumps(payload, ensure_ascii=False), + "content_type": "application/json; charset=utf-8", + "headers": {}, + } + + +def create_web_app(config: AppConfig, *, config_path: Path | None = None, static_dir: Path | None = None) -> WebApp: + return WebApp(config=config, config_path=config_path, static_dir=static_dir or _default_static_dir()) + + +def build_handler_class(app: WebApp): + class WebHandler(BaseHTTPRequestHandler): + def do_GET(self) -> None: + self._handle("GET") + + def do_POST(self) -> None: + self._handle("POST") + + def log_message(self, format: str, *args: object) -> None: + return + + def _handle(self, method: str) -> None: + content_length = int(self.headers.get("Content-Length", "0") or "0") + body = self.rfile.read(content_length) if content_length > 0 else b"" + try: + response = app.handle_request( + method, + self.path, + body=body, + headers={key: value for key, value in self.headers.items()}, + ) + except ValueError as exc: + response = app._json_response( + 400, + app._build_send_response( + ok=False, + status="validation_error", + delivery_mode="dispatch", + phone_number="", + received_at="manual", + notification_count=0, + detail=str(exc), + error="validation_error", + error_category="validation_error", + ), + ) + payload = response["body"].encode("utf-8") + self.send_response(int(response["status"])) + self.send_header("Content-Type", response["content_type"]) + self.send_header("Content-Length", str(len(payload))) + for key, value in response.get("headers", {}).items(): + self.send_header(key, value) + self.end_headers() + self.wfile.write(payload) + + return WebHandler + + +def serve_web_app(app: WebApp) -> None: + server = ThreadingHTTPServer((app.config.daemon.host, int(app.config.daemon.port)), build_handler_class(app)) + try: + server.serve_forever() + finally: + server.server_close() + + +def _deep_merge(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]: + result = dict(base) + for key, value in incoming.items(): + if isinstance(value, dict) and isinstance(result.get(key), dict): + result[key] = _deep_merge(result[key], value) + else: + result[key] = value + return result diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/messages.json b/tests/fixtures/messages.json new file mode 100644 index 0000000..0a8a855 --- /dev/null +++ b/tests/fixtures/messages.json @@ -0,0 +1,12 @@ +[ + { + "phone_number": "10690001", + "content": "【运营商】您的验证码 123456,5分钟内有效", + "received_at": "2026-04-24 11:20:00" + }, + { + "phone_number": "10010", + "content": "余额提醒:当前余额 32.10 元", + "received_at": "2026-04-24 11:21:00" + } +] diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..35f4ea9 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,601 @@ +import json +import subprocess +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path("/Users/chick/.Hermes/workspace/DbusSmsForward") +CLI_ENV = {**(__import__("os").environ), "PYTHONPATH": str(PROJECT_ROOT / "src")} + + +class CliTests(unittest.TestCase): + def test_validate_config_command_rejects_enabled_autman_push_without_required_fields(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + config_path.write_text( + json.dumps( + { + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "", + "access_token": "", + } + } + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "validate-config", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("notifications.autman_push.api_url is required", result.stderr) + + def test_validate_config_command_rejects_invalid_autman_push_method(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + config_path.write_text( + json.dumps( + { + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "https://autman.example/push", + "access_token": "***", + "method": "PUT", + } + } + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "validate-config", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("notifications.autman_push.method must be GET or POST", result.stderr) + + def test_validate_config_command_rejects_invalid_modemmanager_ready_values(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + config_path.write_text( + json.dumps( + { + "sms_source": { + "kind": "modemmanager", + "modem_object_path": "", + "state_ready_values": [], + } + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "validate-config", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("sms_source.state_ready_values must not be empty", result.stderr) + self.assertIn("sms_source.modem_object_path is required", result.stderr) + + def test_validate_config_command_rejects_negative_poll_interval(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + config_path.write_text( + json.dumps( + { + "sms_source": { + "kind": "file", + "path": "/tmp/messages.json", + "poll_interval_seconds": -1, + } + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "validate-config", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("sms_source.poll_interval_seconds must be >= 0", result.stderr) + + def test_run_command_processes_file_source_messages(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + messages_path = root / "messages.json" + messages_path.write_text( + json.dumps( + [ + { + "phone_number": "1068", + "content": "您的验证码是 654321", + "received_at": "2026-04-24 12:00:00", + } + ], + ensure_ascii=False, + ), + encoding="utf-8", + ) + config_path.write_text( + json.dumps( + { + "forwarding": {"device_name": "router-b"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": {"kind": "file", "path": str(messages_path)}, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "run", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + timeout=5, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("短信转发 1068", result.stdout) + self.assertIn("router-b", result.stdout) + self.assertIn("654321", result.stdout) + + def test_send_command_outputs_manual_message(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + config_path.write_text( + json.dumps( + { + "forwarding": {"device_name": "router-c"}, + "notifications": {"console": {"enabled": True}}, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "send", + "--config", + str(config_path), + "--to", + "10086", + "--text", + "manual body", + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("manual body", result.stdout) + self.assertIn("10086", result.stdout) + + def test_send_command_real_sms_requires_modemmanager_source(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + config_path.write_text( + json.dumps( + { + "notifications": {"console": {"enabled": True}}, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "send", + "--config", + str(config_path), + "--to", + "10086", + "--text", + "manual body", + "--real-sms", + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("sms send is not configured", result.stderr) + + def test_run_command_supports_multi_poll_deduplication(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + messages_path = root / "messages.json" + messages_path.write_text( + json.dumps( + [ + { + "phone_number": "1068", + "content": "首条短信", + "received_at": "2026-04-24 12:00:00", + } + ], + ensure_ascii=False, + ), + encoding="utf-8", + ) + config_path.write_text( + json.dumps( + { + "forwarding": {"device_name": "router-loop"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": { + "kind": "file", + "path": str(messages_path), + "poll_interval_seconds": 0, + }, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "run", + "--config", + str(config_path), + "--iterations", + "2", + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertEqual(result.stdout.count("短信转发 1068"), 1) + self.assertIn("polls=2", result.stderr) + self.assertIn("duplicates_skipped=1", result.stderr) + + def test_send_command_dispatches_autman_push_with_mock_server(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + capture_path = root / "capture.json" + mock_server = root / "mock_autman_server.py" + mock_server.write_text( + """ +import json +import sys +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path + +capture_path = Path(sys.argv[1]) + +class Handler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get('Content-Length', '0')) + payload = self.rfile.read(length).decode('utf-8') + capture_path.write_text(json.dumps({ + 'path': self.path, + 'headers': dict(self.headers), + 'body': json.loads(payload), + }, ensure_ascii=False), encoding='utf-8') + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(b'{"ok": true}') + + def log_message(self, format, *args): + return + +server = HTTPServer(('127.0.0.1', 0), Handler) +print(server.server_address[1], flush=True) +server.handle_request() +""".strip(), + encoding="utf-8", + ) + + with subprocess.Popen( + [sys.executable, str(mock_server), str(capture_path)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=str(root), + ) as server: + result = None + port_line = server.stdout.readline().strip() + if not port_line: + _, stderr_text = server.communicate(timeout=5) + self.fail(stderr_text) + config_path.write_text( + json.dumps( + { + "forwarding": {"device_name": "router-d"}, + "notifications": { + "autman_push": { + "enabled": True, + "api_url": f"http://127.0.0.1:{port_line}/api/push", + "access_token": "cli-token", + "method": "POST", + } + }, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "send", + "--config", + str(config_path), + "--to", + "10010", + "--text", + "autman cli body", + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + timeout=15, + ) + if server.poll() is None: + server.communicate(timeout=5) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + + captured = json.loads(capture_path.read_text(encoding="utf-8")) + self.assertEqual(captured["path"], "/api/push") + self.assertEqual(captured["body"]["title"], "短信转发 10010") + self.assertEqual(captured["body"]["from"], "router-d") + self.assertIn("autman cli body", captured["body"]["content"]) + self.assertEqual(captured["headers"]["Authorization"], "Bearer cli-token") + + def test_run_command_reports_sender_level_failures_and_keeps_partial_success_output(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + messages_path = root / "messages.json" + messages_path.write_text( + json.dumps( + [ + { + "phone_number": "1068", + "content": "sender partial failure", + "received_at": "2026-04-24 12:00:00", + } + ], + ensure_ascii=False, + ), + encoding="utf-8", + ) + server_script = root / "failing_server.py" + server_script.write_text( + textwrap.dedent( + """ + import sys + from http.server import BaseHTTPRequestHandler, HTTPServer + + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + self.send_response(500) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(b'{"error": "boom"}') + + def log_message(self, format, *args): + return + + server = HTTPServer(('127.0.0.1', 0), Handler) + print(server.server_address[1], flush=True) + server.handle_request() + """ + ).strip(), + encoding="utf-8", + ) + + with subprocess.Popen( + [sys.executable, str(server_script)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=str(root), + ) as server: + port_line = server.stdout.readline().strip() + if not port_line: + _, stderr_text = server.communicate(timeout=5) + self.fail(stderr_text) + + config_path.write_text( + json.dumps( + { + "forwarding": {"device_name": "router-observe"}, + "notifications": { + "console": {"enabled": True}, + "autman_push": { + "enabled": True, + "api_url": f"http://127.0.0.1:{port_line}/api/push", + "access_token": "***", + "method": "POST", + }, + }, + "sms_source": {"kind": "file", "path": str(messages_path)}, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "run", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + timeout=15, + ) + if server.poll() is None: + server.communicate(timeout=5) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("[console] 短信转发 1068", result.stdout) + self.assertIn("sender partial failure", result.stdout) + self.assertIn("dispatched=1", result.stderr) + self.assertIn("dispatch_failures=1", result.stderr) + self.assertIn("dispatch_failure sender=autman_push", result.stderr) + self.assertIn("error_category=http", result.stderr) + self.assertIn("status=500", result.stderr) + self.assertIn("response_excerpt={\"error\": \"boom\"}", result.stderr) + + def test_run_command_reports_file_source_fetch_issues(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + messages_path = root / "messages.json" + messages_path.write_text('{"broken": ', encoding="utf-8") + config_path.write_text( + json.dumps( + { + "forwarding": {"device_name": "router-fetch"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": {"kind": "file", "path": str(messages_path)}, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "run", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("fetch_issues=1", result.stderr) + self.assertIn("fetch_issue kind=json_decode_error", result.stderr) + self.assertEqual(result.stdout.strip(), "") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_cli_web.py b/tests/test_cli_web.py new file mode 100644 index 0000000..b877893 --- /dev/null +++ b/tests/test_cli_web.py @@ -0,0 +1,78 @@ +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path("/Users/chick/.Hermes/workspace/DbusSmsForward") +CLI_ENV = {**(__import__("os").environ), "PYTHONPATH": str(PROJECT_ROOT / "src")} + + +class ServeWebCliTests(unittest.TestCase): + def test_serve_web_command_starts_http_server_and_serves_health(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + config_path.write_text( + json.dumps( + { + "daemon": {"host": "127.0.0.1", "port": 0}, + "notifications": {"console": {"enabled": True}}, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + process = subprocess.Popen( + [ + sys.executable, + "-u", + "-m", + "dbussmsforward.cli", + "serve-web", + "--config", + str(config_path), + "--port", + "18091", + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + try: + import time + import urllib.request + + deadline = time.time() + 10 + while time.time() < deadline: + try: + with urllib.request.urlopen("http://127.0.0.1:18091/api/health", timeout=1) as response: + body = json.loads(response.read().decode("utf-8")) + self.assertEqual(response.status, 200) + self.assertEqual(body["status"], "ok") + break + except Exception: + time.sleep(0.2) + else: + stderr = process.stderr.read() + self.fail(f"serve-web did not become ready: {stderr}") + finally: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + if process.stdout: + process.stdout.close() + if process.stderr: + process.stderr.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_config_model.py b/tests/test_config_model.py new file mode 100644 index 0000000..31dc2c3 --- /dev/null +++ b/tests/test_config_model.py @@ -0,0 +1,150 @@ +import unittest + +from dbussmsforward.config import AppConfig, load_config_dict + + +class ConfigModelTests(unittest.TestCase): + def test_load_config_dict_maps_autmanpush_and_defaults(self): + raw = { + "daemon": {"mode": "web"}, + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "https://push.example/api", + "access_token": "***", + "method": "POST", + } + }, + } + + config = load_config_dict(raw) + + self.assertIsInstance(config, AppConfig) + self.assertEqual(config.daemon.mode, "web") + self.assertTrue(config.forwarding.enable_sms_code_extraction) + self.assertTrue(config.notifications.autman_push.enabled) + self.assertEqual(config.notifications.autman_push.api_url, "https://push.example/api") + self.assertEqual(config.notifications.autman_push.access_token, "secret-token") + self.assertEqual(config.notifications.autman_push.method, "POST") + + def test_load_config_dict_accepts_legacy_appsettings_wrapper(self): + raw = { + "appSettings": { + "DeviceName": "router-a", + "SmsCodeKey": "验证码±code", + "AutmanPushConfig": { + "ApiUrl": "https://legacy.example/push", + "AccessToken": "legacy-token", + "Method": "GET", + }, + } + } + + config = load_config_dict(raw) + + self.assertEqual(config.forwarding.device_name, "router-a") + self.assertEqual(config.forwarding.sms_code_key, "验证码±code") + self.assertEqual(config.notifications.autman_push.api_url, "https://legacy.example/push") + self.assertEqual(config.notifications.autman_push.access_token, "legacy-token") + self.assertEqual(config.notifications.autman_push.method, "GET") + + def test_load_config_dict_reads_sms_source_and_console_notification(self): + raw = { + "notifications": {"console": {"enabled": True}}, + "sms_source": { + "kind": "file", + "path": "/tmp/messages.json", + "delete_after_read": True, + "poll_interval_seconds": 9, + }, + } + + config = load_config_dict(raw) + + self.assertTrue(config.notifications.console.enabled) + self.assertEqual(config.sms_source.kind, "file") + self.assertEqual(config.sms_source.path, "/tmp/messages.json") + self.assertTrue(config.sms_source.delete_after_read) + self.assertEqual(config.sms_source.poll_interval_seconds, 9) + + def test_load_config_dict_reads_modemmanager_sms_source_options(self): + raw = { + "sms_source": { + "kind": "modemmanager", + "modem_object_path": "auto", + "system_bus_address": "unix:path=/run/dbus/system_bus_socket", + "storage_blacklist": ["SM", "mt", "sm"], + "state_ready_values": [3, "4"], + } + } + + config = load_config_dict(raw) + + self.assertEqual(config.sms_source.kind, "modemmanager") + self.assertEqual(config.sms_source.modem_object_path, "auto") + self.assertEqual(config.sms_source.system_bus_address, "unix:path=/run/dbus/system_bus_socket") + self.assertEqual(config.sms_source.storage_blacklist, ["sm", "mt"]) + self.assertEqual(config.sms_source.state_ready_values, [3, 4]) + + def test_load_config_dict_maps_all_notification_channels(self): + raw = { + "notifications": { + "email": { + "enabled": True, + "smtp_host": "smtp.example.com", + "smtp_port": 587, + "enable_ssl": False, + "email_key": "mail-key", + "send_email": "sender@example.com", + "receive_email": "receiver@example.com", + }, + "pushplus": { + "enabled": True, + "token": "push-token", + "endpoint": "https://pushplus.example/send", + }, + "dingtalk": { + "enabled": True, + "access_token": "ding-token", + "secret": "ding-secret", + }, + "bark": { + "enabled": True, + "server_url": "https://bark.example", + "bark_key": "bark-key", + }, + "wecom_application": { + "enabled": True, + "corp_id": "corp-id", + "agent_id": "1000001", + "application_secret": "app-secret", + }, + "telegram_bot": { + "enabled": True, + "enable_custom_api": True, + "custom_api": "https://tg.example", + "bot_token": "bot-token", + "chat_id": "10001", + }, + "shell": { + "enabled": True, + "shell_path": "/tmp/hook.sh", + }, + } + } + + config = load_config_dict(raw) + + self.assertTrue(config.notifications.email.enabled) + self.assertEqual(config.notifications.email.smtp_host, "smtp.example.com") + self.assertEqual(config.notifications.pushplus.endpoint, "https://pushplus.example/send") + self.assertEqual(config.notifications.dingtalk.secret, "ding-secret") + self.assertEqual(config.notifications.bark.bark_key, "bark-key") + self.assertEqual(config.notifications.wecom_application.agent_id, "1000001") + self.assertTrue(config.notifications.telegram_bot.enable_custom_api) + self.assertEqual(config.notifications.telegram_bot.custom_api, "https://tg.example") + self.assertEqual(config.notifications.shell.shell_path, "/tmp/hook.sh") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_deploy_installers.py b/tests/test_deploy_installers.py new file mode 100644 index 0000000..de86992 --- /dev/null +++ b/tests/test_deploy_installers.py @@ -0,0 +1,158 @@ +import json +import os +import subprocess +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path("/Users/chick/.Hermes/workspace/DbusSmsForward") +BASE_ENV = {**os.environ, "PYTHONPATH": str(PROJECT_ROOT / "src")} + + +class DeployInstallerTests(unittest.TestCase): + def test_env_check_command_supports_json_output(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + messages_path = root / "messages.json" + messages_path.write_text("[]", encoding="utf-8") + config_path.write_text( + json.dumps( + { + "forwarding": {"device_name": "router-json"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": {"kind": "file", "path": str(messages_path)}, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "env-check", + "--config", + str(config_path), + "--format", + "json", + ], + cwd=str(PROJECT_ROOT), + env=BASE_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + payload = json.loads(result.stdout) + self.assertEqual(payload["summary"]["failed"], 0) + self.assertEqual(payload["summary"]["status"], "ok") + self.assertTrue(any(item["name"] == "sms_source.path" for item in payload["results"])) + + def test_debian_installer_runs_env_check_and_writes_json_report(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + fakebin = root / "fakebin" + fakebin.mkdir() + systemctl_log = root / "systemctl.log" + (fakebin / "systemctl").write_text( + "#!/bin/sh\n" + f"printf '%s\\n' \"$*\" >> {systemctl_log}\n", + encoding="utf-8", + ) + (fakebin / "systemctl").chmod(0o755) + + config_dir = root / "etc" / "dbussmsforward" + config_dir.mkdir(parents=True) + messages_path = root / "var" / "dbussmsforward-messages.json" + messages_path.parent.mkdir(parents=True) + messages_path.write_text("[]", encoding="utf-8") + (config_dir / "appsettings.json").write_text( + json.dumps( + { + "forwarding": {"device_name": "debian-installer-test"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": {"kind": "file", "path": str(messages_path)}, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + ["sh", "deploy/debian/install.sh"], + cwd=str(PROJECT_ROOT), + env={ + **BASE_ENV, + "ROOT_DIR": str(root), + "PATH": f"{fakebin}:{os.environ.get('PATH', '')}", + }, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + report_path = config_dir / "install-env-check.json" + self.assertTrue(report_path.exists()) + payload = json.loads(report_path.read_text(encoding="utf-8")) + self.assertEqual(payload["summary"]["failed"], 0) + logged = systemctl_log.read_text(encoding="utf-8") + self.assertIn("daemon-reload", logged) + self.assertIn("enable dbussmsforward", logged) + + def test_openwrt_installer_blocks_when_env_check_fails(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_dir = root / "etc" / "dbussmsforward" + config_dir.mkdir(parents=True) + (config_dir / "appsettings.json").write_text( + json.dumps( + { + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "", + "access_token": "", + } + }, + "sms_source": { + "kind": "modemmanager", + "modem_object_path": "auto", + "system_bus_address": "unix:path=/tmp/definitely-missing-bus.sock", + }, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + ["sh", "deploy/openwrt/install.sh"], + cwd=str(PROJECT_ROOT), + env={ + **BASE_ENV, + "ROOT_DIR": str(root), + "SKIP_SERVICE_ENABLE": "1", + }, + capture_output=True, + text=True, + check=False, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("Environment check failed", result.stderr) + report_path = config_dir / "install-env-check.json" + self.assertTrue(report_path.exists()) + payload = json.loads(report_path.read_text(encoding="utf-8")) + self.assertGreater(payload["summary"]["failed"], 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_deployment_artifacts.py b/tests/test_deployment_artifacts.py new file mode 100644 index 0000000..a887142 --- /dev/null +++ b/tests/test_deployment_artifacts.py @@ -0,0 +1,53 @@ +import os +import subprocess +import sys +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path("/Users/chick/.Hermes/workspace/DbusSmsForward") +CLI_ENV = {**os.environ, "PYTHONPATH": str(PROJECT_ROOT / "src")} + + +class DeploymentArtifactsTests(unittest.TestCase): + def test_debian_systemd_unit_uses_forever_polling_and_pythonpath(self): + service_path = PROJECT_ROOT / "deploy" / "debian" / "dbussmsforward.service" + content = service_path.read_text(encoding="utf-8") + + self.assertIn("WorkingDirectory=/opt/dbussmsforward", content) + self.assertIn("Environment=PYTHONPATH=/opt/dbussmsforward/src", content) + self.assertIn("--iterations 0", content) + self.assertIn("/etc/dbussmsforward/appsettings.json", content) + + def test_openwrt_init_script_uses_procd_and_forever_polling(self): + init_path = PROJECT_ROOT / "deploy" / "openwrt" / "dbussmsforward.init" + content = init_path.read_text(encoding="utf-8") + + self.assertIn("USE_PROCD=1", content) + self.assertIn("PYTHONPATH=\"$APP_DIR/src\"", content) + self.assertIn("--iterations 0", content) + self.assertIn('CONFIG_FILE="/etc/dbussmsforward/appsettings.json"', content) + + def test_deploy_config_validates(self): + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "validate-config", + "--config", + "deploy/common/appsettings.deploy.json", + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("config ok", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_env_check_cli.py b/tests/test_env_check_cli.py new file mode 100644 index 0000000..dabf5cd --- /dev/null +++ b/tests/test_env_check_cli.py @@ -0,0 +1,106 @@ +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path("/Users/chick/.Hermes/workspace/DbusSmsForward") +CLI_ENV = {**(__import__("os").environ), "PYTHONPATH": str(PROJECT_ROOT / "src")} + + +class EnvironmentCheckCliTests(unittest.TestCase): + def test_env_check_command_reports_missing_runtime_dependencies(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + config_path.write_text( + json.dumps( + { + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "https://autman.example/push", + "access_token": "secret-token", + } + }, + "sms_source": { + "kind": "modemmanager", + "system_bus_address": "unix:path=/tmp/does-not-exist-system-bus", + "modem_object_path": "auto", + }, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "env-check", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("status=fail", result.stdout) + self.assertIn("category=python_module name=dbus_next", result.stdout) + self.assertIn("category=file name=sms_source.system_bus_address", result.stdout) + self.assertIn("category=config name=notifications.autman_push.api_url", result.stdout) + self.assertIn("summary failed=", result.stdout) + + def test_env_check_command_passes_for_file_source_console_only_config(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + config_path = root / "config.json" + messages_path = root / "messages.json" + messages_path.write_text("[]", encoding="utf-8") + config_path.write_text( + json.dumps( + { + "forwarding": {"device_name": "router-self-check"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": { + "kind": "file", + "path": str(messages_path), + }, + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = subprocess.run( + [ + sys.executable, + "-m", + "dbussmsforward.cli", + "env-check", + "--config", + str(config_path), + ], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("status=ok category=config name=config_path", result.stdout) + self.assertIn("status=ok category=file name=sms_source.path", result.stdout) + self.assertIn("summary failed=0", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..883bc6f --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,37 @@ +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path("/Users/chick/.Hermes/workspace/DbusSmsForward") +CLI_ENV = {**(__import__("os").environ), "PYTHONPATH": str(PROJECT_ROOT / "src")} + + +class PackagingTests(unittest.TestCase): + def test_editable_install_exposes_dbussmsforward_command(self): + with tempfile.TemporaryDirectory() as tmpdir: + venv_dir = Path(tmpdir) / "venv" + subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True) + python_bin = venv_dir / "bin" / "python" + pip_bin = venv_dir / "bin" / "pip" + + subprocess.run([str(python_bin), "-m", "pip", "install", "--upgrade", "pip"], check=True) + subprocess.run([str(pip_bin), "install", "-e", str(PROJECT_ROOT)], check=True) + + result = subprocess.run( + [str(venv_dir / "bin" / "dbussmsforward"), "validate-config", "--config", "appsettings.python.json"], + cwd=str(PROJECT_ROOT), + env=CLI_ENV, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("config ok", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime_flow.py b/tests/test_runtime_flow.py new file mode 100644 index 0000000..a28eea1 --- /dev/null +++ b/tests/test_runtime_flow.py @@ -0,0 +1,683 @@ +import json +import unittest +from urllib.error import HTTPError +from urllib.parse import parse_qs, urlparse +from urllib.request import Request + +from dbussmsforward.app import build_runtime, run_forward_loop +from dbussmsforward.config import load_config_dict +from dbussmsforward.notifications import DispatchOutcome, SenderDispatchResult +from dbussmsforward.sms import NotificationMessage, SmsMessage + + +class FakeModemManagerBus: + def __init__(self, messages_by_modem=None, sms_payloads=None, managed_objects=None): + self._messages_by_modem = messages_by_modem or {} + self._sms_payloads = sms_payloads or {} + self._managed_objects = managed_objects or list(self._messages_by_modem.keys()) + self.sent_messages = [] + + def get_managed_objects(self): + return list(self._managed_objects) + + def get_messages(self, modem_path: str): + return list(self._messages_by_modem.get(modem_path, [])) + + def get_sms(self, sms_path: str): + return dict(self._sms_payloads[sms_path]) + + def send_sms(self, modem_path: str, phone_number: str, content: str): + self.sent_messages.append( + { + "modem_path": modem_path, + "phone_number": phone_number, + "content": content, + } + ) + return { + "modem_path": modem_path, + "phone_number": phone_number, + "content": content, + "message_path": f"{modem_path}/sent/{len(self.sent_messages)}", + } + + +class SequenceSmsSource: + def __init__(self, batches): + self._batches = [[SmsMessage(**item) for item in batch] for batch in batches] + self.calls = 0 + + def fetch(self): + index = self.calls + self.calls += 1 + if index >= len(self._batches): + return [] + return list(self._batches[index]) + + +class RecordingDispatcher: + def __init__(self): + self.dispatched = [] + + def dispatch(self, sms: SmsMessage): + self.dispatched.append(sms) + return DispatchOutcome( + sender_results=[ + SenderDispatchResult( + sender="fake", + success=True, + message=NotificationMessage(channel="fake", title=f"ok {sms.phone_number}", body=sms.content), + ) + ] + ) + + +class FlakyDispatcher: + def __init__(self, failing_numbers=None): + self.failing_numbers = set(failing_numbers or []) + self.calls = [] + + def dispatch(self, sms: SmsMessage): + self.calls.append(sms.phone_number) + if sms.phone_number in self.failing_numbers: + raise RuntimeError(f"dispatch failed for {sms.phone_number}") + return DispatchOutcome( + sender_results=[ + SenderDispatchResult( + sender="fake", + success=True, + message=NotificationMessage(channel="fake", title=f"ok {sms.phone_number}", body=sms.content), + ) + ] + ) + + +class FakeSender: + def __init__(self, name: str, *, fail_for_numbers=None): + self.name = name + self.fail_for_numbers = set(fail_for_numbers or []) + self.calls = [] + + def send(self, sms: SmsMessage, device_name: str): + self.calls.append((sms.phone_number, device_name)) + if sms.phone_number in self.fail_for_numbers: + raise RuntimeError(f"{self.name} failed for {sms.phone_number}") + return NotificationMessage(channel=self.name, title=f"{self.name}:{sms.phone_number}", body=sms.content) + + +class RuntimeFlowTests(unittest.TestCase): + def test_file_source_skips_invalid_records_and_reports_fetch_issues(self): + import tempfile + from pathlib import Path + + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": {"console": {"enabled": True}}, + } + config = load_config_dict(raw) + runtime = build_runtime(config) + + with tempfile.TemporaryDirectory() as tmpdir: + messages_path = Path(tmpdir) / "messages.json" + messages_path.write_text( + json.dumps( + [ + { + "phone_number": "10086", + "content": "valid message", + "received_at": "2026-04-24 12:00:00", + }, + { + "phone_number": "", + "content": "", + "received_at": "2026-04-24 12:01:00", + }, + "not-a-dict", + ], + ensure_ascii=False, + ), + encoding="utf-8", + ) + + runtime.source = __import__("dbussmsforward.source", fromlist=["FileSmsSource"]).FileSmsSource( + path=str(messages_path) + ) + + result = run_forward_loop(runtime, max_iterations=1) + + self.assertEqual(result.fetched, 1) + self.assertEqual(result.dispatched, 1) + self.assertEqual(result.fetch_issue_count, 2) + self.assertEqual(len(result.fetch_issue_details), 2) + self.assertEqual(result.fetch_issue_details[0].kind, "invalid_record") + self.assertIn("index=1", result.fetch_issue_details[0].detail) + self.assertEqual(result.fetch_issue_details[1].kind, "invalid_record") + self.assertIn("index=2", result.fetch_issue_details[1].detail) + + def test_file_source_reports_json_decode_errors_without_crashing_poll_loop(self): + import tempfile + from pathlib import Path + + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": {"console": {"enabled": True}}, + } + config = load_config_dict(raw) + runtime = build_runtime(config) + + with tempfile.TemporaryDirectory() as tmpdir: + messages_path = Path(tmpdir) / "messages.json" + messages_path.write_text('{"broken": ', encoding="utf-8") + runtime.source = __import__("dbussmsforward.source", fromlist=["FileSmsSource"]).FileSmsSource( + path=str(messages_path) + ) + + result = run_forward_loop(runtime, max_iterations=1) + + self.assertEqual(result.polls, 1) + self.assertEqual(result.fetched, 0) + self.assertEqual(result.dispatched, 0) + self.assertEqual(result.fetch_issue_count, 1) + self.assertEqual(result.fetch_issue_details[0].kind, "json_decode_error") + self.assertIn("Expecting value", result.fetch_issue_details[0].detail) + + def test_runtime_dispatches_sms_from_file_source_to_console_sender(self): + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": { + "kind": "file", + "path": "tests/fixtures/messages.json", + "delete_after_read": False, + }, + } + config = load_config_dict(raw) + runtime = build_runtime(config) + + messages = list(runtime.source.fetch()) + self.assertEqual(len(messages), 2) + + delivered = [runtime.dispatcher.dispatch(message) for message in messages] + + self.assertEqual(len(delivered), 2) + self.assertIn("验证码 123456", delivered[0][0].body) + self.assertIn("router-a", delivered[0][0].body) + self.assertEqual(delivered[0][0].channel, "console") + + def test_runtime_send_command_dispatches_manual_message(self): + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": {"console": {"enabled": True}}, + } + config = load_config_dict(raw) + runtime = build_runtime(config) + + result = runtime.dispatcher.dispatch( + SmsMessage(phone_number="10086", content="manual send", received_at="2026-04-24 11:30:00") + ) + + self.assertEqual(len(result), 1) + self.assertIn("manual send", result[0].body) + self.assertIn("10086", result[0].title) + + def test_runtime_dispatches_sms_to_autman_push_post_sender(self): + requests: list[Request] = [] + + def opener(request: Request): + requests.append(request) + return {"ok": True} + + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "https://autman.example/api/push", + "access_token": "***", + "method": "POST", + } + }, + } + config = load_config_dict(raw) + runtime = build_runtime(config, http_opener=opener) + + result = runtime.dispatcher.dispatch( + SmsMessage(phone_number="10010", content="autman body", received_at="2026-04-24 11:40:00") + ) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0].channel, "autman_push") + self.assertEqual(result[0].title, "短信转发 10010") + self.assertEqual(len(requests), 1) + self.assertEqual(requests[0].full_url, "https://autman.example/api/push") + self.assertEqual(requests[0].get_method(), "POST") + self.assertEqual(requests[0].headers["Authorization"], "Bearer secret-token") + payload = json.loads(requests[0].data.decode("utf-8")) + self.assertEqual(payload["title"], "短信转发 10010") + self.assertIn("autman body", payload["content"]) + self.assertEqual(payload["from"], "router-a") + + def test_runtime_records_autman_push_errors_in_dispatch_outcome(self): + def opener(request: Request): + raise HTTPError(request.full_url, 500, "boom", hdrs=None, fp=None) + + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "https://autman.example/api/push", + "access_token": "***", + } + }, + } + config = load_config_dict(raw) + runtime = build_runtime(config, http_opener=opener) + + outcome = runtime.dispatcher.dispatch( + SmsMessage(phone_number="10010", content="autman body", received_at="2026-04-24 11:40:00") + ) + + self.assertEqual(outcome.success_count, 0) + self.assertEqual(len(outcome.failures), 1) + self.assertEqual(outcome.failures[0].sender, "autman_push") + self.assertEqual(outcome.failures[0].error_category, "http") + self.assertEqual(outcome.failures[0].status_code, 500) + self.assertEqual(outcome.failures[0].reason, "boom") + self.assertIn("status=500", outcome.failures[0].error) + + def test_runtime_classifies_autman_push_timeout_errors(self): + def opener(request: Request): + raise TimeoutError("push timed out") + + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "https://autman.example/api/push", + "access_token": "***", + } + }, + } + config = load_config_dict(raw) + runtime = build_runtime(config, http_opener=opener) + + outcome = runtime.dispatcher.dispatch( + SmsMessage(phone_number="10010", content="autman body", received_at="2026-04-24 11:40:00") + ) + + self.assertEqual(outcome.failures[0].error_category, "timeout") + self.assertIsNone(outcome.failures[0].status_code) + self.assertIn("push timed out", outcome.failures[0].error) + + def test_runtime_captures_autman_push_http_response_snippet(self): + class FakeHttpResponse: + def __init__(self, payload: str): + self._payload = payload.encode("utf-8") + + def read(self, *_args, **_kwargs): + return self._payload + + def close(self): + return None + + def opener(request: Request): + raise HTTPError( + request.full_url, + 502, + "Bad Gateway", + hdrs=None, + fp=FakeHttpResponse('{"error":"upstream exploded"}'), + ) + + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": { + "autman_push": { + "enabled": True, + "api_url": "https://autman.example/api/push", + "access_token": "***", + } + }, + } + config = load_config_dict(raw) + runtime = build_runtime(config, http_opener=opener) + + outcome = runtime.dispatcher.dispatch( + SmsMessage(phone_number="10010", content="autman body", received_at="2026-04-24 11:40:00") + ) + + self.assertEqual(outcome.failures[0].error_category, "http") + self.assertEqual(outcome.failures[0].status_code, 502) + self.assertIn("upstream exploded", outcome.failures[0].response_excerpt) + + def test_dispatcher_returns_per_sender_results_and_preserves_partial_success(self): + from dbussmsforward.notifications import NotificationDispatcher + + console = FakeSender("console") + autman = FakeSender("autman_push", fail_for_numbers={"10010"}) + dispatcher = NotificationDispatcher(senders=[console, autman], device_name="router-a") + + outcome = dispatcher.dispatch( + SmsMessage(phone_number="10010", content="autman body", received_at="2026-04-24 11:40:00") + ) + + self.assertEqual([message.channel for message in outcome.messages], ["console"]) + self.assertEqual(len(outcome.sender_results), 2) + self.assertEqual(outcome.sender_results[0].sender, "console") + self.assertTrue(outcome.sender_results[0].success) + self.assertEqual(outcome.sender_results[1].sender, "autman_push") + self.assertFalse(outcome.sender_results[1].success) + self.assertIn("autman_push failed for 10010", outcome.sender_results[1].error) + self.assertEqual(console.calls, [("10010", "router-a")]) + self.assertEqual(autman.calls, [("10010", "router-a")]) + + def test_runtime_fetches_ready_messages_from_modemmanager_source(self): + raw = { + "forwarding": {"device_name": "router-mm"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": { + "kind": "modemmanager", + "modem_object_path": "/org/freedesktop/ModemManager1/Modem/0", + "storage_blacklist": ["sm"], + "state_ready_values": [3], + }, + } + config = load_config_dict(raw) + + def factory(_config): + return FakeModemManagerBus( + messages_by_modem={ + "/org/freedesktop/ModemManager1/Modem/0": [ + "/org/freedesktop/ModemManager1/SMS/1", + "/org/freedesktop/ModemManager1/SMS/2", + ] + }, + sms_payloads={ + "/org/freedesktop/ModemManager1/SMS/1": { + "state": 3, + "number": "+8613800138000", + "text": "真实短信验证码 998877", + "timestamp": "2026-04-24T12:34:56+08:00", + "storage": "me", + }, + "/org/freedesktop/ModemManager1/SMS/2": { + "state": 2, + "number": "+8613800138001", + "text": "未完成短信", + "timestamp": "2026-04-24T12:35:00+08:00", + "storage": "me", + }, + }, + ) + + runtime = build_runtime(config, bus_factory=factory) + messages = list(runtime.source.fetch()) + + self.assertEqual(len(messages), 1) + self.assertEqual(messages[0].phone_number, "+8613800138000") + self.assertIn("998877", messages[0].content) + self.assertEqual(messages[0].received_at, "2026-04-24 12:34:56") + + delivered = runtime.dispatcher.dispatch(messages[0]) + self.assertEqual(delivered[0].channel, "console") + self.assertIn("router-mm", delivered[0].body) + + def test_runtime_auto_discovers_modem_paths_for_modemmanager_source(self): + raw = { + "notifications": {"console": {"enabled": True}}, + "sms_source": { + "kind": "modemmanager", + "modem_object_path": "auto", + "storage_blacklist": [], + "state_ready_values": [3], + }, + } + config = load_config_dict(raw) + + def factory(_config): + return FakeModemManagerBus( + messages_by_modem={ + "/org/freedesktop/ModemManager1/Modem/0": ["/org/freedesktop/ModemManager1/SMS/10"], + "/org/freedesktop/ModemManager1/Modem/1": ["/org/freedesktop/ModemManager1/SMS/11"], + }, + sms_payloads={ + "/org/freedesktop/ModemManager1/SMS/10": { + "state": 3, + "number": "10000", + "text": "modem0 ready", + "timestamp": "2026-04-24T10:00:00+00:00", + "storage": "me", + }, + "/org/freedesktop/ModemManager1/SMS/11": { + "state": 3, + "number": "10001", + "text": "modem1 ready", + "timestamp": "2026-04-24T10:05:00+00:00", + "storage": "mt", + }, + }, + managed_objects=[ + "/org/freedesktop/ModemManager1/Modem/0", + "/org/freedesktop/ModemManager1/Modem/1", + ], + ) + + runtime = build_runtime(config, bus_factory=factory) + messages = list(runtime.source.fetch()) + + self.assertEqual(len(messages), 2) + self.assertEqual({message.phone_number for message in messages}, {"10000", "10001"}) + + def test_runtime_send_sms_uses_modemmanager_sender_and_returns_delivery_record(self): + raw = { + "forwarding": {"device_name": "router-send"}, + "notifications": {"console": {"enabled": True}}, + "sms_source": { + "kind": "modemmanager", + "modem_object_path": "/org/freedesktop/ModemManager1/Modem/9", + "state_ready_values": [3], + }, + } + config = load_config_dict(raw) + bus = FakeModemManagerBus(messages_by_modem={"/org/freedesktop/ModemManager1/Modem/9": []}) + runtime = build_runtime(config, bus_factory=lambda _config: bus) + + result = runtime.sms_sender.send(phone_number="10086", content="真实发送链路") + + self.assertTrue(result.success) + self.assertEqual(result.phone_number, "10086") + self.assertEqual(result.content, "真实发送链路") + self.assertEqual(result.modem_path, "/org/freedesktop/ModemManager1/Modem/9") + self.assertEqual(result.message_path, "/org/freedesktop/ModemManager1/Modem/9/sent/1") + self.assertEqual( + bus.sent_messages, + [ + { + "modem_path": "/org/freedesktop/ModemManager1/Modem/9", + "phone_number": "10086", + "content": "真实发送链路", + } + ], + ) + + def test_runtime_send_sms_without_sender_returns_disabled_result(self): + runtime = build_runtime(load_config_dict({})) + + result = runtime.sms_sender.send(phone_number="10086", content="noop") + + self.assertFalse(result.success) + self.assertEqual(result.error_category, "disabled") + self.assertIn("sms send is not configured", result.error) + + def test_forward_loop_deduplicates_messages_across_polls(self): + runtime = build_runtime(load_config_dict({})) + runtime.source = SequenceSmsSource( + [ + [ + { + "phone_number": "10086", + "content": "验证码 123456", + "received_at": "2026-04-24 12:00:00", + } + ], + [ + { + "phone_number": "10086", + "content": "验证码 123456", + "received_at": "2026-04-24 12:00:00", + }, + { + "phone_number": "10010", + "content": "余额提醒", + "received_at": "2026-04-24 12:01:00", + }, + ], + ] + ) + runtime.dispatcher = RecordingDispatcher() + + result = run_forward_loop(runtime, max_iterations=2) + + self.assertEqual(result.polls, 2) + self.assertEqual(result.fetched, 3) + self.assertEqual(result.dispatched, 2) + self.assertEqual(result.duplicates_skipped, 1) + self.assertEqual( + [(sms.phone_number, sms.content) for sms in runtime.dispatcher.dispatched], + [("10086", "验证码 123456"), ("10010", "余额提醒")], + ) + + def test_forward_loop_respects_max_history_limit(self): + runtime = build_runtime(load_config_dict({})) + runtime.source = SequenceSmsSource( + [ + [{"phone_number": "1", "content": "A", "received_at": "t1"}], + [{"phone_number": "2", "content": "B", "received_at": "t2"}], + [{"phone_number": "3", "content": "C", "received_at": "t3"}], + [{"phone_number": "1", "content": "A", "received_at": "t1"}], + ] + ) + runtime.dispatcher = RecordingDispatcher() + + result = run_forward_loop(runtime, max_iterations=4, max_seen_messages=2) + + self.assertEqual(result.dispatched, 4) + self.assertEqual(result.duplicates_skipped, 0) + self.assertEqual( + [sms.phone_number for sms in runtime.dispatcher.dispatched], + ["1", "2", "3", "1"], + ) + + def test_forward_loop_continues_after_dispatch_errors_and_retries_failed_messages(self): + runtime = build_runtime(load_config_dict({})) + runtime.source = SequenceSmsSource( + [ + [ + {"phone_number": "500", "content": "first try", "received_at": "t1"}, + {"phone_number": "200", "content": "delivered", "received_at": "t2"}, + ], + [ + {"phone_number": "500", "content": "first try", "received_at": "t1"}, + ], + ] + ) + runtime.dispatcher = FlakyDispatcher(failing_numbers={"500"}) + + result = run_forward_loop(runtime, max_iterations=2) + + self.assertEqual(result.polls, 2) + self.assertEqual(result.fetched, 3) + self.assertEqual(result.dispatched, 1) + self.assertEqual(result.dispatch_failures, 2) + self.assertEqual(result.duplicates_skipped, 0) + self.assertEqual(runtime.dispatcher.calls, ["500", "200", "500"]) + self.assertEqual([message.title for message in result.delivered_messages], ["ok 200"]) + + def test_forward_loop_records_sender_failure_details_without_retrying_partial_success_message(self): + from dbussmsforward.notifications import NotificationDispatcher + + runtime = build_runtime(load_config_dict({})) + runtime.source = SequenceSmsSource( + [ + [ + {"phone_number": "10010", "content": "partial failure", "received_at": "t1"}, + ], + [ + {"phone_number": "10010", "content": "partial failure", "received_at": "t1"}, + ], + ] + ) + runtime.dispatcher = NotificationDispatcher( + senders=[FakeSender("console"), FakeSender("autman_push", fail_for_numbers={"10010"})], + device_name="router-a", + ) + + result = run_forward_loop(runtime, max_iterations=2) + + self.assertEqual(result.dispatched, 1) + self.assertEqual(result.dispatch_failures, 1) + self.assertEqual(result.duplicates_skipped, 1) + self.assertEqual([message.channel for message in result.delivered_messages], ["console"]) + self.assertEqual(len(result.failure_details), 1) + self.assertEqual(result.failure_details[0].sender, "autman_push") + self.assertEqual(result.failure_details[0].phone_number, "10010") + self.assertIn("autman_push failed for 10010", result.failure_details[0].error) + + def test_forward_loop_records_fetch_issues_and_structured_sender_failures(self): + import tempfile + from pathlib import Path + + raw = { + "forwarding": {"device_name": "router-a"}, + "notifications": { + "console": {"enabled": True}, + "autman_push": { + "enabled": True, + "api_url": "https://autman.example/api/push", + "access_token": "***", + }, + }, + } + + class FakeHttpResponse: + def read(self, *_args, **_kwargs): + return b'{"error":"boom body"}' + + def close(self): + return None + + def opener(request: Request): + raise HTTPError(request.full_url, 503, "Service Unavailable", hdrs=None, fp=FakeHttpResponse()) + + config = load_config_dict(raw) + runtime = build_runtime(config, http_opener=opener) + + with tempfile.TemporaryDirectory() as tmpdir: + messages_path = Path(tmpdir) / "messages.json" + messages_path.write_text( + json.dumps( + [ + {"phone_number": "10010", "content": "ok", "received_at": "t1"}, + {"phone_number": "", "content": "", "received_at": "t2"}, + ], + ensure_ascii=False, + ), + encoding="utf-8", + ) + runtime.source = __import__("dbussmsforward.source", fromlist=["FileSmsSource"]).FileSmsSource( + path=str(messages_path) + ) + result = run_forward_loop(runtime, max_iterations=1) + + self.assertEqual(result.fetch_issue_count, 1) + self.assertEqual(result.dispatch_failures, 1) + self.assertEqual(result.failure_details[0].sender, "autman_push") + self.assertEqual(result.failure_details[0].error_category, "http") + self.assertEqual(result.failure_details[0].status_code, 503) + self.assertIn("boom body", result.failure_details[0].response_excerpt) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_web.py b/tests/test_web.py new file mode 100644 index 0000000..7b407e8 --- /dev/null +++ b/tests/test_web.py @@ -0,0 +1,229 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from dbussmsforward.config import load_config_dict +from dbussmsforward.sms import SmsMessage, SmsSendResult +from dbussmsforward.web import build_handler_class, create_web_app + + +class RecordingDispatcher: + def __init__(self): + self.messages = [] + + def dispatch(self, sms: SmsMessage): + self.messages.append(sms) + return [] + + +class RecordingSmsSender: + def __init__(self, *, result: SmsSendResult | None = None): + self.calls = [] + self._result = result + + def send(self, phone_number: str, content: str): + self.calls.append({"phone_number": phone_number, "content": content}) + if self._result is not None: + return SmsSendResult( + success=self._result.success, + phone_number=phone_number, + content=content, + modem_path=self._result.modem_path, + message_path=self._result.message_path, + error=self._result.error, + error_category=self._result.error_category, + ) + return SmsSendResult( + success=True, + phone_number=phone_number, + content=content, + modem_path="/org/freedesktop/ModemManager1/Modem/0", + message_path="/org/freedesktop/ModemManager1/SMS/100", + ) + + +class WebAppTests(unittest.TestCase): + def test_health_status_and_settings_round_trip(self): + with tempfile.TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "appsettings.json" + app = create_web_app( + load_config_dict( + { + "daemon": {"host": "127.0.0.1", "port": 9090}, + "forwarding": {"device_name": "router-web"}, + "notifications": {"console": {"enabled": True}}, + } + ), + config_path=config_path, + ) + + health = app.handle_request("GET", "/api/health") + self.assertEqual(health["status"], 200) + self.assertEqual(json.loads(health["body"])["status"], "ok") + + system_status = app.handle_request("GET", "/api/system/status") + status_payload = json.loads(system_status["body"]) + self.assertEqual(system_status["status"], 200) + self.assertEqual(status_payload["daemon"]["host"], "127.0.0.1") + self.assertEqual(status_payload["forwarding"]["device_name"], "router-web") + self.assertEqual(status_payload["notifications"]["enabled_channels"], ["console"]) + + settings = app.handle_request("GET", "/api/settings") + self.assertEqual(settings["status"], 200) + settings_payload = json.loads(settings["body"]) + self.assertEqual(settings_payload["forwarding"]["device_name"], "router-web") + + update = app.handle_request( + "POST", + "/api/settings", + body=json.dumps( + { + "forwarding": {"device_name": "router-updated"}, + "notifications": {"console": {"enabled": False}}, + } + ).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(update["status"], 200) + updated_payload = json.loads(update["body"]) + self.assertEqual(updated_payload["forwarding"]["device_name"], "router-updated") + self.assertFalse(updated_payload["notifications"]["console"]["enabled"]) + + persisted = json.loads(config_path.read_text(encoding="utf-8")) + self.assertEqual(persisted["forwarding"]["device_name"], "router-updated") + + def test_send_api_dispatches_message(self): + app = create_web_app(load_config_dict({"forwarding": {"device_name": "router-web"}})) + dispatcher = RecordingDispatcher() + app.runtime.dispatcher = dispatcher + + response = app.handle_request( + "POST", + "/api/sms/send", + body=json.dumps({"phone_number": "10086", "content": "hello from web"}).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + + self.assertEqual(response["status"], 200) + payload = json.loads(response["body"]) + self.assertEqual(payload["status"], "queued") + self.assertEqual(len(dispatcher.messages), 1) + self.assertEqual(dispatcher.messages[0].phone_number, "10086") + self.assertEqual(dispatcher.messages[0].content, "hello from web") + self.assertEqual(dispatcher.messages[0].received_at, "manual") + + def test_send_api_supports_real_sms_send_mode(self): + app = create_web_app(load_config_dict({})) + sender = RecordingSmsSender() + app.runtime.sms_sender = sender + + response = app.handle_request( + "POST", + "/api/sms/send", + body=json.dumps( + {"phone_number": "10086", "content": "web real send", "delivery_mode": "real_sms"} + ).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + + self.assertEqual(response["status"], 200) + payload = json.loads(response["body"]) + self.assertEqual(payload["status"], "sent") + self.assertEqual(payload["delivery_mode"], "real_sms") + self.assertEqual(payload["modem_path"], "/org/freedesktop/ModemManager1/Modem/0") + self.assertEqual(payload["message_path"], "/org/freedesktop/ModemManager1/SMS/100") + self.assertEqual(sender.calls, [{"phone_number": "10086", "content": "web real send"}]) + + def test_send_api_reports_real_sms_send_failures(self): + app = create_web_app(load_config_dict({})) + sender = RecordingSmsSender( + result=SmsSendResult( + success=False, + phone_number="", + content="", + error="sms send is not configured", + error_category="disabled", + ) + ) + app.runtime.sms_sender = sender + + response = app.handle_request( + "POST", + "/api/sms/send", + body=json.dumps( + {"phone_number": "10010", "content": "will fail", "delivery_mode": "real_sms"} + ).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + + self.assertEqual(response["status"], 503) + payload = json.loads(response["body"]) + self.assertEqual(payload["error"], "sms_send_failed") + self.assertEqual(payload["error_category"], "disabled") + self.assertIn("sms send is not configured", payload["detail"]) + + def test_send_api_rejects_missing_fields(self): + app = create_web_app(load_config_dict({})) + + response = app.handle_request( + "POST", + "/api/sms/send", + body=json.dumps({"content": "missing phone"}).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + + self.assertEqual(response["status"], 400) + payload = json.loads(response["body"]) + self.assertEqual(payload["error"], "validation_error") + self.assertIn("phone_number", payload["detail"]) + + def test_static_assets_are_served(self): + app = create_web_app(load_config_dict({})) + + index_response = app.handle_request("GET", "/") + self.assertEqual(index_response["status"], 200) + self.assertIn("DbusSmsForward", index_response["body"]) + self.assertEqual(index_response["content_type"], "text/html; charset=utf-8") + + js_response = app.handle_request("GET", "/app.js") + self.assertEqual(js_response["status"], 200) + self.assertIn("fetch", js_response["body"]) + self.assertEqual(js_response["content_type"], "application/javascript; charset=utf-8") + + def test_handler_class_serves_http_requests(self): + app = create_web_app(load_config_dict({"forwarding": {"device_name": "router-http"}})) + handler_class = build_handler_class(app) + + with tempfile.TemporaryDirectory() as tmpdir: + request_path = Path(tmpdir) / "request.txt" + request_path.write_text( + "GET /api/health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + encoding="utf-8", + ) + + import socketserver + import threading + from http.server import HTTPServer + + class TestServer(socketserver.TCPServer): + allow_reuse_address = True + + server = TestServer(("127.0.0.1", 0), handler_class) + thread = threading.Thread(target=server.handle_request, daemon=True) + thread.start() + + import urllib.request + + try: + with urllib.request.urlopen(f"http://127.0.0.1:{server.server_address[1]}/api/health", timeout=5) as response: + body = response.read().decode("utf-8") + self.assertEqual(response.status, 200) + self.assertEqual(json.loads(body)["status"], "ok") + finally: + server.server_close() + thread.join(timeout=5) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_web_product.py b/tests/test_web_product.py new file mode 100644 index 0000000..089e8f1 --- /dev/null +++ b/tests/test_web_product.py @@ -0,0 +1,223 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from dbussmsforward.config import load_config_dict +from dbussmsforward.sms import NotificationMessage, SmsMessage +from dbussmsforward.web import create_web_app + + +class RecordingDispatcher: + def __init__(self): + self.messages = [] + + def dispatch(self, sms: SmsMessage): + self.messages.append(sms) + return [ + NotificationMessage( + channel="console", + title=f"短信转发 {sms.phone_number}", + body=f"{sms.content}", + ) + ] + + +class WebProductTests(unittest.TestCase): + def test_sms_send_response_shape_is_consistent_across_delivery_modes(self): + class RealSmsSender: + def send(self, phone_number: str, content: str): + from types import SimpleNamespace + + return SimpleNamespace( + success=True, + phone_number=phone_number, + content=content, + modem_path="/org/freedesktop/ModemManager1/Modem/0", + message_path="/org/freedesktop/ModemManager1/SMS/1", + error=None, + error_category=None, + ) + + app = create_web_app(load_config_dict({})) + app.runtime.dispatcher = RecordingDispatcher() + app.runtime.sms_sender = RealSmsSender() + + dispatch_response = app.handle_request( + "POST", + "/api/sms/send", + body=json.dumps({"phone_number": "10086", "content": "dispatch ok", "delivery_mode": "dispatch"}).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + real_sms_response = app.handle_request( + "POST", + "/api/sms/send", + body=json.dumps({"phone_number": "10010", "content": "real sms ok", "delivery_mode": "real_sms"}).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + + dispatch_payload = json.loads(dispatch_response["body"]) + real_sms_payload = json.loads(real_sms_response["body"]) + self.assertEqual(dispatch_response["status"], 200) + self.assertEqual(real_sms_response["status"], 200) + for payload in (dispatch_payload, real_sms_payload): + self.assertEqual(payload["ok"], True) + self.assertIn(payload["delivery_mode"], {"dispatch", "real_sms"}) + self.assertIn("status", payload) + self.assertIn("phone_number", payload) + self.assertIn("received_at", payload) + self.assertIn("notification_count", payload) + self.assertIn("message_path", payload) + self.assertIn("modem_path", payload) + self.assertIn("error_category", payload) + self.assertIn("detail", payload) + + self.assertEqual(dispatch_payload["notification_count"], 1) + self.assertEqual(dispatch_payload["message_path"], None) + self.assertEqual(dispatch_payload["modem_path"], None) + self.assertEqual(real_sms_payload["notification_count"], 1) + self.assertEqual(real_sms_payload["message_path"], "/org/freedesktop/ModemManager1/SMS/1") + + app = create_web_app( + load_config_dict( + { + "forwarding": {"device_name": "router-finish"}, + "notifications": {"console": {"enabled": True}}, + } + ) + ) + app.runtime.dispatcher = RecordingDispatcher() + + send_response = app.handle_request( + "POST", + "/api/sms/send", + body=json.dumps({"phone_number": "10010", "content": "hello dashboard"}).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(send_response["status"], 200) + + dashboard = app.handle_request("GET", "/api/dashboard") + dashboard_payload = json.loads(dashboard["body"]) + self.assertEqual(dashboard["status"], 200) + self.assertEqual(dashboard_payload["summary"]["manual_send_count"], 1) + self.assertEqual(dashboard_payload["summary"]["event_count"], 1) + self.assertEqual(dashboard_payload["summary"]["last_sms_phone_number"], "10010") + self.assertTrue(dashboard_payload["latest_events"]) + + events = app.handle_request("GET", "/api/events") + events_payload = json.loads(events["body"]) + self.assertEqual(events["status"], 200) + self.assertEqual(len(events_payload["events"]), 1) + self.assertEqual(events_payload["events"][0]["phone_number"], "10010") + self.assertEqual(events_payload["events"][0]["notification_count"], 1) + self.assertEqual(events_payload["events"][0]["result"], "success") + self.assertEqual(events_payload["events"][0]["delivery_mode"], "dispatch") + self.assertEqual(events_payload["events"][0]["error_category"], None) + + def test_dashboard_reports_send_failures(self): + class FailingDispatcher: + def dispatch(self, sms: SmsMessage): + raise RuntimeError("dispatch exploded") + + app = create_web_app(load_config_dict({})) + app.runtime.dispatcher = FailingDispatcher() + + response = app.handle_request( + "POST", + "/api/sms/send", + body=json.dumps({"phone_number": "10000", "content": "boom"}).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(response["status"], 500) + payload = json.loads(response["body"]) + self.assertEqual(payload["error"], "dispatch_failed") + self.assertEqual(payload["ok"], False) + self.assertEqual(payload["status"], "failed") + self.assertEqual(payload["phone_number"], "10000") + + dashboard = json.loads(app.handle_request("GET", "/api/dashboard")["body"]) + self.assertEqual(dashboard["summary"]["failure_count"], 1) + self.assertEqual(dashboard["summary"]["event_count"], 1) + self.assertEqual(dashboard["latest_events"][0]["result"], "failed") + self.assertEqual(dashboard["latest_events"][0]["delivery_mode"], "dispatch") + self.assertEqual(dashboard["latest_events"][0]["error_category"], "dispatch_failed") + + def test_static_index_contains_dashboard_sections(self): + app = create_web_app(load_config_dict({})) + index_response = app.handle_request("GET", "/") + body = index_response["body"] + self.assertEqual(index_response["status"], 200) + self.assertIn("运行总览", body) + self.assertIn("最近事件", body) + self.assertIn("配置中心", body) + self.assertIn("短信测试台", body) + + def test_asset_bundle_contains_dashboard_rendering_logic(self): + app = create_web_app(load_config_dict({})) + js_response = app.handle_request("GET", "/app.js") + self.assertEqual(js_response["status"], 200) + self.assertIn("loadDashboard", js_response["body"]) + self.assertIn("renderEvents", js_response["body"]) + + def test_web_console_exposes_delivery_mode_selector_for_product_use(self): + app = create_web_app(load_config_dict({})) + index_response = app.handle_request("GET", "/") + js_response = app.handle_request("GET", "/app.js") + css_response = app.handle_request("GET", "/app.css") + + self.assertEqual(index_response["status"], 200) + self.assertEqual(js_response["status"], 200) + self.assertEqual(css_response["status"], 200) + self.assertIn("delivery_mode", index_response["body"]) + self.assertIn("真实短信", index_response["body"]) + self.assertIn("mode-hint", index_response["body"]) + self.assertIn("dispatch", js_response["body"]) + self.assertIn("real_sms", js_response["body"]) + self.assertIn("formData.get('delivery_mode')", js_response["body"]) + self.assertIn("需要已配置 ModemManager 发送能力", js_response["body"]) + self.assertIn("renderResult", js_response["body"]) + self.assertIn("error_category", js_response["body"]) + self.assertIn("mode-hint-warning", css_response["body"]) + + def test_recent_events_support_operator_filtering_and_failure_visuals(self): + app = create_web_app(load_config_dict({})) + index_response = app.handle_request("GET", "/") + js_response = app.handle_request("GET", "/app.js") + css_response = app.handle_request("GET", "/app.css") + + self.assertEqual(index_response["status"], 200) + self.assertEqual(js_response["status"], 200) + self.assertEqual(css_response["status"], 200) + self.assertIn("事件搜索", index_response["body"]) + self.assertIn("只看失败", index_response["body"]) + self.assertIn("event-search", index_response["body"]) + self.assertIn("getFilteredEvents", js_response["body"]) + self.assertIn("没有匹配当前筛选条件的事件", js_response["body"]) + self.assertIn("eventResultFilter", js_response["body"]) + self.assertIn("event-mode-badge", css_response["body"]) + self.assertIn("event-failure-meta", css_response["body"]) + self.assertIn("event-toolbar", css_response["body"]) + self.assertIn("escapeHtml", js_response["body"]) + self.assertIn("当前事件数", js_response["body"]) + self.assertIn("状态、方式", index_response["body"]) + + def test_settings_update_reflects_in_dashboard(self): + with tempfile.TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "appsettings.json" + app = create_web_app(load_config_dict({"forwarding": {"device_name": "before"}}), config_path=config_path) + response = app.handle_request( + "POST", + "/api/settings", + body=json.dumps({"forwarding": {"device_name": "after"}}).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(response["status"], 200) + + dashboard = json.loads(app.handle_request("GET", "/api/dashboard")["body"]) + self.assertEqual(dashboard["runtime"]["forwarding"]["device_name"], "after") + persisted = json.loads(config_path.read_text(encoding="utf-8")) + self.assertEqual(persisted["forwarding"]["device_name"], "after") + + +if __name__ == "__main__": + unittest.main() diff --git a/wwwroot/app.css b/wwwroot/app.css new file mode 100644 index 0000000..bb80210 --- /dev/null +++ b/wwwroot/app.css @@ -0,0 +1,358 @@ +:root { + color-scheme: dark; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: #0f1115; + color: #f5f7fa; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: + radial-gradient(circle at top right, rgba(59, 130, 246, 0.12), transparent 30%), + linear-gradient(180deg, #0b1020 0%, #0f1115 100%); +} + +.layout { + max-width: 1180px; + margin: 0 auto; + padding: 28px; + display: grid; + gap: 18px; +} + +.card { + background: rgba(24, 28, 36, 0.92); + border: 1px solid #2a3140; + border-radius: 16px; + padding: 22px; + box-shadow: 0 14px 36px rgba(0, 0, 0, 0.22); + backdrop-filter: blur(10px); +} + +.hero { + display: flex; + justify-content: space-between; + gap: 20px; + align-items: flex-start; +} + +.hero-actions, +.actions, +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.panel-header { + margin-bottom: 14px; +} + +.eyebrow { + margin: 0 0 10px; + color: #60a5fa; + font-size: 13px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +h1, +h2, +p { + margin-top: 0; +} + +h1 { + margin-bottom: 10px; + font-size: 34px; +} + +h2 { + margin-bottom: 6px; +} + +.muted { + color: #94a3b8; +} + +.metrics-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.metric-card { + display: grid; + gap: 8px; +} + +.metric-label { + color: #94a3b8; + font-size: 14px; +} + +.metric-value { + font-size: 32px; + line-height: 1.1; +} + +.metric-value.small { + font-size: 24px; + word-break: break-all; +} + +.metric-value.danger { + color: #f87171; +} + +.summary-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.summary-item { + padding: 14px; + border-radius: 12px; + background: #111827; + border: 1px solid #263041; + display: grid; + gap: 6px; +} + +.summary-label, +.event-meta, +.event-detail, +.empty-state { + color: #94a3b8; + font-size: 14px; +} + +.summary-value { + font-size: 16px; + word-break: break-word; +} + +.event-toolbar { + display: grid; + gap: 12px; + grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); + margin-bottom: 14px; +} + +.events-list { + display: grid; + gap: 12px; +} + +.event-item { + border: 1px solid #263041; + border-radius: 12px; + padding: 14px; + background: #0b1220; + display: grid; + gap: 8px; +} + +.event-topline { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.event-badges { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.event-mode-badge, +.event-status, +.event-error-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 72px; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + text-transform: uppercase; +} + +.event-mode-badge { + background: rgba(59, 130, 246, 0.16); + color: #93c5fd; +} + +.event-error-badge { + min-width: auto; + background: rgba(239, 68, 68, 0.16); + color: #fca5a5; +} + +.status-success { + background: rgba(34, 197, 94, 0.18); + color: #4ade80; +} + +.status-failed { + background: rgba(239, 68, 68, 0.18); + color: #f87171; +} + +.event-item-failed { + border-color: rgba(239, 68, 68, 0.3); +} + +.event-failure-meta { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + color: #fecaca; + font-size: 13px; +} + +.event-body { + white-space: pre-wrap; + word-break: break-word; +} + +pre, +textarea, +input, +select { + width: 100%; + border-radius: 10px; + border: 1px solid #334155; + background: #0b1220; + color: #e2e8f0; + padding: 12px; + font: 14px/1.5 'SFMono-Regular', ui-monospace, monospace; +} + +textarea { + min-height: 260px; +} + +label, +.form-grid { + display: grid; + gap: 12px; +} + +button { + border: 0; + border-radius: 10px; + background: linear-gradient(135deg, #3b82f6, #2563eb); + color: white; + padding: 10px 16px; + font-weight: 600; + cursor: pointer; +} + +button:hover { + filter: brightness(1.08); +} + +.empty-state { + padding: 18px; + border: 1px dashed #334155; + border-radius: 12px; + background: rgba(15, 23, 42, 0.55); +} + +.mode-hint { + padding: 12px 14px; + border-radius: 12px; + border: 1px solid #263041; + background: rgba(15, 23, 42, 0.7); + color: #cbd5e1; + font-size: 14px; + line-height: 1.6; +} + +.mode-hint-warning { + border-color: rgba(250, 204, 21, 0.35); + background: rgba(120, 53, 15, 0.28); + color: #fde68a; +} + +.result-panel { + display: grid; + gap: 10px; +} + +.result-title { + font-size: 16px; + font-weight: 700; +} + +.result-title.success { + color: #4ade80; +} + +.result-title.failed { + color: #f87171; +} + +.result-grid { + display: grid; + gap: 10px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.result-item { + padding: 12px; + border-radius: 12px; + border: 1px solid #263041; + background: #0b1220; + display: grid; + gap: 4px; +} + +.result-label { + color: #94a3b8; + font-size: 13px; +} + +.result-value { + color: #f8fafc; + font-size: 14px; + word-break: break-word; +} + +@media (max-width: 960px) { + .metrics-grid, + .summary-grid, + .event-toolbar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .hero, + .panel-header, + .actions, + .event-topline { + flex-direction: column; + align-items: stretch; + } +} + +@media (max-width: 640px) { + .layout { + padding: 16px; + } + + .metrics-grid, + .summary-grid, + .event-toolbar { + grid-template-columns: 1fr; + } +} diff --git a/wwwroot/app.js b/wwwroot/app.js new file mode 100644 index 0000000..d6f8e2d --- /dev/null +++ b/wwwroot/app.js @@ -0,0 +1,390 @@ +const statusView = document.getElementById('status-view'); +const settingsEditor = document.getElementById('settings-editor'); +const resultView = document.getElementById('result-view'); +const dashboardSummary = document.getElementById('dashboard-summary'); +const eventsList = document.getElementById('events-list'); +const eventsEmpty = document.getElementById('events-empty'); +const refreshDashboardButton = document.getElementById('refresh-dashboard'); +const refreshStatusButton = document.getElementById('refresh-status'); +const refreshEventsButton = document.getElementById('refresh-events'); +const reloadSettingsButton = document.getElementById('reload-settings'); +const saveSettingsButton = document.getElementById('save-settings'); +const smsForm = document.getElementById('sms-form'); +const deliveryModeSelect = document.getElementById('sms-delivery-mode'); +const modeHint = document.getElementById('mode-hint'); +const eventSearchInput = document.getElementById('event-search'); +const eventResultFilter = document.getElementById('event-result-filter'); + +const metricEventCount = document.getElementById('metric-event-count'); +const metricManualSendCount = document.getElementById('metric-manual-send-count'); +const metricFailureCount = document.getElementById('metric-failure-count'); +const metricLastPhone = document.getElementById('metric-last-phone'); + +const deliveryModeLabelMap = { + dispatch: '通知转发', + real_sms: '真实短信', +}; + +let latestEvents = []; + +function getDeliveryModeLabel(mode) { + return deliveryModeLabelMap[mode] || mode || '未知'; +} + +function escapeHtml(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function formatText(value, fallback = '-') { + const text = String(value ?? '').trim(); + return text || fallback; +} + +function getResultLabel(result) { + if (result === 'success') { + return '成功'; + } + if (result === 'failed') { + return '失败'; + } + return formatText(result, '未知'); +} + +function updateModeHint() { + const deliveryMode = deliveryModeSelect?.value || 'dispatch'; + if (!modeHint) { + return; + } + if (deliveryMode === 'real_sms') { + modeHint.textContent = '当前为真实短信:需要已配置 ModemManager 发送能力;若发送失败,将展示错误分类与失败原因。'; + modeHint.classList.add('mode-hint-warning'); + return; + } + modeHint.textContent = '当前为通知转发:会按已启用通道分发,并写入事件留痕。'; + modeHint.classList.remove('mode-hint-warning'); +} + +async function fetchJson(url, options = {}) { + const response = await fetch(url, options); + const text = await response.text(); + let data; + try { + data = text ? JSON.parse(text) : {}; + } catch (error) { + throw new Error(`响应不是合法 JSON: ${text}`); + } + if (!response.ok) { + const failure = new Error(data.detail || data.error || `HTTP ${response.status}`); + failure.payload = data; + failure.status = response.status; + throw failure; + } + return data; +} + +function renderJson(element, payload) { + element.textContent = JSON.stringify(payload, null, 2); +} + +function renderResult(payload, options = {}) { + const status = options.status || 'success'; + const title = options.title || (status === 'failed' ? '发送失败' : '操作成功'); + const rows = Object.entries(payload || {}).filter(([, value]) => value !== undefined && value !== null && value !== ''); + if (!rows.length) { + renderJson(resultView, payload || {}); + return; + } + resultView.innerHTML = ` +
+
${escapeHtml(title)}
+
+ ${rows + .map( + ([label, value]) => ` +
+ ${escapeHtml(label)} + ${escapeHtml(formatText(value))} +
+ ` + ) + .join('')} +
+
+ `; +} + +function updateMetric(element, value, fallback = '-') { + element.textContent = value ?? fallback; +} + +function renderSummaryItem(label, value) { + return ` +
+ ${escapeHtml(label)} + ${escapeHtml(formatText(value))} +
+ `; +} + +function renderDashboard(payload) { + const { summary = {}, runtime = {} } = payload; + updateMetric(metricEventCount, summary.event_count ?? 0, '0'); + updateMetric(metricManualSendCount, summary.manual_send_count ?? 0, '0'); + updateMetric(metricFailureCount, summary.failure_count ?? 0, '0'); + updateMetric(metricLastPhone, summary.last_sms_phone_number || '-'); + + dashboardSummary.innerHTML = [ + renderSummaryItem('服务状态', runtime.status || 'unknown'), + renderSummaryItem('启动时间', runtime.started_at || '-'), + renderSummaryItem('请求次数', runtime.request_count ?? 0), + renderSummaryItem('配置文件', runtime.config_path || '未落盘'), + renderSummaryItem('运行模式', runtime.daemon?.mode || '-'), + renderSummaryItem('监听地址', `${runtime.daemon?.host || '-'}:${runtime.daemon?.port || '-'}`), + renderSummaryItem('日志级别', runtime.daemon?.log_level || '-'), + renderSummaryItem('设备名', runtime.forwarding?.device_name || '-'), + renderSummaryItem('启用通道', (runtime.notifications?.enabled_channels || []).join(', ') || '无'), + renderSummaryItem('短信源', runtime.sms_source?.kind || '-'), + renderSummaryItem('源路径', runtime.sms_source?.path || '-'), + renderSummaryItem('轮询间隔', runtime.sms_source?.poll_interval_seconds ?? '-'), + renderSummaryItem('当前事件数', summary.filtered_event_count ?? summary.event_count ?? 0), + ].join(''); +} + +function getEventSearchText(event) { + return [ + event.phone_number, + event.content, + event.detail, + event.error_category, + getDeliveryModeLabel(event.delivery_mode || ((event.channels || []).includes('real_sms') ? 'real_sms' : 'dispatch')), + (event.channels || []).join(' '), + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); +} + +function getFilteredEvents(events = latestEvents) { + const keyword = (eventSearchInput?.value || '').trim().toLowerCase(); + const resultFilter = eventResultFilter?.value || 'all'; + return events.filter((event) => { + const normalizedResult = event.result === 'success' || event.result === 'failed' ? event.result : 'unknown'; + if (resultFilter !== 'all' && normalizedResult !== resultFilter) { + return false; + } + if (!keyword) { + return true; + } + return `${normalizedResult} ${getResultLabel(normalizedResult)} ${getEventSearchText(event)}`.includes(keyword); + }); +} + +function renderEvents(events = latestEvents) { + latestEvents = Array.isArray(events) ? events : []; + const filteredEvents = getFilteredEvents(latestEvents); + if (!filteredEvents.length) { + eventsList.innerHTML = ''; + eventsEmpty.style.display = 'block'; + eventsEmpty.textContent = latestEvents.length + ? '没有匹配当前筛选条件的事件,换个关键字或筛选项试试。' + : '还没有事件,先去下面的短信测试台发一条试试。'; + return; + } + + eventsEmpty.style.display = 'none'; + eventsList.innerHTML = filteredEvents + .map((event) => { + const channels = (event.channels || []).join(', ') || '无'; + const normalizedResult = event.result === 'success' || event.result === 'failed' ? event.result : 'unknown'; + const statusClass = normalizedResult === 'success' ? 'status-success' : 'status-failed'; + const deliveryMode = event.delivery_mode || ((event.channels || []).includes('real_sms') ? 'real_sms' : 'dispatch'); + const deliveryModeLabel = getDeliveryModeLabel(deliveryMode); + const errorCategory = formatText(event.error_category, ''); + return ` +
+
+ ${escapeHtml(formatText(event.phone_number))} +
+ ${escapeHtml(deliveryModeLabel)} + ${escapeHtml(getResultLabel(normalizedResult))} +
+
+
时间:${escapeHtml(formatText(event.created_at))} · 来源:${escapeHtml(formatText(event.received_at))}
+
${escapeHtml(formatText(event.content))}
+
通知数:${escapeHtml(formatText(event.notification_count, '0'))} · 通道:${escapeHtml(channels)}
+ ${errorCategory ? `
错误分类${escapeHtml(errorCategory)}
` : ''} +
${escapeHtml(formatText(event.detail, '无补充说明'))}
+
+ `; + }) + .join(''); +} + +async function loadStatus() { + const status = await fetchJson('/api/system/status'); + renderJson(statusView, status); +} + +async function loadSettings() { + const settings = await fetchJson('/api/settings'); + settingsEditor.value = JSON.stringify(settings, null, 2); +} + +async function loadDashboard() { + const payload = await fetchJson('/api/dashboard'); + renderDashboard(payload); + renderEvents(payload.latest_events || []); +} + +async function loadEvents() { + const payload = await fetchJson('/api/events'); + renderEvents(payload.events || []); +} + +function bindEventFilters() { + eventSearchInput?.addEventListener('input', () => renderEvents(latestEvents)); + eventResultFilter?.addEventListener('change', () => renderEvents(latestEvents)); +} + +async function refreshAll() { + await Promise.all([loadDashboard(), loadStatus(), loadSettings()]); +} + +async function saveSettings() { + const parsed = JSON.parse(settingsEditor.value || '{}'); + const payload = await fetchJson('/api/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(parsed), + }); + renderResult( + { + 操作: '保存配置', + 状态: '成功', + 说明: '配置已通过校验并写入当前运行实例。', + }, + { title: '配置保存成功' } + ); + await refreshAll(); +} + +async function sendSms(event) { + event.preventDefault(); + const formData = new FormData(smsForm); + const deliveryMode = formData.get('delivery_mode') || 'dispatch'; + const delivery_mode_label = getDeliveryModeLabel(deliveryMode); + try { + const payload = await fetchJson('/api/sms/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + phone_number: formData.get('phone_number'), + content: formData.get('content'), + delivery_mode: deliveryMode === 'real_sms' ? 'real_sms' : 'dispatch', + }), + }); + renderResult( + { + 操作: '发送测试短信', + 状态: '成功', + delivery_mode_label, + 手机号: payload.phone_number, + 来源: payload.received_at, + MessagePath: payload.message_path, + ModemPath: payload.modem_path, + }, + { title: `${delivery_mode_label}发送成功` } + ); + await Promise.all([loadDashboard(), loadEvents(), loadStatus()]); + } catch (error) { + renderResult( + { + 操作: '发送测试短信', + 状态: '发送失败', + delivery_mode_label, + 错误分类: error.payload?.error_category || error.payload?.error || 'unknown', + 失败原因: error.payload?.detail || error.message, + HTTP状态: error.status || '-', + }, + { status: 'failed', title: `${delivery_mode_label}发送失败` } + ); + await Promise.all([loadDashboard(), loadEvents(), loadStatus()]); + } +} + +async function boot() { + try { + updateModeHint(); + bindEventFilters(); + await refreshAll(); + } catch (error) { + resultView.textContent = error.message; + } +} + +refreshDashboardButton.addEventListener('click', async () => { + try { + await loadDashboard(); + } catch (error) { + resultView.textContent = error.message; + } +}); + +refreshStatusButton.addEventListener('click', async () => { + try { + await loadStatus(); + } catch (error) { + resultView.textContent = error.message; + } +}); + +refreshEventsButton.addEventListener('click', async () => { + try { + await loadEvents(); + } catch (error) { + resultView.textContent = error.message; + } +}); + +reloadSettingsButton.addEventListener('click', async () => { + try { + await loadSettings(); + renderResult( + { + 操作: '重载配置', + 状态: '成功', + 说明: '编辑器已同步当前运行配置。', + }, + { title: '配置已重载' } + ); + } catch (error) { + resultView.textContent = error.message; + } +}); + +saveSettingsButton.addEventListener('click', async () => { + try { + await saveSettings(); + } catch (error) { + resultView.textContent = error.message; + } +}); + +if (deliveryModeSelect) { + deliveryModeSelect.addEventListener('change', updateModeHint); +} + +smsForm.addEventListener('submit', async (event) => { + try { + await sendSms(event); + } catch (error) { + resultView.textContent = error.message; + } +}); + +boot(); diff --git a/wwwroot/index.html b/wwwroot/index.html new file mode 100644 index 0000000..af4e248 --- /dev/null +++ b/wwwroot/index.html @@ -0,0 +1,149 @@ + + + + + + DbusSmsForward Web + + + +
+
+
+

DbusSmsForward · Debian-first

+

短信转发控制台

+

+ 一个可直接交付的成品界面:运行总览、最近事件、配置中心、短信测试台,全部放到同一个 Web 面板里。 +

+
+
+ + +
+
+ +
+
+ 累计事件 + - +
+
+ 手工发送成功 + - +
+
+ 失败次数 + - +
+
+ 最近手机号 + - +
+
+ +
+
+
+

运行总览

+

聚合运行状态、配置来源、通知通道与轮询源信息。

+
+
+
+
+ +
+
+
+

最近事件

+

展示最近短信转发记录,便于直接看成功/失败、发送方式与错误分类。

+
+ +
+
+ + +
+
还没有事件,先去下面的短信测试台发一条试试。
+
+
+ +
+
+
+

系统状态

+

原始接口状态,适合排错时直接查看 JSON。

+
+
+
加载中...
+
+ +
+
+
+

配置中心

+

直接在线改配置并落盘,适合 Debian/OpenWRT 迁移后的单机维护。

+
+
+ + +
+
+ +
+ +
+
+
+

短信测试台

+

用于手工发送测试短信,验证通知链路与事件留痕。

+
+
+
+ + + +
+ 当前为通知转发:会按已启用通道分发,并写入事件留痕。 +
+
+ +
+
+
+ +
+
+
+

操作结果

+

显示最近一次保存配置、刷新或手工发送的回执。

+
+
+
等待操作...
+
+
+ + + +