diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..59bc1ff --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c92cbd8 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f35a6c8 --- /dev/null +++ b/README.md @@ -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) diff --git a/config.py b/config.py index 9923856..697ee27 100644 --- a/config.py +++ b/config.py @@ -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) \ No newline at end of file diff --git a/config.json b/config/config.json similarity index 88% rename from config.json rename to config/config.json index f4afcb9..2380ae9 100644 --- a/config.json +++ b/config/config.json @@ -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", diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..93647a2 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/requirements.txt b/requirements.txt index 5998238..dcf80e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ aiohttp>=3.8.0 -asyncio \ No newline at end of file +asyncio +pytz \ No newline at end of file diff --git a/scheduler.py b/scheduler.py index 23c8cf6..b585160 100644 --- a/scheduler.py +++ b/scheduler.py @@ -21,17 +21,38 @@ class NotificationScheduler: def _is_silent_time(self) -> bool: """Check if current time is within silent hours.""" - current_time = datetime.now().time() + import pytz + + # Get the configured timezone + timezone_name = getattr(self.config, 'timezone', 'UTC') + try: + tz = pytz.timezone(timezone_name) + current_datetime = datetime.now(tz) + time_type = f"timezone {timezone_name}" + except pytz.exceptions.UnknownTimeZoneError: + logger.warning(f"Unknown timezone '{timezone_name}', falling back to UTC") + tz = pytz.UTC + current_datetime = datetime.now(tz) + time_type = "UTC (fallback)" + + 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')} ({time_type})") + 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."""