Initial commit: SMS Forwarder project with Docker support
This commit is contained in:
62
.dockerignore
Normal file
62
.dockerignore
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
env
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
.tox
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.log
|
||||||
|
.git
|
||||||
|
.mypy_cache
|
||||||
|
.pytest_cache
|
||||||
|
.hypothesis
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
logs/
|
||||||
|
data/
|
||||||
|
config.yaml
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose*.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs/
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Scripts
|
||||||
|
scripts/
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
tests/
|
162
.gitignore
vendored
Normal file
162
.gitignore
vendored
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
config.yaml
|
||||||
|
logs/
|
||||||
|
data/
|
||||||
|
gotify_data/
|
||||||
|
ssl/
|
||||||
|
|
||||||
|
# OS specific
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
sms_logs/
|
||||||
|
sms_data/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.backup
|
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装系统依赖
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 安装 uv
|
||||||
|
RUN pip install uv
|
||||||
|
|
||||||
|
# 复制项目文件
|
||||||
|
COPY pyproject.toml .
|
||||||
|
COPY sms_forwarder/ ./sms_forwarder/
|
||||||
|
|
||||||
|
# 安装 Python 依赖
|
||||||
|
RUN uv sync --frozen
|
||||||
|
|
||||||
|
# 创建必要目录
|
||||||
|
RUN mkdir -p logs data
|
||||||
|
|
||||||
|
# 创建非 root 用户
|
||||||
|
RUN useradd --create-home --shell /bin/bash app
|
||||||
|
RUN chown -R app:app /app
|
||||||
|
USER app
|
||||||
|
|
||||||
|
# 暴露端口(使用配置文件中的端口)
|
||||||
|
EXPOSE 12152
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:12152/health || exit 1
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["uv", "run", "python", "-m", "sms_forwarder.main"]
|
71
README.md
Normal file
71
README.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# SMS Forwarder
|
||||||
|
|
||||||
|
一个将 iOS 短信转发到 Android 设备的通知服务器。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 接收来自 iPhone 快捷指令的短信内容
|
||||||
|
- 支持多种推送方式(Pushbullet、FCM、Gotify、ntfy 等)
|
||||||
|
- 基于 FastAPI 的高性能 HTTP 服务器
|
||||||
|
- 使用 Apprise 库支持 80+ 种通知服务
|
||||||
|
- YAML 配置文件,支持多推送目标
|
||||||
|
- API 密钥认证,确保安全性
|
||||||
|
- 完整的日志记录和错误处理
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 环境要求
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- uv 包管理器
|
||||||
|
|
||||||
|
### 2. 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone <repository-url>
|
||||||
|
cd notification
|
||||||
|
|
||||||
|
# 使用 uv 安装依赖
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置
|
||||||
|
|
||||||
|
复制配置文件模板并编辑:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp config.example.yaml config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `config.yaml` 文件,配置你的推送服务。
|
||||||
|
|
||||||
|
### 4. 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式
|
||||||
|
uv run uvicorn sms_forwarder.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
# 生产模式
|
||||||
|
uv run sms-forwarder
|
||||||
|
```
|
||||||
|
|
||||||
|
## 支持的推送服务
|
||||||
|
|
||||||
|
- **Pushbullet** - 推荐,设置简单
|
||||||
|
- **FCM (Firebase Cloud Messaging)** - Google 官方推送
|
||||||
|
- **Gotify** - 自托管推送服务
|
||||||
|
- **ntfy** - 开源推送服务
|
||||||
|
- **Discord、Telegram、Slack** 等 80+ 种服务
|
||||||
|
|
||||||
|
## iPhone 快捷指令配置
|
||||||
|
|
||||||
|
详见 [iPhone 配置指南](docs/iphone-setup.md)
|
||||||
|
|
||||||
|
## API 文档
|
||||||
|
|
||||||
|
服务器启动后访问 `http://localhost:8000/docs` 查看 API 文档。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
67
config.example.yaml
Normal file
67
config.example.yaml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# SMS Forwarder 配置文件示例
|
||||||
|
# 复制此文件为 config.yaml 并根据需要修改
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
server:
|
||||||
|
host: "0.0.0.0"
|
||||||
|
port: 12152
|
||||||
|
# API 密钥,用于验证请求(必须设置)
|
||||||
|
# 建议使用强密码生成器生成,例如:python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
|
api_key: "ayNESyIW2pCQ5Ts-O8FC5t8mzhb26kbDZEr4I7PynHg"
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging:
|
||||||
|
level: "INFO" # DEBUG, INFO, WARNING, ERROR
|
||||||
|
file: "logs/sms_forwarder.log"
|
||||||
|
|
||||||
|
# 通知配置
|
||||||
|
notifications:
|
||||||
|
# 默认通知服务(必须配置至少一个)
|
||||||
|
services:
|
||||||
|
# Pushbullet 示例(推荐)
|
||||||
|
# 将 your-pushbullet-access-token 替换为你从 Pushbullet 网站获取的 Access Token
|
||||||
|
- name: "pushbullet"
|
||||||
|
url: "pbul://o.YV9e3ugEsg2bWKZ0U7HA4IlTLkEp1BtU"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# FCM 示例
|
||||||
|
# - name: "fcm"
|
||||||
|
# url: "fcm://project@apikey/DEVICE_ID"
|
||||||
|
# enabled: false
|
||||||
|
|
||||||
|
# Gotify 示例(自托管)
|
||||||
|
# - name: "gotify"
|
||||||
|
# url: "gotify://hostname/token"
|
||||||
|
# enabled: false
|
||||||
|
|
||||||
|
# ntfy 示例
|
||||||
|
# - name: "ntfy"
|
||||||
|
# url: "ntfy://your-topic/"
|
||||||
|
# enabled: false
|
||||||
|
|
||||||
|
# Discord Webhook 示例
|
||||||
|
# - name: "discord"
|
||||||
|
# url: "discord://webhook_id/webhook_token"
|
||||||
|
# enabled: false
|
||||||
|
|
||||||
|
# 通知模板
|
||||||
|
templates:
|
||||||
|
# 短信通知模板
|
||||||
|
sms:
|
||||||
|
title: "📱 新短信 - {sender}"
|
||||||
|
body: |
|
||||||
|
发件人: {sender}
|
||||||
|
时间: {timestamp}
|
||||||
|
内容: {content}
|
||||||
|
|
||||||
|
# 系统通知模板
|
||||||
|
system:
|
||||||
|
title: "🔔 系统通知"
|
||||||
|
body: "{message}"
|
||||||
|
|
||||||
|
# 安全配置
|
||||||
|
security:
|
||||||
|
# 允许的来源 IP(可选,留空表示允许所有)
|
||||||
|
allowed_ips: []
|
||||||
|
# 请求频率限制(每分钟最大请求数)
|
||||||
|
rate_limit: 60
|
54
docker-compose.prod.yml
Normal file
54
docker-compose.prod.yml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
sms-forwarder:
|
||||||
|
build: .
|
||||||
|
container_name: sms-forwarder
|
||||||
|
ports:
|
||||||
|
- "12152:12152"
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
|
- sms_logs:/app/logs
|
||||||
|
- sms_data:/app/data
|
||||||
|
environment:
|
||||||
|
- CONFIG_PATH=/app/config.yaml
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:12152/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: '0.5'
|
||||||
|
reservations:
|
||||||
|
memory: 128M
|
||||||
|
cpus: '0.1'
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
# Nginx 反向代理(可选,用于 HTTPS)
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: sms-forwarder-nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./ssl:/etc/nginx/ssl:ro
|
||||||
|
depends_on:
|
||||||
|
- sms-forwarder
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- nginx
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sms_logs:
|
||||||
|
sms_data:
|
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
sms-forwarder:
|
||||||
|
build: .
|
||||||
|
container_name: sms-forwarder
|
||||||
|
ports:
|
||||||
|
- "12152:12152"
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./data:/app/data
|
||||||
|
environment:
|
||||||
|
- CONFIG_PATH=/app/config.yaml
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:12152/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: '0.5'
|
||||||
|
reservations:
|
||||||
|
memory: 128M
|
||||||
|
cpus: '0.1'
|
||||||
|
|
||||||
|
# 可选:添加 Gotify 服务作为推送后端
|
||||||
|
gotify:
|
||||||
|
image: gotify/server
|
||||||
|
container_name: gotify
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- gotify_data:/app/data
|
||||||
|
environment:
|
||||||
|
- GOTIFY_DEFAULTUSER_PASS=admin
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- gotify
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
gotify_data:
|
264
docs/deployment.md
Normal file
264
docs/deployment.md
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
# 部署指南
|
||||||
|
|
||||||
|
本指南介绍如何部署 SMS Forwarder 服务器。
|
||||||
|
|
||||||
|
## 部署方式
|
||||||
|
|
||||||
|
### 1. 本地部署
|
||||||
|
|
||||||
|
#### 环境要求
|
||||||
|
- Python 3.10+
|
||||||
|
- uv 包管理器
|
||||||
|
|
||||||
|
#### 安装步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 克隆项目
|
||||||
|
git clone <repository-url>
|
||||||
|
cd notification
|
||||||
|
|
||||||
|
# 2. 安装依赖
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# 3. 复制配置文件
|
||||||
|
cp config.example.yaml config.yaml
|
||||||
|
|
||||||
|
# 4. 编辑配置文件
|
||||||
|
nano config.yaml
|
||||||
|
|
||||||
|
# 5. 运行服务器
|
||||||
|
uv run sms-forwarder
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Docker 部署
|
||||||
|
|
||||||
|
#### 创建 Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装 uv
|
||||||
|
RUN pip install uv
|
||||||
|
|
||||||
|
# 复制项目文件
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
RUN uv sync
|
||||||
|
|
||||||
|
# 创建日志目录
|
||||||
|
RUN mkdir -p logs
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["uv", "run", "sms-forwarder"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 构建和运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建镜像
|
||||||
|
docker build -t sms-forwarder .
|
||||||
|
|
||||||
|
# 运行容器
|
||||||
|
docker run -d \
|
||||||
|
--name sms-forwarder \
|
||||||
|
-p 8000:8000 \
|
||||||
|
-v $(pwd)/config.yaml:/app/config.yaml \
|
||||||
|
-v $(pwd)/logs:/app/logs \
|
||||||
|
sms-forwarder
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. systemd 服务部署
|
||||||
|
|
||||||
|
#### 创建服务文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/systemd/system/sms-forwarder.service
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=SMS Forwarder Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=your-user
|
||||||
|
WorkingDirectory=/path/to/sms-forwarder
|
||||||
|
Environment=PATH=/path/to/sms-forwarder/.venv/bin
|
||||||
|
ExecStart=/path/to/sms-forwarder/.venv/bin/python -m sms_forwarder.main
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 启用服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable sms-forwarder
|
||||||
|
sudo systemctl start sms-forwarder
|
||||||
|
sudo systemctl status sms-forwarder
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置推送服务
|
||||||
|
|
||||||
|
### Pushbullet 配置
|
||||||
|
|
||||||
|
1. 访问 [Pushbullet](https://www.pushbullet.com/)
|
||||||
|
2. 注册账号并在 Android 设备上安装应用
|
||||||
|
3. 获取 Access Token:
|
||||||
|
- 访问 https://www.pushbullet.com/#settings/account
|
||||||
|
- 创建 Access Token
|
||||||
|
4. 在配置文件中添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
notifications:
|
||||||
|
services:
|
||||||
|
- name: "pushbullet"
|
||||||
|
url: "pbul://your-access-token-here"
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### FCM 配置
|
||||||
|
|
||||||
|
1. 创建 Firebase 项目
|
||||||
|
2. 获取服务器密钥和项目 ID
|
||||||
|
3. 在 Android 应用中集成 FCM
|
||||||
|
4. 获取设备 token
|
||||||
|
5. 配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
notifications:
|
||||||
|
services:
|
||||||
|
- name: "fcm"
|
||||||
|
url: "fcm://project-id@server-key/device-token"
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gotify 配置
|
||||||
|
|
||||||
|
1. 部署 Gotify 服务器
|
||||||
|
2. 创建应用并获取 token
|
||||||
|
3. 配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
notifications:
|
||||||
|
services:
|
||||||
|
- name: "gotify"
|
||||||
|
url: "gotify://your-server.com/app-token"
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全配置
|
||||||
|
|
||||||
|
### 1. 生成强 API 密钥
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 Python 生成随机密钥
|
||||||
|
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置防火墙
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 只允许特定 IP 访问
|
||||||
|
sudo ufw allow from YOUR_IPHONE_IP to any port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用 HTTPS
|
||||||
|
|
||||||
|
#### 使用 nginx 反向代理
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/cert.pem;
|
||||||
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 监控和维护
|
||||||
|
|
||||||
|
### 1. 日志监控
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看实时日志
|
||||||
|
tail -f logs/sms_forwarder.log
|
||||||
|
|
||||||
|
# 查看错误日志
|
||||||
|
grep ERROR logs/sms_forwarder.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 健康检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查服务状态
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# 检查系统状态
|
||||||
|
curl http://localhost:8000/status
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 性能监控
|
||||||
|
|
||||||
|
可以使用以下工具监控服务器性能:
|
||||||
|
- Prometheus + Grafana
|
||||||
|
- htop / top
|
||||||
|
- systemd journal
|
||||||
|
|
||||||
|
### 4. 备份配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 定期备份配置文件
|
||||||
|
cp config.yaml config.yaml.backup.$(date +%Y%m%d)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **服务启动失败**
|
||||||
|
- 检查配置文件语法
|
||||||
|
- 确认端口未被占用
|
||||||
|
- 查看详细错误日志
|
||||||
|
|
||||||
|
2. **通知发送失败**
|
||||||
|
- 验证推送服务配置
|
||||||
|
- 检查网络连接
|
||||||
|
- 确认 API 密钥有效
|
||||||
|
|
||||||
|
3. **高内存使用**
|
||||||
|
- 检查日志文件大小
|
||||||
|
- 考虑添加日志轮转
|
||||||
|
- 监控请求频率
|
||||||
|
|
||||||
|
### 调试命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查端口占用
|
||||||
|
netstat -tlnp | grep 8000
|
||||||
|
|
||||||
|
# 检查进程状态
|
||||||
|
ps aux | grep sms-forwarder
|
||||||
|
|
||||||
|
# 测试配置文件
|
||||||
|
uv run python -c "from sms_forwarder.config import get_config; print(get_config())"
|
||||||
|
```
|
149
docs/iphone-setup.md
Normal file
149
docs/iphone-setup.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# iPhone 快捷指令配置指南
|
||||||
|
|
||||||
|
本指南将帮助你在 iPhone 上设置快捷指令,自动将收到的短信转发到你的 Android 设备。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
1. iPhone 上已安装"快捷指令"应用
|
||||||
|
2. SMS Forwarder 服务器已部署并运行
|
||||||
|
3. 已获取服务器的 API 密钥
|
||||||
|
|
||||||
|
## 步骤 1: 创建快捷指令
|
||||||
|
|
||||||
|
1. 打开"快捷指令"应用
|
||||||
|
2. 点击右上角的"+"创建新快捷指令
|
||||||
|
3. 点击"添加操作"
|
||||||
|
|
||||||
|
## 步骤 2: 配置快捷指令动作
|
||||||
|
|
||||||
|
### 2.1 获取短信内容
|
||||||
|
|
||||||
|
1. 搜索并添加"获取我的快捷指令"动作
|
||||||
|
2. 搜索并添加"文本"动作,用于获取短信内容
|
||||||
|
3. 在文本框中输入短信内容(这将在自动化中被替换)
|
||||||
|
|
||||||
|
### 2.2 发送 HTTP 请求
|
||||||
|
|
||||||
|
1. 搜索并添加"获取 URL 内容"动作
|
||||||
|
2. 配置如下:
|
||||||
|
- **URL**: `http://your-server-ip:8000/notify`
|
||||||
|
- **方法**: POST
|
||||||
|
- **请求体**: JSON
|
||||||
|
- **标头**:
|
||||||
|
- `Content-Type`: `application/json`
|
||||||
|
|
||||||
|
### 2.3 配置请求体
|
||||||
|
|
||||||
|
在"获取 URL 内容"动作的请求体中,输入以下 JSON 格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "your-secret-api-key-here",
|
||||||
|
"message": {
|
||||||
|
"sender": "发送者号码或名称",
|
||||||
|
"content": "短信内容",
|
||||||
|
"timestamp": "2024-01-01T12:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**: 实际使用时,你需要:
|
||||||
|
- 将 `your-secret-api-key-here` 替换为你的实际 API 密钥
|
||||||
|
- 使用快捷指令的变量来动态获取发送者和内容
|
||||||
|
|
||||||
|
## 步骤 3: 设置自动化
|
||||||
|
|
||||||
|
1. 在快捷指令应用中,点击底部的"自动化"
|
||||||
|
2. 点击右上角的"+"创建新自动化
|
||||||
|
3. 选择"收到信息时"
|
||||||
|
4. 配置触发条件:
|
||||||
|
- 选择"任何人"或特定联系人
|
||||||
|
- 选择"立即运行"
|
||||||
|
5. 选择你刚创建的快捷指令
|
||||||
|
|
||||||
|
## 步骤 4: 高级配置
|
||||||
|
|
||||||
|
### 动态获取短信信息
|
||||||
|
|
||||||
|
为了动态获取短信的发送者和内容,你需要在快捷指令中:
|
||||||
|
|
||||||
|
1. 使用"获取输入内容"动作获取短信
|
||||||
|
2. 使用"获取短信详细信息"动作提取发送者和内容
|
||||||
|
3. 在 HTTP 请求中使用这些变量
|
||||||
|
|
||||||
|
### 示例快捷指令流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 获取输入内容 (短信)
|
||||||
|
2. 获取短信详细信息
|
||||||
|
- 发送者 → 变量: sender
|
||||||
|
- 内容 → 变量: content
|
||||||
|
3. 文本 (JSON 请求体)
|
||||||
|
{
|
||||||
|
"api_key": "your-api-key",
|
||||||
|
"message": {
|
||||||
|
"sender": "[sender变量]",
|
||||||
|
"content": "[content变量]",
|
||||||
|
"timestamp": "[当前日期]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
4. 获取 URL 内容
|
||||||
|
- URL: http://your-server:8000/notify
|
||||||
|
- 方法: POST
|
||||||
|
- 请求体: 上面的文本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 步骤 5: 测试
|
||||||
|
|
||||||
|
1. 保存快捷指令和自动化
|
||||||
|
2. 让朋友给你发送一条测试短信
|
||||||
|
3. 检查你的 Android 设备是否收到通知
|
||||||
|
4. 查看服务器日志确认请求是否成功
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **通知没有发送**
|
||||||
|
- 检查网络连接
|
||||||
|
- 确认服务器地址和端口正确
|
||||||
|
- 验证 API 密钥是否正确
|
||||||
|
|
||||||
|
2. **自动化没有触发**
|
||||||
|
- 确认自动化已启用
|
||||||
|
- 检查触发条件设置
|
||||||
|
- 重启快捷指令应用
|
||||||
|
|
||||||
|
3. **HTTP 请求失败**
|
||||||
|
- 检查 JSON 格式是否正确
|
||||||
|
- 确认 Content-Type 标头已设置
|
||||||
|
- 查看服务器日志获取详细错误信息
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
1. 在快捷指令中添加"显示通知"动作来调试
|
||||||
|
2. 使用"获取 URL 内容"的响应来检查服务器返回
|
||||||
|
3. 查看服务器的 `/health` 端点确认服务正常
|
||||||
|
|
||||||
|
## 安全建议
|
||||||
|
|
||||||
|
1. 使用强 API 密钥
|
||||||
|
2. 考虑使用 HTTPS(需要 SSL 证书)
|
||||||
|
3. 限制服务器的访问 IP
|
||||||
|
4. 定期更换 API 密钥
|
||||||
|
|
||||||
|
## 进阶功能
|
||||||
|
|
||||||
|
### 过滤特定短信
|
||||||
|
|
||||||
|
你可以在快捷指令中添加条件判断,只转发特定的短信:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 获取短信内容
|
||||||
|
2. 如果 (内容包含"验证码"或发送者是"银行")
|
||||||
|
3. 执行 HTTP 请求
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义通知格式
|
||||||
|
|
||||||
|
通过修改服务器配置文件中的模板,你可以自定义通知的显示格式。
|
96
docs/simple-iphone-setup.md
Normal file
96
docs/simple-iphone-setup.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# 简化版 iPhone 快捷指令配置指南
|
||||||
|
|
||||||
|
本指南将帮助你设置一个简单的 iPhone 快捷指令,只需要发送短信内容,无需其他复杂信息。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
1. iPhone 上已安装"快捷指令"应用
|
||||||
|
2. SMS Forwarder 服务器已部署并运行
|
||||||
|
3. 已在 Android 设备上安装 Pushbullet 应用
|
||||||
|
|
||||||
|
## 步骤 1: 创建快捷指令
|
||||||
|
|
||||||
|
1. 打开 iPhone 上的"快捷指令"应用
|
||||||
|
2. 点击右上角的"+"创建新快捷指令
|
||||||
|
3. 点击"添加操作"
|
||||||
|
|
||||||
|
## 步骤 2: 配置快捷指令动作
|
||||||
|
|
||||||
|
### 2.1 获取短信内容
|
||||||
|
|
||||||
|
1. 搜索并添加"获取我的快捷指令的输入"动作
|
||||||
|
2. 这将获取短信内容作为输入
|
||||||
|
|
||||||
|
### 2.2 发送 HTTP 请求
|
||||||
|
|
||||||
|
1. 搜索并添加"获取 URL 内容"动作
|
||||||
|
2. 配置如下:
|
||||||
|
- **URL**: `http://你的服务器IP:12152/notify/simple`
|
||||||
|
- **方法**: POST
|
||||||
|
- **请求体**: JSON
|
||||||
|
- **标头**:
|
||||||
|
- `Content-Type`: `application/json`
|
||||||
|
|
||||||
|
### 2.3 配置请求体
|
||||||
|
|
||||||
|
在"获取 URL 内容"动作的请求体中,输入以下 JSON 格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "ayNESyIW2pCQ5Ts-O8FC5t8mzhb26kbDZEr4I7PynHg",
|
||||||
|
"content": "快捷指令输入"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
- 将 `"快捷指令输入"` 替换为变量,方法是点击它,然后从变量列表中选择"快捷指令输入"
|
||||||
|
- API 密钥已经设置好,不需要修改
|
||||||
|
|
||||||
|
## 步骤 3: 设置自动化
|
||||||
|
|
||||||
|
1. 在快捷指令应用中,点击底部的"自动化"
|
||||||
|
2. 点击右上角的"+"创建新自动化
|
||||||
|
3. 选择"收到信息时"
|
||||||
|
4. 配置触发条件:
|
||||||
|
- 选择"任何人"或特定联系人
|
||||||
|
- 选择"立即运行"
|
||||||
|
5. 选择你刚创建的快捷指令
|
||||||
|
|
||||||
|
## 步骤 4: 测试
|
||||||
|
|
||||||
|
1. 保存快捷指令和自动化
|
||||||
|
2. 让朋友给你发送一条测试短信
|
||||||
|
3. 检查你的 Android 设备上的 Pushbullet 是否收到通知
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **通知没有发送**
|
||||||
|
- 确认服务器地址和端口正确 (12152)
|
||||||
|
- 确认 API 密钥正确
|
||||||
|
- 检查 Android 设备上的 Pushbullet 是否正常运行
|
||||||
|
|
||||||
|
2. **自动化没有触发**
|
||||||
|
- 确认自动化已启用
|
||||||
|
- 检查触发条件设置
|
||||||
|
- 重启快捷指令应用
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
1. 在快捷指令中添加"显示通知"动作来调试
|
||||||
|
2. 使用"获取 URL 内容"的响应来检查服务器返回
|
||||||
|
|
||||||
|
## 高级选项
|
||||||
|
|
||||||
|
如果你想添加发送者信息,可以修改 JSON 为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_key": "ayNESyIW2pCQ5Ts-O8FC5t8mzhb26kbDZEr4I7PynHg",
|
||||||
|
"content": "快捷指令输入",
|
||||||
|
"sender": "发送者名称"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `"发送者名称"` 可以是固定文本,如 "银行短信" 或 "验证码"。
|
44
pyproject.toml
Normal file
44
pyproject.toml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
[project]
|
||||||
|
name = "sms-forwarder"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "iOS SMS to Android notification forwarder"
|
||||||
|
authors = [
|
||||||
|
{name = "Your Name", email = "your.email@example.com"}
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.104.0",
|
||||||
|
"uvicorn[standard]>=0.24.0",
|
||||||
|
"apprise>=1.7.0",
|
||||||
|
"pydantic>=2.5.0",
|
||||||
|
"pydantic-settings>=2.1.0",
|
||||||
|
"pyyaml>=6.0.1",
|
||||||
|
"python-multipart>=0.0.6",
|
||||||
|
]
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
readme = "README.md"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.4.0",
|
||||||
|
"pytest-asyncio>=0.21.0",
|
||||||
|
"httpx>=0.25.0",
|
||||||
|
"black>=23.0.0",
|
||||||
|
"isort>=5.12.0",
|
||||||
|
"flake8>=6.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
target-version = ['py310']
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
line_length = 88
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
sms-forwarder = "sms_forwarder.main:main"
|
138
scripts/deploy.sh
Executable file
138
scripts/deploy.sh
Executable file
@ -0,0 +1,138 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# SMS Forwarder Docker 部署脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 SMS Forwarder Docker 部署脚本"
|
||||||
|
echo "================================"
|
||||||
|
|
||||||
|
# 检查 Docker 和 Docker Compose
|
||||||
|
check_docker() {
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "❌ Docker 未安装,请先安装 Docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||||
|
echo "❌ Docker Compose 未安装,请先安装 Docker Compose"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Docker 环境检查通过"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查配置文件
|
||||||
|
check_config() {
|
||||||
|
if [ ! -f "config.yaml" ]; then
|
||||||
|
echo "⚠️ 配置文件不存在,从示例文件创建..."
|
||||||
|
if [ -f "config.example.yaml" ]; then
|
||||||
|
cp config.example.yaml config.yaml
|
||||||
|
echo "📝 请编辑 config.yaml 文件配置你的推送服务"
|
||||||
|
echo "是否现在编辑配置文件? (y/N)"
|
||||||
|
read -r response
|
||||||
|
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||||
|
${EDITOR:-nano} config.yaml
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ 配置文件模板不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "✅ 配置文件存在"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建镜像
|
||||||
|
build_image() {
|
||||||
|
echo "🔨 构建 Docker 镜像..."
|
||||||
|
docker build -t sms-forwarder:latest .
|
||||||
|
echo "✅ 镜像构建完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 部署服务
|
||||||
|
deploy_service() {
|
||||||
|
echo "🚀 部署服务..."
|
||||||
|
|
||||||
|
# 停止现有服务
|
||||||
|
if docker ps -q --filter "name=sms-forwarder" | grep -q .; then
|
||||||
|
echo "⏹️ 停止现有服务..."
|
||||||
|
docker-compose down
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 启动新服务
|
||||||
|
echo "▶️ 启动服务..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 等待服务启动
|
||||||
|
echo "⏳ 等待服务启动..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# 检查服务状态
|
||||||
|
if docker ps --filter "name=sms-forwarder" --filter "status=running" | grep -q sms-forwarder; then
|
||||||
|
echo "✅ 服务启动成功"
|
||||||
|
else
|
||||||
|
echo "❌ 服务启动失败"
|
||||||
|
docker-compose logs sms-forwarder
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试服务
|
||||||
|
test_service() {
|
||||||
|
echo "🧪 测试服务..."
|
||||||
|
|
||||||
|
# 等待服务完全启动
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
if curl -f http://localhost:12152/health &> /dev/null; then
|
||||||
|
echo "✅ 健康检查通过"
|
||||||
|
else
|
||||||
|
echo "❌ 健康检查失败"
|
||||||
|
docker-compose logs sms-forwarder
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 状态检查
|
||||||
|
if curl -f http://localhost:12152/status &> /dev/null; then
|
||||||
|
echo "✅ 状态接口正常"
|
||||||
|
else
|
||||||
|
echo "❌ 状态接口异常"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示部署信息
|
||||||
|
show_info() {
|
||||||
|
echo ""
|
||||||
|
echo "🎉 部署完成!"
|
||||||
|
echo ""
|
||||||
|
echo "服务信息:"
|
||||||
|
echo " - 服务地址: http://localhost:12152"
|
||||||
|
echo " - API 文档: http://localhost:12152/docs"
|
||||||
|
echo " - 健康检查: http://localhost:12152/health"
|
||||||
|
echo " - 服务状态: http://localhost:12152/status"
|
||||||
|
echo ""
|
||||||
|
echo "管理命令:"
|
||||||
|
echo " - 查看日志: docker-compose logs -f sms-forwarder"
|
||||||
|
echo " - 重启服务: docker-compose restart sms-forwarder"
|
||||||
|
echo " - 停止服务: docker-compose down"
|
||||||
|
echo " - 查看状态: docker-compose ps"
|
||||||
|
echo ""
|
||||||
|
echo "测试命令:"
|
||||||
|
echo " - 发送测试通知: ./scripts/test-docker.sh"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
check_docker
|
||||||
|
check_config
|
||||||
|
build_image
|
||||||
|
deploy_service
|
||||||
|
test_service
|
||||||
|
show_info
|
||||||
|
}
|
||||||
|
|
||||||
|
# 运行主函数
|
||||||
|
main "$@"
|
86
scripts/docker-manage.sh
Executable file
86
scripts/docker-manage.sh
Executable file
@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# SMS Forwarder Docker 管理脚本
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo "SMS Forwarder Docker 管理脚本"
|
||||||
|
echo ""
|
||||||
|
echo "用法: $0 [命令]"
|
||||||
|
echo ""
|
||||||
|
echo "命令:"
|
||||||
|
echo " start 启动服务"
|
||||||
|
echo " stop 停止服务"
|
||||||
|
echo " restart 重启服务"
|
||||||
|
echo " status 查看服务状态"
|
||||||
|
echo " logs 查看实时日志"
|
||||||
|
echo " build 重新构建镜像"
|
||||||
|
echo " test 发送测试通知"
|
||||||
|
echo " stats 查看资源使用"
|
||||||
|
echo " shell 进入容器 shell"
|
||||||
|
echo " clean 清理未使用的镜像和容器"
|
||||||
|
echo " deploy 完整部署流程"
|
||||||
|
echo " help 显示此帮助信息"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
echo "🚀 启动 SMS Forwarder 服务..."
|
||||||
|
docker-compose up -d
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
echo "⏹️ 停止 SMS Forwarder 服务..."
|
||||||
|
docker-compose down
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
echo "🔄 重启 SMS Forwarder 服务..."
|
||||||
|
docker-compose restart sms-forwarder
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
echo "📊 SMS Forwarder 服务状态:"
|
||||||
|
docker-compose ps
|
||||||
|
echo ""
|
||||||
|
echo "容器详细信息:"
|
||||||
|
docker inspect sms-forwarder --format='{{.State.Status}}: {{.State.StartedAt}}'
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
echo "📋 SMS Forwarder 实时日志 (Ctrl+C 退出):"
|
||||||
|
docker-compose logs -f sms-forwarder
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
echo "🔨 重新构建镜像..."
|
||||||
|
docker-compose build --no-cache sms-forwarder
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
echo "🧪 发送测试通知..."
|
||||||
|
./scripts/test-docker.sh
|
||||||
|
;;
|
||||||
|
stats)
|
||||||
|
echo "💾 资源使用统计:"
|
||||||
|
docker stats sms-forwarder --no-stream
|
||||||
|
;;
|
||||||
|
shell)
|
||||||
|
echo "🐚 进入容器 shell..."
|
||||||
|
docker-compose exec sms-forwarder /bin/bash
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
echo "🧹 清理未使用的 Docker 资源..."
|
||||||
|
docker system prune -f
|
||||||
|
docker image prune -f
|
||||||
|
;;
|
||||||
|
deploy)
|
||||||
|
echo "🚀 执行完整部署流程..."
|
||||||
|
./scripts/deploy.sh
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ 未知命令: $1"
|
||||||
|
echo ""
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
128
scripts/install.sh
Executable file
128
scripts/install.sh
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# SMS Forwarder 安装脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 开始安装 SMS Forwarder..."
|
||||||
|
|
||||||
|
# 检查 Python 版本
|
||||||
|
check_python() {
|
||||||
|
if command -v python3 &> /dev/null; then
|
||||||
|
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
|
||||||
|
if [[ $(echo "$PYTHON_VERSION >= 3.10" | bc -l) -eq 1 ]]; then
|
||||||
|
echo "✅ Python $PYTHON_VERSION 已安装"
|
||||||
|
else
|
||||||
|
echo "❌ 需要 Python 3.10 或更高版本,当前版本: $PYTHON_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ 未找到 Python 3,请先安装 Python 3.10+"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 安装 uv
|
||||||
|
install_uv() {
|
||||||
|
if command -v uv &> /dev/null; then
|
||||||
|
echo "✅ uv 已安装"
|
||||||
|
else
|
||||||
|
echo "📦 安装 uv..."
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
source $HOME/.cargo/env
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
install_dependencies() {
|
||||||
|
echo "📦 安装项目依赖..."
|
||||||
|
uv sync
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建配置文件
|
||||||
|
setup_config() {
|
||||||
|
if [ ! -f "config.yaml" ]; then
|
||||||
|
echo "⚙️ 创建配置文件..."
|
||||||
|
cp config.example.yaml config.yaml
|
||||||
|
echo "📝 请编辑 config.yaml 文件配置你的推送服务"
|
||||||
|
else
|
||||||
|
echo "✅ 配置文件已存在"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建必要目录
|
||||||
|
create_directories() {
|
||||||
|
echo "📁 创建必要目录..."
|
||||||
|
mkdir -p logs
|
||||||
|
mkdir -p data
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生成 API 密钥
|
||||||
|
generate_api_key() {
|
||||||
|
if command -v python3 &> /dev/null; then
|
||||||
|
API_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
|
||||||
|
echo "🔑 生成的 API 密钥: $API_KEY"
|
||||||
|
echo "请将此密钥添加到 config.yaml 文件中的 server.api_key 字段"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建 systemd 服务文件
|
||||||
|
create_systemd_service() {
|
||||||
|
read -p "是否创建 systemd 服务? (y/N): " create_service
|
||||||
|
if [[ $create_service =~ ^[Yy]$ ]]; then
|
||||||
|
CURRENT_DIR=$(pwd)
|
||||||
|
USER=$(whoami)
|
||||||
|
|
||||||
|
cat > sms-forwarder.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=SMS Forwarder Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$USER
|
||||||
|
WorkingDirectory=$CURRENT_DIR
|
||||||
|
Environment=PATH=$CURRENT_DIR/.venv/bin
|
||||||
|
ExecStart=$CURRENT_DIR/.venv/bin/python -m sms_forwarder.main
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "📄 systemd 服务文件已创建: sms-forwarder.service"
|
||||||
|
echo "运行以下命令安装服务:"
|
||||||
|
echo " sudo cp sms-forwarder.service /etc/systemd/system/"
|
||||||
|
echo " sudo systemctl daemon-reload"
|
||||||
|
echo " sudo systemctl enable sms-forwarder"
|
||||||
|
echo " sudo systemctl start sms-forwarder"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主安装流程
|
||||||
|
main() {
|
||||||
|
echo "SMS Forwarder 安装程序"
|
||||||
|
echo "======================"
|
||||||
|
|
||||||
|
check_python
|
||||||
|
install_uv
|
||||||
|
install_dependencies
|
||||||
|
create_directories
|
||||||
|
setup_config
|
||||||
|
generate_api_key
|
||||||
|
create_systemd_service
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 安装完成!"
|
||||||
|
echo ""
|
||||||
|
echo "下一步:"
|
||||||
|
echo "1. 编辑 config.yaml 文件配置推送服务"
|
||||||
|
echo "2. 运行 'uv run sms-forwarder' 启动服务器"
|
||||||
|
echo "3. 访问 http://localhost:8000/docs 查看 API 文档"
|
||||||
|
echo "4. 参考 docs/iphone-setup.md 配置 iPhone 快捷指令"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 运行主函数
|
||||||
|
main "$@"
|
96
scripts/manage.sh
Executable file
96
scripts/manage.sh
Executable file
@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# SMS Forwarder 管理脚本
|
||||||
|
|
||||||
|
SERVICE_NAME="sms-forwarder"
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo "SMS Forwarder 管理脚本"
|
||||||
|
echo ""
|
||||||
|
echo "用法: $0 [命令]"
|
||||||
|
echo ""
|
||||||
|
echo "命令:"
|
||||||
|
echo " start 启动服务"
|
||||||
|
echo " stop 停止服务"
|
||||||
|
echo " restart 重启服务"
|
||||||
|
echo " status 查看服务状态"
|
||||||
|
echo " logs 查看实时日志"
|
||||||
|
echo " enable 启用开机自启"
|
||||||
|
echo " disable 禁用开机自启"
|
||||||
|
echo " test 发送测试通知"
|
||||||
|
echo " update 更新依赖"
|
||||||
|
echo " config 编辑配置文件"
|
||||||
|
echo " install 安装 systemd 服务"
|
||||||
|
echo " help 显示此帮助信息"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
echo "🚀 启动 SMS Forwarder 服务..."
|
||||||
|
sudo systemctl start $SERVICE_NAME
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
echo "⏹️ 停止 SMS Forwarder 服务..."
|
||||||
|
sudo systemctl stop $SERVICE_NAME
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
echo "🔄 重启 SMS Forwarder 服务..."
|
||||||
|
sudo systemctl restart $SERVICE_NAME
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
echo "📊 SMS Forwarder 服务状态:"
|
||||||
|
sudo systemctl status $SERVICE_NAME --no-pager
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
echo "📋 SMS Forwarder 实时日志 (Ctrl+C 退出):"
|
||||||
|
sudo journalctl -u $SERVICE_NAME -f
|
||||||
|
;;
|
||||||
|
enable)
|
||||||
|
echo "✅ 启用 SMS Forwarder 开机自启..."
|
||||||
|
sudo systemctl enable $SERVICE_NAME
|
||||||
|
;;
|
||||||
|
disable)
|
||||||
|
echo "❌ 禁用 SMS Forwarder 开机自启..."
|
||||||
|
sudo systemctl disable $SERVICE_NAME
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
echo "🧪 发送测试通知..."
|
||||||
|
if [ -f "config.yaml" ]; then
|
||||||
|
API_KEY=$(grep "api_key:" config.yaml | awk '{print $2}' | tr -d '"')
|
||||||
|
uv run python scripts/test_notification.py http://localhost:12152 "$API_KEY"
|
||||||
|
else
|
||||||
|
echo "❌ 配置文件不存在"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
update)
|
||||||
|
echo "📦 更新依赖..."
|
||||||
|
uv sync
|
||||||
|
echo "🔄 重启服务以应用更新..."
|
||||||
|
sudo systemctl restart $SERVICE_NAME
|
||||||
|
;;
|
||||||
|
config)
|
||||||
|
echo "⚙️ 编辑配置文件..."
|
||||||
|
${EDITOR:-nano} config.yaml
|
||||||
|
echo "是否重启服务以应用配置? (y/N)"
|
||||||
|
read -r response
|
||||||
|
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||||
|
sudo systemctl restart $SERVICE_NAME
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
echo "🔧 安装 systemd 服务..."
|
||||||
|
./scripts/setup-systemd.sh
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ 未知命令: $1"
|
||||||
|
echo ""
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
71
scripts/setup-systemd.sh
Executable file
71
scripts/setup-systemd.sh
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# SMS Forwarder systemd 服务安装脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 配置 SMS Forwarder systemd 服务..."
|
||||||
|
|
||||||
|
# 获取当前用户和路径
|
||||||
|
CURRENT_USER=$(whoami)
|
||||||
|
CURRENT_DIR=$(pwd)
|
||||||
|
SERVICE_FILE="sms-forwarder.service"
|
||||||
|
|
||||||
|
# 检查是否在正确的目录
|
||||||
|
if [ ! -f "pyproject.toml" ] || [ ! -f "config.yaml" ]; then
|
||||||
|
echo "❌ 请在项目根目录运行此脚本"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查 uv 虚拟环境
|
||||||
|
if [ ! -d ".venv" ]; then
|
||||||
|
echo "📦 创建虚拟环境..."
|
||||||
|
uv sync
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 获取虚拟环境路径
|
||||||
|
VENV_PATH="$CURRENT_DIR/.venv"
|
||||||
|
if [ ! -f "$VENV_PATH/bin/python" ]; then
|
||||||
|
echo "❌ 虚拟环境未找到,请先运行 'uv sync'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建日志和数据目录
|
||||||
|
mkdir -p logs data
|
||||||
|
|
||||||
|
# 更新服务文件中的路径
|
||||||
|
echo "📝 更新服务文件配置..."
|
||||||
|
sed -i "s|your-username|$CURRENT_USER|g" $SERVICE_FILE
|
||||||
|
sed -i "s|/path/to/notification|$CURRENT_DIR|g" $SERVICE_FILE
|
||||||
|
|
||||||
|
# 复制服务文件到系统目录
|
||||||
|
echo "📋 安装 systemd 服务文件..."
|
||||||
|
sudo cp $SERVICE_FILE /etc/systemd/system/
|
||||||
|
|
||||||
|
# 重新加载 systemd
|
||||||
|
echo "🔄 重新加载 systemd..."
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# 启用服务
|
||||||
|
echo "✅ 启用 SMS Forwarder 服务..."
|
||||||
|
sudo systemctl enable sms-forwarder
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
echo "🚀 启动 SMS Forwarder 服务..."
|
||||||
|
sudo systemctl start sms-forwarder
|
||||||
|
|
||||||
|
# 检查状态
|
||||||
|
echo "📊 检查服务状态..."
|
||||||
|
sudo systemctl status sms-forwarder --no-pager
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 SMS Forwarder 服务安装完成!"
|
||||||
|
echo ""
|
||||||
|
echo "常用命令:"
|
||||||
|
echo " 查看状态: sudo systemctl status sms-forwarder"
|
||||||
|
echo " 查看日志: sudo journalctl -u sms-forwarder -f"
|
||||||
|
echo " 重启服务: sudo systemctl restart sms-forwarder"
|
||||||
|
echo " 停止服务: sudo systemctl stop sms-forwarder"
|
||||||
|
echo " 禁用服务: sudo systemctl disable sms-forwarder"
|
||||||
|
echo ""
|
||||||
|
echo "服务将在系统重启后自动启动。"
|
114
scripts/test-docker.sh
Executable file
114
scripts/test-docker.sh
Executable file
@ -0,0 +1,114 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Docker 环境测试脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🧪 SMS Forwarder Docker 测试"
|
||||||
|
echo "============================"
|
||||||
|
|
||||||
|
# 检查服务是否运行
|
||||||
|
check_service() {
|
||||||
|
if ! docker ps --filter "name=sms-forwarder" --filter "status=running" | grep -q sms-forwarder; then
|
||||||
|
echo "❌ SMS Forwarder 服务未运行"
|
||||||
|
echo "请先运行: docker-compose up -d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ 服务正在运行"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取 API 密钥
|
||||||
|
get_api_key() {
|
||||||
|
if [ -f "config.yaml" ]; then
|
||||||
|
API_KEY=$(grep "api_key:" config.yaml | awk '{print $2}' | tr -d '"')
|
||||||
|
if [ -z "$API_KEY" ]; then
|
||||||
|
echo "❌ 无法从配置文件获取 API 密钥"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ 获取到 API 密钥"
|
||||||
|
else
|
||||||
|
echo "❌ 配置文件不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试健康检查
|
||||||
|
test_health() {
|
||||||
|
echo "🔍 测试健康检查..."
|
||||||
|
if curl -f http://localhost:12152/health &> /dev/null; then
|
||||||
|
echo "✅ 健康检查通过"
|
||||||
|
else
|
||||||
|
echo "❌ 健康检查失败"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试状态接口
|
||||||
|
test_status() {
|
||||||
|
echo "📊 测试状态接口..."
|
||||||
|
STATUS=$(curl -s http://localhost:12152/status)
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ 状态接口正常"
|
||||||
|
echo "服务状态: $STATUS"
|
||||||
|
else
|
||||||
|
echo "❌ 状态接口异常"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试通知发送
|
||||||
|
test_notification() {
|
||||||
|
echo "📱 测试通知发送..."
|
||||||
|
|
||||||
|
RESPONSE=$(curl -s -X POST "http://localhost:12152/notify/simple" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"api_key\": \"$API_KEY\",
|
||||||
|
\"content\": \"Docker 测试通知 - $(date)\",
|
||||||
|
\"sender\": \"Docker 测试\"
|
||||||
|
}")
|
||||||
|
|
||||||
|
if echo "$RESPONSE" | grep -q '"success":true'; then
|
||||||
|
echo "✅ 通知发送成功"
|
||||||
|
echo "响应: $RESPONSE"
|
||||||
|
else
|
||||||
|
echo "❌ 通知发送失败"
|
||||||
|
echo "响应: $RESPONSE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示容器信息
|
||||||
|
show_container_info() {
|
||||||
|
echo ""
|
||||||
|
echo "📋 容器信息:"
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "💾 资源使用:"
|
||||||
|
docker stats sms-forwarder --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
check_service
|
||||||
|
get_api_key
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "开始测试..."
|
||||||
|
|
||||||
|
test_health
|
||||||
|
test_status
|
||||||
|
test_notification
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 所有测试通过!"
|
||||||
|
|
||||||
|
show_container_info
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "如果你的 Android 设备收到了通知,说明 Docker 部署成功!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 运行主函数
|
||||||
|
main "$@"
|
125
scripts/test_notification.py
Executable file
125
scripts/test_notification.py
Executable file
@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""测试通知发送脚本."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def test_notification(server_url: str, api_key: str):
|
||||||
|
"""测试发送通知."""
|
||||||
|
|
||||||
|
# 测试简化版 API
|
||||||
|
simple_test_data = {
|
||||||
|
"api_key": api_key,
|
||||||
|
"content": "这是一条测试短信,用于验证通知转发功能是否正常工作。",
|
||||||
|
"sender": "测试发送者"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"🔄 正在向 {server_url} 发送测试通知...")
|
||||||
|
|
||||||
|
# 发送请求到简化版 API
|
||||||
|
response = requests.post(
|
||||||
|
f"{server_url}/notify/simple",
|
||||||
|
json=simple_test_data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查响应
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
if result.get("success"):
|
||||||
|
print("✅ 测试通知发送成功!")
|
||||||
|
print(f"📱 请检查你的 Android 设备是否收到通知")
|
||||||
|
else:
|
||||||
|
print(f"❌ 通知发送失败: {result.get('message')}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"❌ HTTP 请求失败: {response.status_code}")
|
||||||
|
print(f"响应内容: {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print(f"❌ 无法连接到服务器 {server_url}")
|
||||||
|
print("请确认服务器正在运行且地址正确")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print("❌ 请求超时")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 发生错误: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_server_status(server_url: str):
|
||||||
|
"""测试服务器状态."""
|
||||||
|
try:
|
||||||
|
print(f"🔄 检查服务器状态...")
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
health_response = requests.get(f"{server_url}/health", timeout=5)
|
||||||
|
if health_response.status_code == 200:
|
||||||
|
print("✅ 服务器健康检查通过")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 健康检查失败: {health_response.status_code}")
|
||||||
|
|
||||||
|
# 状态信息
|
||||||
|
status_response = requests.get(f"{server_url}/status", timeout=5)
|
||||||
|
if status_response.status_code == 200:
|
||||||
|
status = status_response.json()
|
||||||
|
print(f"📊 服务器状态:")
|
||||||
|
print(f" 版本: {status.get('version')}")
|
||||||
|
print(f" 运行时间: {status.get('uptime', 0):.2f} 秒")
|
||||||
|
print(f" 已发送通知: {status.get('notifications_sent', 0)}")
|
||||||
|
print(f" 失败通知: {status.get('notifications_failed', 0)}")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 获取状态信息失败: {status_response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 检查服务器状态时出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数."""
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("使用方法: python test_notification.py <server_url> <api_key>")
|
||||||
|
print("示例: python test_notification.py http://localhost:8000 your-api-key")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
server_url = sys.argv[1].rstrip('/')
|
||||||
|
api_key = sys.argv[2]
|
||||||
|
|
||||||
|
print("SMS Forwarder 通知测试")
|
||||||
|
print("=" * 30)
|
||||||
|
|
||||||
|
# 测试服务器状态
|
||||||
|
if not test_server_status(server_url):
|
||||||
|
print("❌ 服务器状态检查失败")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 测试通知发送
|
||||||
|
if test_notification(server_url, api_key):
|
||||||
|
print("\n🎉 所有测试通过!")
|
||||||
|
print("如果你的 Android 设备收到了通知,说明配置正确。")
|
||||||
|
else:
|
||||||
|
print("\n❌ 测试失败")
|
||||||
|
print("请检查:")
|
||||||
|
print("1. 服务器配置是否正确")
|
||||||
|
print("2. API 密钥是否正确")
|
||||||
|
print("3. 推送服务是否正确配置")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
33
sms-forwarder.service
Normal file
33
sms-forwarder.service
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=SMS Forwarder Service - iOS to Android notification forwarder
|
||||||
|
After=network.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=zack
|
||||||
|
Group=zack
|
||||||
|
WorkingDirectory=/mnt/dsk1/code/project/notification
|
||||||
|
Environment=PATH=/mnt/dsk1/code/project/notification/.venv/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
Environment=CONFIG_PATH=/mnt/dsk1/code/project/notification/config.yaml
|
||||||
|
ExecStart=/mnt/dsk1/code/project/notification/.venv/bin/python -m sms_forwarder.main
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=sms-forwarder
|
||||||
|
|
||||||
|
# 安全设置
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/mnt/dsk1/code/project/notification/logs /mnt/dsk1/code/project/notification/data
|
||||||
|
|
||||||
|
# 资源限制
|
||||||
|
MemoryMax=256M
|
||||||
|
CPUQuota=50%
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
3
sms_forwarder/__init__.py
Normal file
3
sms_forwarder/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""SMS Forwarder - iOS SMS to Android notification forwarder."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
87
sms_forwarder/auth.py
Normal file
87
sms_forwarder/auth.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"""认证和安全模块."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from fastapi import HTTPException, Request, status
|
||||||
|
|
||||||
|
from .config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimiter:
|
||||||
|
"""简单的速率限制器."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.requests: Dict[str, List[float]] = defaultdict(list)
|
||||||
|
|
||||||
|
def is_allowed(self, client_ip: str, limit: int, window: int = 60) -> bool:
|
||||||
|
"""检查是否允许请求."""
|
||||||
|
now = time.time()
|
||||||
|
# 清理过期的请求记录
|
||||||
|
self.requests[client_ip] = [
|
||||||
|
req_time for req_time in self.requests[client_ip]
|
||||||
|
if now - req_time < window
|
||||||
|
]
|
||||||
|
|
||||||
|
# 检查是否超过限制
|
||||||
|
if len(self.requests[client_ip]) >= limit:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录当前请求
|
||||||
|
self.requests[client_ip].append(now)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# 全局速率限制器实例
|
||||||
|
rate_limiter = RateLimiter()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_api_key(api_key: str) -> bool:
|
||||||
|
"""验证 API 密钥."""
|
||||||
|
config = get_config()
|
||||||
|
return api_key == config.server.api_key
|
||||||
|
|
||||||
|
|
||||||
|
def check_ip_allowed(client_ip: str) -> bool:
|
||||||
|
"""检查 IP 是否被允许."""
|
||||||
|
config = get_config()
|
||||||
|
allowed_ips = config.security.allowed_ips
|
||||||
|
|
||||||
|
# 如果没有配置允许的 IP,则允许所有
|
||||||
|
if not allowed_ips:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return client_ip in allowed_ips
|
||||||
|
|
||||||
|
|
||||||
|
def check_rate_limit(client_ip: str) -> bool:
|
||||||
|
"""检查速率限制."""
|
||||||
|
config = get_config()
|
||||||
|
return rate_limiter.is_allowed(client_ip, config.security.rate_limit)
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_request(request: Request, api_key: str) -> None:
|
||||||
|
"""验证请求."""
|
||||||
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
|
||||||
|
# 检查 IP 白名单
|
||||||
|
if not check_ip_allowed(client_ip):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="IP 地址不在允许列表中"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查速率限制
|
||||||
|
if not check_rate_limit(client_ip):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||||
|
detail="请求过于频繁,请稍后再试"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证 API 密钥
|
||||||
|
if not verify_api_key(api_key):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="无效的 API 密钥"
|
||||||
|
)
|
93
sms_forwarder/config.py
Normal file
93
sms_forwarder/config.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
"""配置管理模块."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class ServerConfig(BaseModel):
|
||||||
|
"""服务器配置."""
|
||||||
|
host: str = "0.0.0.0"
|
||||||
|
port: int = 8000
|
||||||
|
api_key: str = Field(..., description="API 密钥")
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingConfig(BaseModel):
|
||||||
|
"""日志配置."""
|
||||||
|
level: str = "INFO"
|
||||||
|
file: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationService(BaseModel):
|
||||||
|
"""通知服务配置."""
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTemplate(BaseModel):
|
||||||
|
"""通知模板."""
|
||||||
|
title: str
|
||||||
|
body: str
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTemplates(BaseModel):
|
||||||
|
"""通知模板集合."""
|
||||||
|
sms: NotificationTemplate
|
||||||
|
system: NotificationTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationConfig(BaseModel):
|
||||||
|
"""通知配置."""
|
||||||
|
services: List[NotificationService]
|
||||||
|
templates: NotificationTemplates
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityConfig(BaseModel):
|
||||||
|
"""安全配置."""
|
||||||
|
allowed_ips: List[str] = []
|
||||||
|
rate_limit: int = 60
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseSettings):
|
||||||
|
"""应用配置."""
|
||||||
|
server: ServerConfig
|
||||||
|
logging: LoggingConfig
|
||||||
|
notifications: NotificationConfig
|
||||||
|
security: SecurityConfig
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_from_file(cls, config_path: str = "config.yaml") -> "Config":
|
||||||
|
"""从 YAML 文件加载配置."""
|
||||||
|
config_file = Path(config_path)
|
||||||
|
if not config_file.exists():
|
||||||
|
raise FileNotFoundError(f"配置文件不存在: {config_path}")
|
||||||
|
|
||||||
|
with open(config_file, "r", encoding="utf-8") as f:
|
||||||
|
config_data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
return cls(**config_data)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局配置实例
|
||||||
|
_config: Optional[Config] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_config() -> Config:
|
||||||
|
"""获取全局配置实例."""
|
||||||
|
global _config
|
||||||
|
if _config is None:
|
||||||
|
config_path = os.getenv("CONFIG_PATH", "config.yaml")
|
||||||
|
_config = Config.load_from_file(config_path)
|
||||||
|
return _config
|
||||||
|
|
||||||
|
|
||||||
|
def reload_config(config_path: str = "config.yaml") -> Config:
|
||||||
|
"""重新加载配置."""
|
||||||
|
global _config
|
||||||
|
_config = Config.load_from_file(config_path)
|
||||||
|
return _config
|
223
sms_forwarder/main.py
Normal file
223
sms_forwarder/main.py
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
"""主应用模块."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI, HTTPException, Request, status
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
from .auth import authenticate_request
|
||||||
|
from .config import get_config
|
||||||
|
from .models import NotificationRequest, NotificationResponse, SimpleNotificationRequest, SMSMessage, SystemStatus
|
||||||
|
from .notifier import notification_manager
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
def setup_logging():
|
||||||
|
"""设置日志配置."""
|
||||||
|
try:
|
||||||
|
config = get_config()
|
||||||
|
log_level = getattr(logging, config.logging.level.upper(), logging.INFO)
|
||||||
|
|
||||||
|
# 创建日志目录
|
||||||
|
if config.logging.file:
|
||||||
|
log_file = Path(config.logging.file)
|
||||||
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 配置日志格式
|
||||||
|
logging.basicConfig(
|
||||||
|
level=log_level,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(),
|
||||||
|
logging.FileHandler(config.logging.file) if config.logging.file else logging.NullHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# 如果配置加载失败,使用默认日志配置
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logging.error(f"日志配置失败,使用默认配置: {e}")
|
||||||
|
|
||||||
|
setup_logging()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 应用启动时间
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 创建 FastAPI 应用
|
||||||
|
app = FastAPI(
|
||||||
|
title="SMS Forwarder",
|
||||||
|
description="iOS SMS to Android notification forwarder",
|
||||||
|
version=__version__,
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加 CORS 中间件
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(Exception)
|
||||||
|
async def global_exception_handler(request: Request, exc: Exception):
|
||||||
|
"""全局异常处理器."""
|
||||||
|
logger.error(f"未处理的异常: {exc}", exc_info=True)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
content={"detail": "服务器内部错误"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""根路径."""
|
||||||
|
return {
|
||||||
|
"message": "SMS Forwarder API",
|
||||||
|
"version": __version__,
|
||||||
|
"docs": "/docs"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""健康检查."""
|
||||||
|
return {"status": "healthy", "timestamp": datetime.now()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/status", response_model=SystemStatus)
|
||||||
|
async def get_status():
|
||||||
|
"""获取系统状态."""
|
||||||
|
stats = notification_manager.get_stats()
|
||||||
|
return SystemStatus(
|
||||||
|
version=__version__,
|
||||||
|
uptime=time.time() - start_time,
|
||||||
|
notifications_sent=stats["sent"],
|
||||||
|
notifications_failed=stats["failed"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/notify", response_model=NotificationResponse)
|
||||||
|
async def send_notification(request: Request, notification_request: NotificationRequest):
|
||||||
|
"""发送通知(完整版)."""
|
||||||
|
try:
|
||||||
|
# 验证请求
|
||||||
|
authenticate_request(request, notification_request.api_key)
|
||||||
|
|
||||||
|
# 发送通知
|
||||||
|
success = notification_manager.send_sms_notification(notification_request.message)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"通知发送成功: {notification_request.message.sender}")
|
||||||
|
return NotificationResponse(
|
||||||
|
success=True,
|
||||||
|
message="通知发送成功"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(f"通知发送失败: {notification_request.message.sender}")
|
||||||
|
return NotificationResponse(
|
||||||
|
success=False,
|
||||||
|
message="通知发送失败"
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理通知请求时出错: {e}", exc_info=True)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="处理请求时出错"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/notify/simple", response_model=NotificationResponse)
|
||||||
|
async def send_simple_notification(request: Request, simple_request: SimpleNotificationRequest):
|
||||||
|
"""发送通知(简化版)- 只需要内容,时间戳自动生成."""
|
||||||
|
try:
|
||||||
|
# 验证请求
|
||||||
|
authenticate_request(request, simple_request.api_key)
|
||||||
|
|
||||||
|
# 创建 SMS 消息对象,时间戳自动生成
|
||||||
|
sms_message = SMSMessage(
|
||||||
|
content=simple_request.content,
|
||||||
|
sender=simple_request.sender or "iPhone"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送通知
|
||||||
|
success = notification_manager.send_sms_notification(sms_message)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"简化通知发送成功: {sms_message.sender}")
|
||||||
|
return NotificationResponse(
|
||||||
|
success=True,
|
||||||
|
message="通知发送成功"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(f"简化通知发送失败: {sms_message.sender}")
|
||||||
|
return NotificationResponse(
|
||||||
|
success=False,
|
||||||
|
message="通知发送失败"
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理简化通知请求时出错: {e}", exc_info=True)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="处理请求时出错"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/reload")
|
||||||
|
async def reload_config(request: Request, api_key: str):
|
||||||
|
"""重新加载配置."""
|
||||||
|
try:
|
||||||
|
# 验证 API 密钥
|
||||||
|
authenticate_request(request, api_key)
|
||||||
|
|
||||||
|
# 重新加载通知服务
|
||||||
|
notification_manager.reload_services()
|
||||||
|
|
||||||
|
return {"message": "配置重新加载成功"}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"重新加载配置时出错: {e}", exc_info=True)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="重新加载配置失败"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数."""
|
||||||
|
try:
|
||||||
|
config = get_config()
|
||||||
|
logger.info(f"启动 SMS Forwarder v{__version__}")
|
||||||
|
logger.info(f"服务器配置: {config.server.host}:{config.server.port}")
|
||||||
|
|
||||||
|
# 启动服务器
|
||||||
|
uvicorn.run(
|
||||||
|
"sms_forwarder.main:app",
|
||||||
|
host=config.server.host,
|
||||||
|
port=config.server.port,
|
||||||
|
log_level=config.logging.level.lower(),
|
||||||
|
access_log=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动服务器失败: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
41
sms_forwarder/models.py
Normal file
41
sms_forwarder/models.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""数据模型."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class SMSMessage(BaseModel):
|
||||||
|
"""短信消息模型."""
|
||||||
|
sender: Optional[str] = Field(default="未知发送者", description="发送者手机号或名称")
|
||||||
|
content: str = Field(..., description="短信内容")
|
||||||
|
timestamp: Optional[datetime] = Field(default_factory=datetime.now, description="接收时间")
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationRequest(BaseModel):
|
||||||
|
"""通知请求模型."""
|
||||||
|
api_key: str = Field(..., description="API 密钥")
|
||||||
|
message: SMSMessage = Field(..., description="短信消息")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleNotificationRequest(BaseModel):
|
||||||
|
"""简化的通知请求模型 - 只需要内容."""
|
||||||
|
api_key: str = Field(..., description="API 密钥")
|
||||||
|
content: str = Field(..., description="短信内容")
|
||||||
|
sender: Optional[str] = Field(default=None, description="发送者(可选)")
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationResponse(BaseModel):
|
||||||
|
"""通知响应模型."""
|
||||||
|
success: bool = Field(..., description="是否成功")
|
||||||
|
message: str = Field(..., description="响应消息")
|
||||||
|
timestamp: datetime = Field(default_factory=datetime.now, description="响应时间")
|
||||||
|
|
||||||
|
|
||||||
|
class SystemStatus(BaseModel):
|
||||||
|
"""系统状态模型."""
|
||||||
|
version: str
|
||||||
|
uptime: float
|
||||||
|
notifications_sent: int
|
||||||
|
notifications_failed: int
|
127
sms_forwarder/notifier.py
Normal file
127
sms_forwarder/notifier.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""通知发送模块."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import apprise
|
||||||
|
|
||||||
|
from .config import get_config
|
||||||
|
from .models import SMSMessage
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationManager:
|
||||||
|
"""通知管理器."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.apprise_obj = apprise.Apprise()
|
||||||
|
self.stats = {
|
||||||
|
"sent": 0,
|
||||||
|
"failed": 0
|
||||||
|
}
|
||||||
|
self._load_services()
|
||||||
|
|
||||||
|
def _load_services(self) -> None:
|
||||||
|
"""加载通知服务."""
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
# 清空现有服务
|
||||||
|
self.apprise_obj.clear()
|
||||||
|
|
||||||
|
# 添加启用的服务
|
||||||
|
for service in config.notifications.services:
|
||||||
|
if service.enabled:
|
||||||
|
try:
|
||||||
|
self.apprise_obj.add(service.url)
|
||||||
|
logger.info(f"已加载通知服务: {service.name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"加载通知服务失败 {service.name}: {e}")
|
||||||
|
|
||||||
|
if len(self.apprise_obj) == 0:
|
||||||
|
logger.warning("没有可用的通知服务")
|
||||||
|
|
||||||
|
def reload_services(self) -> None:
|
||||||
|
"""重新加载通知服务."""
|
||||||
|
logger.info("重新加载通知服务...")
|
||||||
|
self._load_services()
|
||||||
|
|
||||||
|
def send_sms_notification(self, sms: SMSMessage) -> bool:
|
||||||
|
"""发送短信通知."""
|
||||||
|
config = get_config()
|
||||||
|
template = config.notifications.templates.sms
|
||||||
|
|
||||||
|
# 格式化通知内容
|
||||||
|
title = template.title.format(
|
||||||
|
sender=sms.sender,
|
||||||
|
timestamp=sms.timestamp.strftime("%Y-%m-%d %H:%M:%S") if sms.timestamp else "未知"
|
||||||
|
)
|
||||||
|
|
||||||
|
body = template.body.format(
|
||||||
|
sender=sms.sender,
|
||||||
|
content=sms.content,
|
||||||
|
timestamp=sms.timestamp.strftime("%Y-%m-%d %H:%M:%S") if sms.timestamp else "未知"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._send_notification(title, body)
|
||||||
|
|
||||||
|
def send_system_notification(self, message: str) -> bool:
|
||||||
|
"""发送系统通知."""
|
||||||
|
config = get_config()
|
||||||
|
template = config.notifications.templates.system
|
||||||
|
|
||||||
|
title = template.title
|
||||||
|
body = template.body.format(message=message)
|
||||||
|
|
||||||
|
return self._send_notification(title, body)
|
||||||
|
|
||||||
|
def _send_notification(self, title: str, body: str) -> bool:
|
||||||
|
"""发送通知."""
|
||||||
|
try:
|
||||||
|
if len(self.apprise_obj) == 0:
|
||||||
|
logger.error("没有可用的通知服务")
|
||||||
|
self.stats["failed"] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 发送通知
|
||||||
|
result = self.apprise_obj.notify(
|
||||||
|
title=title,
|
||||||
|
body=body
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info(f"通知发送成功: {title}")
|
||||||
|
self.stats["sent"] += 1
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"通知发送失败: {title}")
|
||||||
|
self.stats["failed"] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送通知时出错: {e}")
|
||||||
|
self.stats["failed"] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, int]:
|
||||||
|
"""获取统计信息."""
|
||||||
|
return self.stats.copy()
|
||||||
|
|
||||||
|
def get_services_info(self) -> List[Dict[str, str]]:
|
||||||
|
"""获取服务信息."""
|
||||||
|
config = get_config()
|
||||||
|
services_info = []
|
||||||
|
|
||||||
|
for service in config.notifications.services:
|
||||||
|
services_info.append({
|
||||||
|
"name": service.name,
|
||||||
|
"enabled": service.enabled,
|
||||||
|
"url_scheme": service.url.split("://")[0] if "://" in service.url else "unknown"
|
||||||
|
})
|
||||||
|
|
||||||
|
return services_info
|
||||||
|
|
||||||
|
|
||||||
|
# 全局通知管理器实例
|
||||||
|
notification_manager = NotificationManager()
|
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""测试包."""
|
57
tests/test_main.py
Normal file
57
tests/test_main.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""主应用测试."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from sms_forwarder.main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_root():
|
||||||
|
"""测试根路径."""
|
||||||
|
response = client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "message" in data
|
||||||
|
assert "version" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_check():
|
||||||
|
"""测试健康检查."""
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "healthy"
|
||||||
|
assert "timestamp" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_status():
|
||||||
|
"""测试状态接口."""
|
||||||
|
response = client.get("/status")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "version" in data
|
||||||
|
assert "uptime" in data
|
||||||
|
assert "notifications_sent" in data
|
||||||
|
assert "notifications_failed" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_notify_without_auth():
|
||||||
|
"""测试未认证的通知请求."""
|
||||||
|
response = client.post("/notify", json={
|
||||||
|
"api_key": "invalid-key",
|
||||||
|
"message": {
|
||||||
|
"sender": "test",
|
||||||
|
"content": "test message"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_notify_invalid_data():
|
||||||
|
"""测试无效数据的通知请求."""
|
||||||
|
response = client.post("/notify", json={
|
||||||
|
"invalid": "data"
|
||||||
|
})
|
||||||
|
assert response.status_code == 422
|
105
tests/test_notifier.py
Normal file
105
tests/test_notifier.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
"""通知模块测试."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from sms_forwarder.models import SMSMessage
|
||||||
|
from sms_forwarder.notifier import NotificationManager
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def notification_manager():
|
||||||
|
"""通知管理器测试夹具."""
|
||||||
|
with patch("sms_forwarder.notifier.get_config") as mock_get_config:
|
||||||
|
# 模拟配置
|
||||||
|
mock_config = MagicMock()
|
||||||
|
mock_config.notifications.services = [
|
||||||
|
MagicMock(name="test", url="test://test", enabled=True)
|
||||||
|
]
|
||||||
|
mock_config.notifications.templates.sms.title = "SMS: {sender}"
|
||||||
|
mock_config.notifications.templates.sms.body = "From: {sender}\nContent: {content}\nTime: {timestamp}"
|
||||||
|
mock_config.notifications.templates.system.title = "System"
|
||||||
|
mock_config.notifications.templates.system.body = "{message}"
|
||||||
|
|
||||||
|
mock_get_config.return_value = mock_config
|
||||||
|
|
||||||
|
manager = NotificationManager()
|
||||||
|
# 模拟 Apprise 对象
|
||||||
|
manager.apprise_obj = MagicMock()
|
||||||
|
manager.apprise_obj.__len__.return_value = 1
|
||||||
|
|
||||||
|
yield manager
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_sms_notification_success(notification_manager):
|
||||||
|
"""测试成功发送短信通知."""
|
||||||
|
notification_manager.apprise_obj.notify.return_value = True
|
||||||
|
|
||||||
|
sms = SMSMessage(
|
||||||
|
sender="Test Sender",
|
||||||
|
content="Test Message"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = notification_manager.send_sms_notification(sms)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert notification_manager.stats["sent"] == 1
|
||||||
|
assert notification_manager.stats["failed"] == 0
|
||||||
|
notification_manager.apprise_obj.notify.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_sms_notification_failure(notification_manager):
|
||||||
|
"""测试发送短信通知失败."""
|
||||||
|
notification_manager.apprise_obj.notify.return_value = False
|
||||||
|
|
||||||
|
sms = SMSMessage(
|
||||||
|
sender="Test Sender",
|
||||||
|
content="Test Message"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = notification_manager.send_sms_notification(sms)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
assert notification_manager.stats["sent"] == 0
|
||||||
|
assert notification_manager.stats["failed"] == 1
|
||||||
|
notification_manager.apprise_obj.notify.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_system_notification(notification_manager):
|
||||||
|
"""测试发送系统通知."""
|
||||||
|
notification_manager.apprise_obj.notify.return_value = True
|
||||||
|
|
||||||
|
result = notification_manager.send_system_notification("Test System Message")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert notification_manager.stats["sent"] == 1
|
||||||
|
notification_manager.apprise_obj.notify.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_services_available(notification_manager):
|
||||||
|
"""测试没有可用服务时的行为."""
|
||||||
|
# 模拟没有可用服务
|
||||||
|
notification_manager.apprise_obj.__len__.return_value = 0
|
||||||
|
|
||||||
|
sms = SMSMessage(
|
||||||
|
sender="Test Sender",
|
||||||
|
content="Test Message"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = notification_manager.send_sms_notification(sms)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
assert notification_manager.stats["failed"] == 1
|
||||||
|
# 应该不会调用 notify
|
||||||
|
notification_manager.apprise_obj.notify.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_stats(notification_manager):
|
||||||
|
"""测试获取统计信息."""
|
||||||
|
notification_manager.stats = {"sent": 5, "failed": 2}
|
||||||
|
|
||||||
|
stats = notification_manager.get_stats()
|
||||||
|
|
||||||
|
assert stats == {"sent": 5, "failed": 2}
|
||||||
|
# 确保返回的是副本
|
||||||
|
assert stats is not notification_manager.stats
|
Reference in New Issue
Block a user