Compare commits

...

2 Commits

8 changed files with 250 additions and 9 deletions

53
.dockerignore Normal file
View File

@ -0,0 +1,53 @@
# 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
# IDEs
.vscode
.idea
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Claude
.claude
# Documentation
README.md
CLAUDE.md
# Test files
test_*.py
*_test.py
quick_test.py

52
Dockerfile Normal file
View File

@ -0,0 +1,52 @@
# Multi-stage build for minimal image size
FROM python:3.11-alpine AS builder
# Install build dependencies
RUN apk add --no-cache --virtual .build-deps \
gcc \
musl-dev \
libffi-dev \
&& rm -rf /var/cache/apk/*
# Set working directory
WORKDIR /app
# Copy requirements first for better caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir --user -r requirements.txt
# Final stage - use Alpine without Python to copy only what's needed
FROM alpine:3.18
# Install runtime dependencies only
RUN apk add --no-cache \
python3 \
py3-pip \
&& rm -rf /var/cache/apk/*
# Create non-root user for security
RUN addgroup -g 1000 appuser && \
adduser -D -s /bin/sh -u 1000 -G appuser appuser
# Set working directory
WORKDIR /app
# Copy Python packages from builder stage
COPY --from=builder /root/.local /home/appuser/.local
# Copy application code
COPY --chown=appuser:appuser . .
# Switch to non-root user
USER appuser
# Add local bin to PATH
ENV PATH=/home/appuser/.local/bin:$PATH
# Expose port if needed (adjust as required)
# EXPOSE 8000
# Run the application
CMD ["python3", "main.py"]

76
README.md Normal file
View File

@ -0,0 +1,76 @@
# Dom
A notification scheduling system with Docker support.
## Quick Start with Docker Compose
1. **Build and start the service:**
```bash
docker-compose up -d
```
2. **View logs:**
```bash
docker-compose logs -f
```
3. **Stop the service:**
```bash
docker-compose down
```
## Configuration
The configuration file is located in the `config/` directory and is mapped as a volume, allowing you to modify it without rebuilding the container.
### Modifying Configuration
1. Edit `config/config.json` on your host machine
2. The changes will be automatically picked up by the container
3. Restart the container if needed: `docker-compose restart`
### Configuration Options
- `ollama_endpoint`: Ollama API endpoint
- `ollama_model`: Model to use for text generation
- `silent_start`: Start time for silent period (HH:MM)
- `silent_end`: End time for silent period (HH:MM)
- `timezone`: Timezone for scheduling
- `min_interval`: Minimum interval between notifications (minutes)
- `max_interval`: Maximum interval between notifications (minutes)
- `bark_api_url`: Bark notification service URL
- `bark_device_key`: Bark device key
- `ntfy_api_url`: Ntfy notification service URL
- `ntfy_topic`: Ntfy topic
- `ntfy_access_token`: Ntfy access token
- `templates_dir`: Directory containing notification templates
## Development
### Local Development
```bash
python3 main.py
```
### Building Docker Image
```bash
docker build -t dom .
```
### Running with Docker
```bash
docker run -d \
--name dom \
-v $(pwd)/config:/app/config \
-v $(pwd)/templates:/app/templates \
dom
```
## Volumes
- `./config:/app/config`: Configuration directory (read-write)
- `./templates:/app/templates`: Templates directory (read-only)
## Environment Variables
- `TZ`: Timezone (default: Asia/Shanghai)

View File

@ -13,6 +13,7 @@ class Config:
# Silent time configuration (12pm to 8am)
silent_start: str = "20:00"
silent_end: str = "12:00"
timezone: str = "UTC"
# Interval configuration (in minutes)
min_interval: int = 3
@ -32,7 +33,7 @@ class Config:
def __post_init__(self):
"""Load configuration from file if exists."""
config_file = Path("config.json")
config_file = Path("config/config.json")
if config_file.exists():
with open(config_file, 'r') as f:
data = json.load(f)
@ -42,6 +43,6 @@ class Config:
def save(self):
"""Save current configuration to file."""
config_file = Path("config.json")
config_file = Path("config/config.json")
with open(config_file, 'w') as f:
json.dump(asdict(self), f, indent=2)

View File

@ -2,7 +2,8 @@
"ollama_endpoint": "http://192.168.2.245:11434",
"ollama_model": "goekdenizguelmez/JOSIEFIED-Qwen3:8b",
"silent_start": "20:00",
"silent_end": "12:00",
"silent_end": "07:00",
"timezone": "Asia/Shanghai",
"min_interval": 3,
"max_interval": 180,
"bark_api_url": "https://bark.xorbitlab.xyz",

28
docker-compose.yml Normal file
View File

@ -0,0 +1,28 @@
version: '3.8'
services:
dom:
build: .
container_name: dom
restart: unless-stopped
volumes:
# Map the config directory for external modification
- ./config:/app/config:rw
# Map templates directory if needed
- ./templates:/app/templates:rw
environment:
# Set timezone for the container
- TZ=Asia/Shanghai
networks:
- dom-network
# Health check to ensure the service is running
healthcheck:
test: ["CMD", "python3", "-c", "import sys; sys.exit(0)"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
dom-network:
driver: bridge

View File

@ -1,2 +1,3 @@
aiohttp>=3.8.0
asyncio
asyncio
pytz

View File

@ -18,20 +18,48 @@ class NotificationScheduler:
self.template_manager = TemplateManager(config.templates_dir)
self.notification_client = NotificationClient(config)
self.running = False
self._timezone = None
self._timezone_name = None
self._init_timezone()
def _init_timezone(self):
"""Initialize timezone from config."""
import pytz
timezone_name = getattr(self.config, 'timezone', 'UTC')
try:
self._timezone = pytz.timezone(timezone_name)
self._timezone_name = timezone_name
except pytz.exceptions.UnknownTimeZoneError:
logger.warning(f"Unknown timezone '{timezone_name}', falling back to UTC")
self._timezone = pytz.UTC
self._timezone_name = "UTC (fallback)"
def _get_current_time(self):
"""Get current time in configured timezone."""
return datetime.now(self._timezone)
def _is_silent_time(self) -> bool:
"""Check if current time is within silent hours."""
current_time = datetime.now().time()
current_datetime = self._get_current_time()
current_time = current_datetime.time()
# Parse silent time configuration
silent_start = datetime.strptime(self.config.silent_start, "%H:%M").time()
silent_end = datetime.strptime(self.config.silent_end, "%H:%M").time()
# Log current time for debugging
logger.info(f"Current time: {current_datetime.strftime('%Y-%m-%d %H:%M:%S')} (timezone {self._timezone_name})")
logger.info(f"Silent period: {self.config.silent_start} to {self.config.silent_end}")
# Handle overnight silent period (e.g., 20:00 to 12:00)
if silent_start >= silent_end:
return current_time >= silent_start or current_time <= silent_end
is_silent = current_time >= silent_start or current_time <= silent_end
else:
return silent_start <= current_time <= silent_end
is_silent = silent_start <= current_time <= silent_end
logger.info(f"Silent time check: {is_silent}")
return is_silent
def _get_next_interval(self) -> int:
"""Get random interval in seconds between min and max."""
@ -89,8 +117,9 @@ class NotificationScheduler:
# Calculate next interval
interval = self._get_next_interval()
next_time = datetime.now() + timedelta(seconds=interval)
logger.info(f"Next notification scheduled for: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
current_time = self._get_current_time()
next_time = current_time + timedelta(seconds=interval)
logger.info(f"Next notification scheduled for: {next_time.strftime('%Y-%m-%d %H:%M:%S')} (timezone {self._timezone_name})")
# Wait for next interval
await asyncio.sleep(interval)